diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 509e808..fe04e64 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -43,35 +43,4 @@ jobs:
build-platform: ${{ matrix.build.platform }}
package: true
go-version: '1.23'
-
- build-container:
- runs-on: ubuntu-latest
- permissions:
- contents: read
- packages: write
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v3
-
- - name: Log in to the Container registry
- uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
- with:
- registry: ${{ env.REGISTRY }}
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Extract metadata (tags, labels) for Docker
- id: meta
- uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
- with:
- images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
-
- - name: Build and push Docker image
- uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
- with:
- context: .
- push: true
- tags: ${{ steps.meta.outputs.tags }}
- labels: ${{ steps.meta.outputs.labels }}
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 5a56260..7c0bd85 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -1,4 +1,4 @@
-name: Wails build
+name: Fyne build
on:
push:
@@ -10,6 +10,7 @@ env:
NODE_OPTIONS: "--max-old-space-size=4096"
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
+ CGO_ENABLED: 1
jobs:
build:
@@ -19,57 +20,61 @@ jobs:
matrix:
build:
- name: 'gogallery'
- platform: 'linux/amd64'
os: 'ubuntu-latest'
+ goos: 'linux'
+ goarch: 'amd64'
- name: 'gogallery'
- platform: 'windows/amd64'
os: 'windows-latest'
+ goos: 'windows'
+ goarch: 'amd64'
- name: 'gogallery'
- platform: 'darwin/universal'
os: 'macos-latest'
+ goos: 'darwin'
+ goarch: 'amd64'
+ - name: 'gogallery'
+ os: 'macos-latest'
+ goos: 'darwin'
+ goarch: 'arm64'
runs-on: ${{ matrix.build.os }}
steps:
- name: Checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
with:
submodules: recursive
- - name: Build wails
- uses: dAppServer/wails-build-action@v2.2
- id: build
+ - name: Set up Go
+ uses: actions/setup-go@v4
with:
- build-name: ${{ matrix.build.name }}
- build-platform: ${{ matrix.build.platform }}
- package: false
- go-version: '1.21'
- build-and-push-image:
- runs-on: ubuntu-latest
- permissions:
- contents: read
- packages: write
+ go-version: '>=1.24.2'
+ check-latest: true
- steps:
- - name: Checkout repository
- uses: actions/checkout@v3
+ - name: Install Fyne dependencies (Ubuntu)
+ if: matrix.build.os == 'ubuntu-latest'
+ run: |
+ make fyne-deps-ubuntu
- - name: Log in to the Container registry
- uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
- with:
- registry: ${{ env.REGISTRY }}
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Install Fyne dependencies (macOS)
+ if: matrix.build.os == 'macos-latest'
+ run: |
+ # macOS has the necessary frameworks built-in
- - name: Extract metadata (tags, labels) for Docker
- id: meta
- uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
- with:
- images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ - name: Install Fyne CLI
+ run: make fyne-cli
+
+ - name: Build Fyne app
+ env:
+ GOOS: ${{ matrix.build.goos }}
+ GOARCH: ${{ matrix.build.goarch }}
+ CGO_ENABLED: 1
+ shell: bash
+ run: |
+ make fyne-build
- - name: Build and push Docker image
- uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v4
with:
- context: .
- push: true
- tags: ${{ steps.meta.outputs.tags }}
- labels: ${{ steps.meta.outputs.labels }}
+ name: ${{ matrix.build.name }}-${{ matrix.build.goos }}-${{ matrix.build.goarch }}
+ path: |
+ ${{ matrix.build.name }}*
+
diff --git a/.gitignore b/.gitignore
index 07b49cb..6402c95 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,7 +28,9 @@ temp
# production
/build
-ui
+.env
+.sql
+
# misc
.DS_Store
.env.local
@@ -36,6 +38,7 @@ ui
.env.test.local
.env.production.local
+themes/**/package-lock.json
npm-debug.log*
yarn-debug.log*
yarn-error.log*
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 465bfc0..c667525 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -7,7 +7,7 @@
"mode": "auto",
"cwd": "${workspaceFolder}",
"program": "main.go",
- "args": ["dev"]
+ // "args": ["serve"]
}
]
}
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index fcfcd4e..0000000
--- a/Dockerfile
+++ /dev/null
@@ -1,27 +0,0 @@
-FROM node:lts-alpine as UI_BUILDER
-ARG VER
-WORKDIR /frontend
-ADD /frontend .
-RUN npm i; npm run build;
-
-FROM golang:1.23.1 as GO_BUILDER
-ARG VER
-WORKDIR /server
-ADD go.mod .
-ADD go.sum .
-ADD main.go main.go
-ADD backend backend
-ADD themes themes
-COPY --from=UI_BUILDER /frontend/dist /server/frontend/dist
-RUN CGO_ENABLED=1 GOOS=linux go build
-
-FROM ubuntu
-LABEL org.opencontainers.image.source="https://github.com/robrotheram/gogallery"
-WORKDIR /app
-COPY --from=GO_BUILDER /server/gogallery /app/gogallery
-COPY config_sample.yml /app/config.yml
-ENV GLLRY_SERVER_PORT ":80"
-ENV GLLRY_GALLERY_BASEPATH "/app/pictures"
-ENV GLLRY_GALLERY_THEME "default"
-WORKDIR /app
-ENTRYPOINT ["/app/gogallery", "--config", "/app/config.yml", "serve"]
\ No newline at end of file
diff --git a/themes/eastnor/assets/logos/logo192.png b/Icon.png
similarity index 100%
rename from themes/eastnor/assets/logos/logo192.png
rename to Icon.png
diff --git a/Makefile b/Makefile
index b5b955c..617580f 100644
--- a/Makefile
+++ b/Makefile
@@ -3,8 +3,8 @@ GOBUILD=$(GOCMD) build
GOCLEAN=$(GOCMD) clean
GOTEST=$(GOCMD) test
GOGET=$(GOCMD) get
-BINARY_NAME=../gogallery
-BINARY_UNIX=$(BINARY_NAME)_unix
+FYNE=fyne
+BINARY_NAME=gogallery
ifndef CIRCLE_BRANCH
override CIRCLE_BRANCH = latest
@@ -12,32 +12,57 @@ else
override CIRCLE_BRANCH = $(shell git rev-parse --abbrev-ref HEAD | sed 's/[^a-zA-Z0-9]/-/g')
endif
-all: clean test build
+build-themes:
+ @echo "Building themes..."
+ @for theme in themes/*; do \
+ if [ -d "$$theme" ]; then \
+ echo "Building theme: $$theme"; \
+ cd "$$theme" && npm install && npm run build && npm run clean; \
+ cd -; \
+ fi; \
+ done
-dep:
- go install github.com/wailsapp/wails/v2/cmd/wails@latest
+build:
+ @echo "Building GoGallery..."
+ $(GOBUILD) -o $(BINARY_NAME) -v .
-test:
- cd server && $(GOTEST) -v ./...
+# Cross-platform Fyne build (mimics CI logic)
+.PHONY: fyne-build
+fyne-build:
+ @echo "Building GoGallery with Fyne packaging..."
+ go mod download
+ @export GOOS=$(GOOS); export GOARCH=$(GOARCH); export CGO_ENABLED=1; \
+ if [ "$(GOOS)" = "windows" ]; then \
+ OUTPUT_NAME="$(BINARY_NAME).exe"; \
+ $(FYNE) package -os windows -name "$$OUTPUT_NAME" .; \
+ elif [ "$(GOOS)" = "darwin" ]; then \
+ $(FYNE) package -os darwin -name "$(BINARY_NAME).app" .; \
+ else \
+ OUTPUT_NAME="$(BINARY_NAME)"; \
+ $(FYNE) package -os linux -name "$$OUTPUT_NAME" .; \
+ fi
-package:
- tar -czvf gogallery-linux-amd64.tgz gogallery config_sample.yml ui
-# Cross compilation
-build-linux:
- wails build -tags webkit2_41
-build-windows:
- CC=x86_64-w64-mingw32-gccGOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ wails build -skipbindings
+# Install Fyne CLI
+.PHONY: fyne-cli
+fyne-cli:
+ $(GOCMD) install fyne.io/tools/cmd/fyne@latest
-docker:
- docker build . -t robrotheram/gogallery:$(CIRCLE_BRANCH)
- docker build . -t robrotheram/gogallery:latest
-docker-publish:
- docker push robrotheram/gogallery:$(CIRCLE_BRANCH)
- docker push robrotheram/gogallery:latest
+# Install Fyne dependencies (Ubuntu)
+.PHONY: fyne-deps-ubuntu
+fyne-deps-ubuntu:
+ sudo apt-get update
+ sudo apt-get install -y gcc libgl1-mesa-dev xorg-dev libxkbcommon-dev
-install:
- cp build/bin/gogallery /home/${HOME}/.local/bin/gogallery
+# Install Fyne dependencies (RedHat/Fedora)
+.PHONY: fyne-deps-fedora
+fyne-deps-fedora:
+ sudo dnf install -y gcc mesa-libGL-devel libX11-devel libxkbcommon-devel
-update:
- go get -u
- go mod tidy
\ No newline at end of file
+# Build all (default)
+.PHONY: all
+all: fyne-cli fyne-build
+
+# Clean
+.PHONY: clean
+clean:
+ rm -rf $(BINARY_NAME) $(BINARY_NAME).exe $(BINARY_NAME).app
\ No newline at end of file
diff --git a/Readme.md b/Readme.md
index 666d096..92d66c9 100644
--- a/Readme.md
+++ b/Readme.md
@@ -25,14 +25,11 @@ gogallery [flags]
### SEE ALSO
* [gogallery build](docs/cli/gogallery_build.md) - build static site
-* [gogallery completion](docs/cli/gogallery_completion.md) - Generate the autocompletion script for the specified shell
-* [gogallery dashboard](docs/cli/gogallery_dashboard.md) - dashboard
* [gogallery deploy](docs/cli/gogallery_deploy.md) - deploy static site
-* [gogallery docs](docs/cli/gogallery_docs.md) - cli docs
-* [gogallery init](docs/cli/gogallery_init.md) - create site
* [gogallery serve](docs/cli/gogallery_serve.md) - serve static site
* [gogallery template](docs/cli/gogallery_template.md) - extract template
+
---
## History
@@ -49,104 +46,70 @@ Demo at https://gallery.exceptionerror.io
## Screenshots
-### Gallery
-
-
-
-### Dashboard:
-
-
-
-
-
-
-## Installation
-Makefile to the rescue
-
-### build all
-```
- make
-```
+### Dashboard/App
-### build dashboard
-```
- make build-dashboard
-```
-### build server
-```
- make build-server
-```
-
-
-## Usage
+| Dashboard Home | Settings | Tasks |
+|----------------|----------|-------|
+|  |  |  |
-Edit the config and change the name basepath and base folder that is used for scanning images
+| Album View | Sidebar |
+|------------|---------|
+|  |  |
-#### Dashboard login
-Gogallery on first run will autocreate a admin accound with username `admin` and a 8 character autogenerated password which you will find in the log. Once loged in you can go to settings and user to set it
+### Generated Website
-Dashboard url `%GALLERY_PATH/dashboard`
+| Website Home | Photo Page | Collection Page |
+|--------------|------------|-----------------|
+|  |  |  |
-If you forget the admin password for any reason you can use the `gogallery --reset-admin` which will recrate the admin username and password
+## Contributing
+Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
-## Themes
-
-Version 5.x has support for users themes. The theme engine uses Go’s html/template and text/template libraries as the basis for the templating. This is similar to the one hugo uses https://gohugo.io/templates/introduction/ but with the varibles being different.
-
-All Themes will need to have the following pages in order for the site to work:
- - 404.hbs
- - albums.hbs
- - collections.hbs
- - main.hbs
- - photo.hbs
+Please make sure to update tests as appropriate.
-Optionally you can have a *"default.hbs"* to define common heading and footers
-The engine has support for partials that can be stored in the subfolder partials and all other assests can be stored the assets folder.
+## License
+[apache-2.0](https://choosealicense.com/licenses/apache-2.0)
-See the example theme **eastnor** for reference
-### Caching
-The server implements its own caching layer after the page has been visited it will cache the page in memory so next load of that page becomes very quick this can produce 10X improvement of load times.
+## Building GoGallery
-The Cache gets invalidated if the server restarts or if you have made a change in the dashboard.
+You can build GoGallery from source using Go. Make sure you have Go 1.20 or newer installed.
+### Standard Build (CLI and Web)
-#### Docker Configuration
-Config can be also edited via environmental variables
-
+You can use the provided Makefile for building:
+```bash
+make build # Build the GoGallery CLI/web binary
+make build-themes # Build all theme assets (npm install/build/clean in each theme)
+make clean # Remove built binaries
```
-GLLRY_SERVER_PORT
-GLLRY_SERVER_WORKERS
-GLLRY_SERVER_CAPTIONURL
+This will produce the `gogallery` binary in your current directory.
-GLLRY_DATABASE_BASEURL
+Or, to build manually:
-GLLRY_GALLERY_NAME
-GLLRY_GALLERY_BASEPATH
-GLLRY_GALLERY_URL
-GALLRY_GALLERY_THEME
-GALLRY_GALLERY_PICTUREBLACKLIST
-GALLRY_GALLERY_ALBUMBLACKLIST
+```bash
+go build -o gogallery main.go
+```
-GLLRY_ABOUT_INSTAGRAM
-GLLRY_ABOUT_TWITTER
-GLLRY_ABOUT_EMAIL
+### Building the Fyne Desktop App
-GLLRY_ABOUT_WEBSITE
-GLLRY_ABOUT_DESCRIPTION
-GLLRY_ABOUT_PHOTOGRAPHER
-GLLRY_ABOUT_BACKGROUNDPHOTO
-GLLRY_ABOUT_PROFILEPHOTO
+You can use the Makefile to build the Fyne desktop app and install dependencies:
+```bash
+make fyne-cli # Install the Fyne CLI tool
+make fyne-deps-ubuntu # Install Fyne dependencies (Ubuntu)
+make fyne-deps-fedora # Install Fyne dependencies (Fedora/RedHat)
+make fyne-build # Build the Fyne desktop app for your platform
```
-## Contributing
-Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
+- The `fyne-build` target will auto-detect your OS and build the appropriate package.
+- You can also run `make all` to install Fyne CLI and build the desktop app in one step.
-Please make sure to update tests as appropriate.
+> For more details, see the [Fyne documentation](https://developer.fyne.io/started/packaging) and the GoGallery wiki.
-## License
-[apache-2.0](https://choosealicense.com/licenses/apache-2.0)
+## Theme Development
+
+See the [Theme Development Guide](themes/DEVELOPER_README.md) for instructions on building and customizing GoGallery templates and themes.
diff --git a/backend/api/collections.go b/backend/api/collections.go
deleted file mode 100644
index 0e1451f..0000000
--- a/backend/api/collections.go
+++ /dev/null
@@ -1,142 +0,0 @@
-package api
-
-import (
- "encoding/json"
- "fmt"
- "net/http"
- "os"
- "path/filepath"
- "time"
-
- "github.com/gorilla/mux"
- "github.com/robrotheram/gogallery/backend/config"
- "github.com/robrotheram/gogallery/backend/datastore"
- "github.com/robrotheram/gogallery/backend/datastore/models"
-)
-
-type Collections struct {
- CaptureDates []string `json:"dates"`
- UploadDates []string `json:"uploadDates"`
- Albums map[string]models.Album `json:"albums"`
-}
-
-type MoveCollection struct {
- Album string `json:"album"`
- Photos []models.Picture `json:"photos"`
-}
-
-func (api *GoGalleryAPI) moveCollectionHandler(w http.ResponseWriter, r *http.Request) {
- var moveCollection MoveCollection
- _ = json.NewDecoder(r.Body).Decode(&moveCollection)
- for _, photo := range moveCollection.Photos {
- oldPicture := api.db.Pictures.FindByID(photo.Id)
- if oldPicture.Album != moveCollection.Album {
- api.db.Albums.MovePictureToAlbum(&photo, moveCollection.Album)
- photo.Meta.DateModified = time.Now()
- api.db.Pictures.Save(&photo)
- }
- }
-
-}
-
-func (api *GoGalleryAPI) updateCollectionHandler(w http.ResponseWriter, r *http.Request) {
- albumID := mux.Vars(r)["id"]
- oldAlbum := api.db.Albums.FindByID(albumID)
-
- var album models.Album
- _ = json.NewDecoder(r.Body).Decode(&album)
-
- if oldAlbum.Name != album.Name {
- oldPath := fmt.Sprintf("%s/%s", filepath.Dir(oldAlbum.ParenetPath), oldAlbum.Name)
- newPath := fmt.Sprintf("%s/%s", filepath.Dir(oldAlbum.ParenetPath), album.Name)
- os.Rename(oldPath, newPath)
- oldAlbum.Name = album.Name
- }
-
- if oldAlbum.ProfileID != album.ProfileID {
- oldAlbum.ProfileID = album.ProfileID
- }
-
- if oldAlbum.GPS.Lat != album.GPS.Lat || oldAlbum.GPS.Lng != album.GPS.Lng {
- oldAlbum.GPS = album.GPS
- }
-
- api.db.Albums.Save(&oldAlbum)
-
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(oldAlbum)
-}
-
-func (api *GoGalleryAPI) createCollectionHandler(w http.ResponseWriter, r *http.Request) {
- var album models.Album
- _ = json.NewDecoder(r.Body).Decode(&album)
-
- path := ""
- if album.Id != "" {
- newAlbum := api.db.Albums.FindByID(album.Id)
- path = fmt.Sprintf("%s/%s/%s", newAlbum.ParenetPath, newAlbum.Name, album.Name)
- } else {
- path = fmt.Sprintf("%s/%s", api.config.Gallery.Basepath, album.Name)
- }
-
- album.Id = config.GetMD5Hash(path)
- album.ParenetPath = filepath.Dir(path)
- album.ModTime = time.Now()
- album.Children = make(map[string]models.Album)
-
- fmt.Println(album)
- if _, err := os.Stat(path); os.IsNotExist(err) {
- os.Mkdir(path, 0755)
- }
-
- api.db.Albums.Save(&album)
-}
-
-func (api *GoGalleryAPI) getCollectionPhotosHandler(w http.ResponseWriter, r *http.Request) {
- albumID := mux.Vars(r)["id"]
- pictures := api.db.Pictures.FindManyFeild("Album", albumID)
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(pictures)
-}
-
-func (api *GoGalleryAPI) getCollectionHandler(w http.ResponseWriter, r *http.Request) {
- albumID := mux.Vars(r)["id"]
- album := api.db.Albums.FindByID(albumID)
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(album)
-}
-
-func (api *GoGalleryAPI) getCollectionsHandler(w http.ResponseWriter, r *http.Request) {
-
- pics := api.db.Pictures.GetAll()
- albms := api.db.Albums.GetAll()
- newalbms := datastore.SliceToTree(albms, api.config.Gallery.Basepath)
- dates := []string{}
- uploadDates := []string{}
- for _, pic := range pics {
- _date := pic.Exif.DateTaken.Format("2006-01-02")
- _uploadDate := pic.Meta.DateAdded.Format("2006-01-02 15:04")
-
- found := false
- uploadFound := false
-
- for _, date := range dates {
- if date == _date {
- found = true
- }
- }
- for _, date := range uploadDates {
- if date == _uploadDate {
- uploadFound = true
- }
- }
- if !found {
- dates = append(dates, _date)
- }
- if !uploadFound {
- uploadDates = append(uploadDates, _uploadDate)
- }
- }
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(Collections{CaptureDates: dates, UploadDates: uploadDates, Albums: newalbms})
-}
diff --git a/backend/api/error.go b/backend/api/error.go
deleted file mode 100644
index 7cd8d88..0000000
--- a/backend/api/error.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package api
-
-import (
- "encoding/json"
- "net/http"
-)
-
-type APIError struct {
- Message string `json:"message"`
- error
-}
-
-func NewAPIError(err error) *APIError {
- return &APIError{
- Message: "Issue Dealing with the request",
- error: err,
- }
-}
-
-func (a *APIError) HandleError(w http.ResponseWriter) {
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(http.StatusBadRequest)
- json.NewEncoder(w).Encode(a)
-}
diff --git a/backend/api/photo.go b/backend/api/photo.go
deleted file mode 100644
index 8d159ff..0000000
--- a/backend/api/photo.go
+++ /dev/null
@@ -1,66 +0,0 @@
-package api
-
-import (
- "encoding/json"
- "fmt"
- "net/http"
- "os"
- "path/filepath"
- "time"
-
- "github.com/gorilla/mux"
- "github.com/robrotheram/gogallery/backend/datastore/models"
-)
-
-// r.Handle("/api/admin/photo/{id}", auth.AuthMiddleware(getPhotoHandler)).Methods("GET")
-// r.Handle("/api/admin/photo/{id}", auth.AuthMiddleware(editPhotoHandler)).Methods("POST")
-// r.Handle("/api/admin/photo/{id}", auth.AuthMiddleware(deletePhotoHandler)).Methods("DELETE")
-
-func (api *GoGalleryAPI) GetPicturesHandler(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(models.SortByTime(api.db.Pictures.GetAll()))
-}
-
-func (api *GoGalleryAPI) GetPictureHandler(w http.ResponseWriter, r *http.Request) {
- photoID := mux.Vars(r)["id"]
- picture := api.db.Pictures.FindByID(photoID)
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(picture)
-}
-
-func (api *GoGalleryAPI) UpdatePictureHandler(w http.ResponseWriter, r *http.Request) {
- photoID := mux.Vars(r)["id"]
- oldPicture := api.db.Pictures.FindByID(photoID)
-
- var picture models.Picture
- err := json.NewDecoder(r.Body).Decode(&picture)
- if err != nil {
- NewAPIError(err).HandleError(w)
- return
- }
- if oldPicture.Name != picture.Name {
- newName := fmt.Sprintf("%s/%s%s", filepath.Dir(oldPicture.Path), picture.Name, filepath.Ext(oldPicture.Path))
- os.Rename(oldPicture.Path, newName)
- picture.Path = newName
- }
-
- if oldPicture.Path != picture.Path {
- os.Rename(oldPicture.Path, picture.Path)
- }
-
- if oldPicture.Album != picture.Album {
- api.db.Albums.MovePictureToAlbum(&picture, picture.Album)
- }
- picture.Meta.DateModified = time.Now()
- api.db.Pictures.Save(&picture)
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(picture)
-}
-
-func (api *GoGalleryAPI) DeletePictureHandler(w http.ResponseWriter, r *http.Request) {
- photoID := mux.Vars(r)["id"]
- picture := api.db.Pictures.FindByID(photoID)
- api.db.Pictures.Delete(picture)
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(picture)
-}
diff --git a/backend/api/router.go b/backend/api/router.go
deleted file mode 100644
index 926d7e8..0000000
--- a/backend/api/router.go
+++ /dev/null
@@ -1,214 +0,0 @@
-package api
-
-import (
- "fmt"
- "io"
- "log"
- "net/http"
- "os"
- "path"
- "path/filepath"
- "strings"
-
- "github.com/gorilla/handlers"
- "github.com/gorilla/mux"
- "github.com/robrotheram/gogallery/backend/config"
- "github.com/robrotheram/gogallery/backend/datastore"
- "github.com/robrotheram/gogallery/backend/monitor"
- "github.com/robrotheram/gogallery/backend/pipeline"
- templateengine "github.com/robrotheram/gogallery/backend/templateEngine"
- "golang.org/x/net/html"
-)
-
-type GoGalleryAPI struct {
- db *datastore.DataStore
- config *config.Configuration
- router *mux.Router
- monitor *monitor.TasksMonitor
-}
-
-func NewGoGalleryAPI(config *config.Configuration, db *datastore.DataStore) *GoGalleryAPI {
- api := &GoGalleryAPI{config: config, db: db, monitor: monitor.NewMonitor()}
- api.router = mux.NewRouter()
- api.setupDashboardRoutes()
- return api
-}
-
-func (api *GoGalleryAPI) setupDashboardRoutes() {
- fmt.Println("Setting up API")
- api.router.HandleFunc("/img/{id}", api.ImgHandler)
- api.router.HandleFunc("/img/{id}/{size}.{ext}", api.ImgHandler)
-
- api.router.HandleFunc("/api/admin/photos", api.GetPicturesHandler).Methods("GET")
- api.router.HandleFunc("/api/admin/photo/{id}", api.GetPictureHandler).Methods("GET")
- api.router.HandleFunc("/api/admin/photo/{id}", api.UpdatePictureHandler).Methods("POST")
- api.router.HandleFunc("/api/admin/photo/{id}", api.DeletePictureHandler).Methods("DELETE")
-
- api.router.HandleFunc("/api/admin/collection/move", api.moveCollectionHandler).Methods("POST")
- api.router.HandleFunc("/api/admin/collection/uploadFile", api.uploadFileHandler).Methods("POST")
- api.router.HandleFunc("/api/admin/collection/upload", api.uploadHandler).Methods("POST")
-
- api.router.HandleFunc("/api/admin/collections", api.getCollectionsHandler).Methods("GET")
- api.router.HandleFunc("/api/admin/collection", api.createCollectionHandler).Methods("POST")
- api.router.HandleFunc("/api/admin/collection/{id}/photos", api.getCollectionPhotosHandler).Methods("GET")
- api.router.HandleFunc("/api/admin/collection/{id}", api.getCollectionHandler).Methods("GET")
- api.router.HandleFunc("/api/admin/collection/{id}", api.updateCollectionHandler).Methods("POST")
-
- api.router.HandleFunc("/api/admin/settings/stats", api.statsHandler).Methods("GET")
- api.router.HandleFunc("/api/admin/settings/gallery", api.getGallerySettings).Methods("GET")
- api.router.HandleFunc("/api/admin/settings/gallery", api.setGallerySettings).Methods("POST")
- api.router.HandleFunc("/api/admin/settings/profile", api.getProfileInfo).Methods("GET")
- api.router.HandleFunc("/api/admin/settings/profile", api.setProfileInfo).Methods("POST")
- api.router.HandleFunc("/api/admin/settings/deploy", api.getDeploymentSettings).Methods("GET")
- api.router.HandleFunc("/api/admin/settings/deploy", api.setDeploymentSettings).Methods("POST")
-
- api.router.HandleFunc("/api/admin/tasks", api.getTasks).Methods("GET")
- api.router.HandleFunc("/api/admin/tasks/purge", api.purgeTaskHandler).Methods("GET")
- api.router.HandleFunc("/api/admin/tasks/rescan", api.rescanTaskHandler).Methods("GET")
- api.router.HandleFunc("/api/admin/tasks/backup", api.backupTaskHandler).Methods("GET")
- api.router.HandleFunc("/api/admin/tasks/upload", api.uploadTaskHandler).Methods("POST")
-
- api.router.HandleFunc("/api/admin/tasks/build", api.buildTaskHandler).Methods("POST")
- api.router.HandleFunc("/api/admin/tasks/publish", api.deployTaskHandler).Methods("POST")
-}
-
-func (api *GoGalleryAPI) ImgHandler(w http.ResponseWriter, r *http.Request) {
- size := r.URL.Query().Get("size")
- vars := mux.Vars(r)
- id := vars["id"]
- if len(size) == 0 {
- size = vars["size"]
- }
- pic := api.db.Pictures.FindByID(id)
- //Is image in cache
- if file, err := api.db.ImageCache.Get(pic.Id, size); err == nil {
- io.Copy(w, file)
- return
- }
- src, err := pic.Load()
- if err != nil {
- return
- }
- cache, _ := api.db.ImageCache.Writer(pic.Id, size)
- writer := io.MultiWriter(w, cache)
- if size, ok := templateengine.ImageSizes[size]; ok {
- pipeline.ProcessImage(src, size, writer)
- return
- }
- pipeline.ProcessImage(src, templateengine.ImageSizes["xsmall"], writer)
-}
-
-func (api *GoGalleryAPI) DashboardAPI() {
- headers := handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Authorization"})
- methods := handlers.AllowedMethods([]string{"GET", "POST", "PUT", "HEAD", "DELETE", "OPTIONS"})
- origins := handlers.AllowedOrigins([]string{"*"})
-
- api.router.PathPrefix("/preview-build").Handler(&home{
- base: api.config.Gallery.Destpath,
- })
- spa := SPAHandler{StaticPath: "frontend/dist", IndexPath: "index.html"}
- api.router.PathPrefix("/").Handler(spa)
-
- log.Println("Starting api server on port: http://" + api.config.Server.GetLocalAddr())
- log.Fatal(http.ListenAndServe(api.config.Server.GetLocalAddr(), handlers.CORS(origins, headers, methods)(api.router)))
-}
-
-func (api *GoGalleryAPI) Serve() {
- fs := http.FileServer(http.Dir(api.config.Gallery.Destpath))
- http.Handle("/", fs)
- log.Println("Starting server on port: http://" + api.config.Server.GetAddr())
- log.Fatal(http.ListenAndServe(api.config.Server.GetAddr(), nil))
-}
-
-type home struct {
- base string
-}
-
-func getAttribute(n *html.Node, attributeName string) string {
- for _, attr := range n.Attr {
- if attr.Key == attributeName {
- return attr.Val
- }
- }
- return ""
-}
-
-func updateLinks(n *html.Node) {
- if n.Type == html.ElementNode {
- if n.Data == "a" || n.Data == "img" || (n.Data == "link" && getAttribute(n, "rel") == "stylesheet") {
- updateHrefAttribute(n, "href")
- updateHrefAttribute(n, "src")
- }
- }
-
- for c := n.FirstChild; c != nil; c = c.NextSibling {
- updateLinks(c)
- }
-}
-
-func updateHrefAttribute(n *html.Node, attributeName string) {
- if hrefAttr := findAttribute(n, attributeName); hrefAttr != nil {
- href := hrefAttr.Val
- if href != "" && !strings.HasPrefix(href, "http") {
- // Update the href attribute value by adding the "/dev/" prefix
- hrefAttr.Val = "/preview-build" + href
- }
- }
-}
-
-func findAttribute(n *html.Node, attributeName string) *html.Attribute {
- for i := range n.Attr {
- if n.Attr[i].Key == attributeName {
- return &n.Attr[i]
- }
- }
- return nil
-}
-
-func GetFileContentType(ouput *os.File) (string, error) {
- buf := make([]byte, 512)
- _, err := ouput.Read(buf)
- if err != nil {
- return "", err
- }
- contentType := http.DetectContentType(buf)
- if contentType == "text/plain; charset=utf-8" && filepath.Ext(ouput.Name()) == ".svg" {
- contentType = "image/svg+xml; charset=utf-8"
- } else if contentType == "text/plain; charset=utf-8" && filepath.Ext(ouput.Name()) == ".css" {
- contentType = "text/css; charset=utf-8"
- }
- ouput.Seek(0, 0)
- return contentType, nil
-}
-
-func IsHtml(constentType string) bool {
- return strings.Contains(constentType, "text/html")
-}
-
-func (h *home) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- data := strings.Replace(r.URL.Path, "/preview-build", "", -1)
- data = path.Join(h.base, data)
- fileInfo, err := os.Stat(data)
- if err != nil {
- return
- }
- if fileInfo.IsDir() {
- data = path.Join(data, "index.html")
- }
- file, err := os.Open(data)
- if err != nil {
- return
- }
- contentType, _ := GetFileContentType(file)
- if IsHtml(contentType) {
- doc, err := html.Parse(file)
- if err != nil {
- fmt.Println(err)
- }
- updateLinks(doc)
- html.Render(w, doc)
- } else {
- w.Header().Set("Content-Type", contentType)
- io.Copy(w, file)
- }
-}
diff --git a/backend/api/settings.go b/backend/api/settings.go
deleted file mode 100644
index ae56dba..0000000
--- a/backend/api/settings.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package api
-
-import (
- "encoding/json"
- "net/http"
- "os"
-
- "github.com/robrotheram/gogallery/backend/config"
-)
-
-type Stats struct {
- Photos int
- Albums int
- Rubish int
-}
-
-func (api *GoGalleryAPI) statsHandler(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "application/json")
- stats := Stats{0, 0, 0}
- stats.Photos = len(api.db.Pictures.GetAll())
- stats.Albums = len(api.db.Albums.GetAll())
- files, _ := os.ReadDir(config.Config.Gallery.Basepath + "/rubish")
- stats.Rubish = len(files)
- json.NewEncoder(w).Encode(stats)
-}
-
-func (api *GoGalleryAPI) getProfileInfo(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(api.config.About)
-}
-
-func (api *GoGalleryAPI) setProfileInfo(w http.ResponseWriter, r *http.Request) {
- var about = config.AboutConfiguration{}
- _ = json.NewDecoder(r.Body).Decode(&about)
- about.Save()
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(api.config.About)
-}
-
-func (api *GoGalleryAPI) getGallerySettings(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(api.config.Gallery)
-}
-
-func (api *GoGalleryAPI) getDeploymentSettings(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(api.config.Deploy)
-}
-
-func (api *GoGalleryAPI) getPublicGallerySettings(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "application/json")
- config := map[string]string{
- "name": api.config.Gallery.Name,
- }
- json.NewEncoder(w).Encode(config)
-}
-
-func (api *GoGalleryAPI) setGallerySettings(w http.ResponseWriter, r *http.Request) {
- var gallery = config.GalleryConfiguration{}
- _ = json.NewDecoder(r.Body).Decode(&gallery)
- gallery.Save()
-
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(api.config.Gallery)
-}
-
-func (api *GoGalleryAPI) setDeploymentSettings(w http.ResponseWriter, r *http.Request) {
- var deploy = config.DeployConfig{}
- _ = json.NewDecoder(r.Body).Decode(&deploy)
- deploy.Save()
-
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(api.config.Deploy)
-}
diff --git a/backend/api/spaHandler.go b/backend/api/spaHandler.go
deleted file mode 100644
index e5e78b2..0000000
--- a/backend/api/spaHandler.go
+++ /dev/null
@@ -1,50 +0,0 @@
-package api
-
-import (
- "net/http"
- "os"
- "path/filepath"
-)
-
-// spaHandler implements the http.Handler interface, so we can use it
-// to respond to HTTP requests. The path to the static directory and
-// path to the index file within that static directory are used to
-// serve the SPA in the given static directory.
-type SPAHandler struct {
- StaticPath string
- IndexPath string
-}
-
-// ServeHTTP inspects the URL path to locate a file within the static dir
-// on the SPA handler. If a file is found, it will be served. If not, the
-// file located at the index path on the SPA handler will be served. This
-// is suitable behavior for serving an SPA (single page application).
-func (h SPAHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- // get the absolute path to prevent directory traversal
- path, err := filepath.Abs(r.URL.Path)
- if err != nil {
- // if we failed to get the absolute path respond with a 400 bad request
- // and stop
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
-
- // prepend the path with the path to the static directory
- path = filepath.Join(h.StaticPath, path)
-
- // check whether a file exists at the given path
- _, err = os.Stat(path)
- if os.IsNotExist(err) {
- // file does not exist, serve index.html
- http.ServeFile(w, r, filepath.Join(h.StaticPath, h.IndexPath))
- return
- } else if err != nil {
- // if we got an error (that wasn't that the file doesn't exist) stating the
- // file, return a 500 internal server error and stop
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
-
- // otherwise, use http.FileServer to serve the static dir
- http.FileServer(http.Dir(h.StaticPath)).ServeHTTP(w, r)
-}
diff --git a/backend/api/tasks.go b/backend/api/tasks.go
deleted file mode 100644
index e2db2c3..0000000
--- a/backend/api/tasks.go
+++ /dev/null
@@ -1,91 +0,0 @@
-package api
-
-import (
- "bytes"
- "encoding/json"
- "fmt"
- "io"
- "net/http"
-
- "github.com/robrotheram/gogallery/backend/config"
- "github.com/robrotheram/gogallery/backend/datastore/models"
- "github.com/robrotheram/gogallery/backend/deploy"
- "github.com/robrotheram/gogallery/backend/pipeline"
-)
-
-type backup struct {
- Albums []models.Album `json:"albums"`
- Pictures []models.Picture `json:"pictures"`
- Config config.Configuration `json:"config"`
-}
-
-func (api *GoGalleryAPI) purgeTaskHandler(w http.ResponseWriter, r *http.Request) {
- stat := api.monitor.NewTask("Delete Site", 0)
- go func() {
- stat.Start()
- pipeline.NewRenderPipeline(&api.config.Gallery, api.db, api.monitor).DeleteSite()
- stat.End()
- }()
- fmt.Fprintf(w, "Deleted Site")
-}
-
-func (api *GoGalleryAPI) getTasks(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(api.monitor.GetTasks())
-}
-
-func (api *GoGalleryAPI) rescanTaskHandler(w http.ResponseWriter, r *http.Request) {
- stat := api.monitor.NewTask("Rescan", 0)
- go func() {
- stat.Start()
- api.db.ScanPath(api.config.Gallery.Basepath)
- stat.End()
- }()
- fmt.Fprintf(w, "rescan task started")
-}
-
-func (api *GoGalleryAPI) buildTaskHandler(w http.ResponseWriter, r *http.Request) {
- go pipeline.NewRenderPipeline(&api.config.Gallery, api.db, api.monitor).BuildSite()
- fmt.Fprintf(w, "Build task started")
-}
-
-func (api *GoGalleryAPI) deployTaskHandler(w http.ResponseWriter, r *http.Request) {
- go deploy.DeploySite(*api.config, api.monitor.NewTask("netify deploy", 0))
- fmt.Fprintf(w, "Deploy task started")
-}
-
-func (api *GoGalleryAPI) uploadTaskHandler(w http.ResponseWriter, r *http.Request) {
- bk := backup{}
- r.ParseMultipartForm(32 << 20)
- file, _, err := r.FormFile("file")
- if err != nil {
- fmt.Println(err)
- return
- }
- defer file.Close()
- buf := bytes.NewBuffer(nil)
- if _, err := io.Copy(buf, file); err != nil {
- fmt.Println(err)
- return
- }
- json.Unmarshal(buf.Bytes(), &bk)
- for _, p := range bk.Pictures {
- api.db.Pictures.Save(&p)
- }
- for _, a := range bk.Albums {
- api.db.Albums.Save(&a)
- }
- bk.Config.About.Save()
- bk.Config.Gallery.Save()
-}
-
-func (api *GoGalleryAPI) backupTaskHandler(w http.ResponseWriter, r *http.Request) {
- bk := backup{}
- bk.Albums = api.db.Albums.GetAll()
- bk.Pictures = api.db.Pictures.GetAll()
- bk.Config = *api.config
-
- w.Header().Set("Content-Disposition", "attachment; filename=Gallery-Backup.json")
- w.Header().Set("Content-Type", r.Header.Get("Content-Type"))
- json.NewEncoder(w).Encode(bk)
-}
diff --git a/backend/api/upload.go b/backend/api/upload.go
deleted file mode 100644
index 35a33ec..0000000
--- a/backend/api/upload.go
+++ /dev/null
@@ -1,100 +0,0 @@
-package api
-
-import (
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "os"
- "path/filepath"
- "strings"
- "time"
-
- "github.com/robrotheram/gogallery/backend/config"
- "github.com/robrotheram/gogallery/backend/datastore"
- "github.com/robrotheram/gogallery/backend/datastore/models"
-)
-
-type UploadCollection struct {
- Album string `json:"album"`
- Photos []string `json:"photos"`
-}
-
-func (api *GoGalleryAPI) uploadHandler(w http.ResponseWriter, r *http.Request) {
- var uploadCollection UploadCollection
- _ = json.NewDecoder(r.Body).Decode(&uploadCollection)
-
- album := api.db.Albums.FindByID(uploadCollection.Album)
- for _, photo := range uploadCollection.Photos {
- albumPath := fmt.Sprintf("%s/%s", album.ParenetPath, album.Name)
- newPath := fmt.Sprintf("%s/%s", albumPath, photo)
- oldPath := fmt.Sprintf("./temp/%s", config.GetMD5Hash(photo))
- err := datastore.MoveFile(oldPath, newPath)
- if err == nil {
- picName := strings.TrimSuffix(photo, filepath.Ext(photo))
- p := models.Picture{
- Id: config.GetMD5Hash(newPath),
- Name: picName,
- Path: newPath,
- Album: album.Id,
- Ext: filepath.Ext(newPath),
- Exif: models.Exif{},
- RootPath: api.config.Gallery.Basepath,
- Meta: models.PictureMeta{
- PostedToIG: false,
- Visibility: "PUBLIC",
- DateAdded: time.Now(),
- DateModified: time.Now()}}
- p.CreateExif()
- api.db.Pictures.Save(&p)
- }
- }
-}
-
-func (api *GoGalleryAPI) uploadFileHandler(w http.ResponseWriter, r *http.Request) {
- fmt.Println("File Upload Endpoint Hit")
- //photoID := mux.Vars(r)["id"]
-
- // Parse our multipart form, 10 << 20 specifies a maximum
- // upload of 10 MB files.
- r.ParseMultipartForm(10 << 20)
- // FormFile returns the first file for the given key `myFile`
- // it also returns the FileHeader so we can get the Filename,
- // the Header and the size of the file
- file, handler, err := r.FormFile("file")
- if err != nil {
- fmt.Println("Error Retrieving the File")
- fmt.Println(err)
- return
- }
- defer file.Close()
- fmt.Printf("Uploaded File: %+v\n", handler.Filename)
- fmt.Printf("File Size: %+v\n", handler.Size)
- fmt.Printf("MIME Header: %+v\n", handler.Header)
-
- // Create a temporary file within our temp-images directory that follows
- // a particular naming pattern
-
- if _, err := os.Stat("temp"); os.IsNotExist(err) {
- os.Mkdir("temp", 0755)
- }
-
- tfile, err := os.OpenFile("./temp/"+config.GetMD5Hash(handler.Filename), os.O_WRONLY|os.O_CREATE, 0666)
- if err != nil {
- fmt.Println(err)
- return
- }
- defer tfile.Close()
-
- // read all of the contents of our uploaded file into a
- // byte array
- fileBytes, err := io.ReadAll(file)
- if err != nil {
- fmt.Println(err)
- return
- }
- // write this byte array to our temporary file
- tfile.Write(fileBytes)
- // return that we have successfully uploaded our file!
- fmt.Fprintf(w, "Successfully Uploaded File\n")
-}
diff --git a/backend/cmd/dasboard.go b/backend/cmd/dasboard.go
deleted file mode 100644
index 670b49e..0000000
--- a/backend/cmd/dasboard.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package cmd
-
-import (
- "github.com/robrotheram/gogallery/backend/api"
- "github.com/robrotheram/gogallery/backend/config"
- "github.com/robrotheram/gogallery/backend/datastore"
- "github.com/robrotheram/gogallery/backend/embeds"
- "github.com/spf13/cobra"
- "github.com/wailsapp/wails/v2"
- "github.com/wailsapp/wails/v2/pkg/options"
-)
-
-func init() {
- rootCmd.AddCommand(dashboadCMD)
-}
-
-var dashboadCMD = &cobra.Command{
- Use: "dashboard",
- Short: "Launch UI",
- Long: "Launch UI",
- Run: func(cmd *cobra.Command, args []string) {
- LaunchDashboard()
- },
-}
-
-func LaunchDashboard() error {
- config := config.LoadConfig()
- db := datastore.Open(config.Gallery.Basepath)
- defer db.Close()
- config.Server.Port = "8800"
- go api.NewGoGalleryAPI(config, db).DashboardAPI()
-
- return wails.Run(&options.App{
- Title: "gogallery",
- Width: 1024,
- Height: 768,
- MinWidth: 800,
- Assets: &embeds.DashboardFS,
- // EnableDefaultContextMenu: true,
- BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
- })
-}
diff --git a/backend/cmd/docs.go b/backend/cmd/docs.go
deleted file mode 100644
index 0a38fe5..0000000
--- a/backend/cmd/docs.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package cmd
-
-import (
- "fmt"
- "os"
-
- "github.com/spf13/cobra"
- "github.com/spf13/cobra/doc"
-)
-
-func init() {
- rootCmd.AddCommand(docCMD)
-}
-
-var docCMD = &cobra.Command{
- Use: "docs",
- Short: "Generate CLI documentation",
- Long: "Generate CLI documentation",
- Run: func(cmd *cobra.Command, args []string) {
- os.MkdirAll("./docs/cli", os.ModePerm)
- err := doc.GenMarkdownTree(rootCmd, "./docs/cli")
- if err != nil {
- fmt.Println(err)
- }
- },
-}
diff --git a/backend/cmd/init.go b/backend/cmd/init.go
deleted file mode 100644
index 4c96c25..0000000
--- a/backend/cmd/init.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package cmd
-
-import (
- "github.com/k0kubun/pp/v3"
- "github.com/manifoldco/promptui"
- "github.com/robrotheram/gogallery/backend/config"
- "github.com/spf13/cobra"
-)
-
-func init() {
- rootCmd.AddCommand(initCMD)
-}
-
-var initCMD = &cobra.Command{
- Use: "init",
- Short: "Create site",
- Long: "Create site",
- Run: func(cmd *cobra.Command, args []string) {
- config := config.LoadConfig()
- config.PromptSiteName()
- config.PromptGalleryBasePath()
- config.PromptGalleryDest()
- config.PromptGalleryTheme()
-
- pp.Print(config)
- prompt := promptui.Prompt{
- Label: "Is info correct",
- IsConfirm: true,
- }
- prompt.Run()
- config.Save()
- },
-}
diff --git a/backend/cmd/serve.go b/backend/cmd/serve.go
deleted file mode 100644
index 9a80c62..0000000
--- a/backend/cmd/serve.go
+++ /dev/null
@@ -1,73 +0,0 @@
-package cmd
-
-import (
- "fmt"
- "log"
- "os"
- "os/exec"
- "runtime"
-
- "github.com/robrotheram/gogallery/backend/api"
- "github.com/robrotheram/gogallery/backend/config"
- "github.com/robrotheram/gogallery/backend/datastore"
- "github.com/spf13/cobra"
-)
-
-func init() {
- rootCmd.AddCommand(serveCMD)
- rootCmd.AddCommand(devCMD)
-}
-
-var serveCMD = &cobra.Command{
- Use: "serve",
- Short: "Serve static site",
- Long: "Serve static site",
- Run: func(cmd *cobra.Command, args []string) {
-
- config := config.LoadConfig()
- db := datastore.Open(config.Gallery.Basepath)
- defer db.Close()
-
- _, err := os.Stat(config.Gallery.Destpath)
- if os.IsNotExist(err) {
- log.Fatalf("Sorry it does not look like the site has been built, there is nothing at: \"%s\". Please check the config ", config.Gallery.Destpath)
- return
- }
-
- if len(args) == 1 {
- config.Server.Port = ":" + args[0]
- }
- openbrowser(fmt.Sprintf("http://%s", config.Server.GetLocalAddr()))
- api.NewGoGalleryAPI(config, db).Serve()
- },
-}
-
-var devCMD = &cobra.Command{
- Use: "dev",
- Run: func(cmd *cobra.Command, args []string) {
- config := config.LoadConfig()
- db := datastore.Open(config.Gallery.Basepath)
- defer db.Close()
- // db.ScanPath(config.Gallery.Basepath)
- config.Server.Port = "8800"
- api.NewGoGalleryAPI(config, db).DashboardAPI()
- },
-}
-
-func openbrowser(url string) {
- var err error
- switch runtime.GOOS {
- case "linux":
- err = exec.Command("xdg-open", url).Start()
- case "windows":
- err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
- case "darwin":
- err = exec.Command("open", url).Start()
- default:
- err = fmt.Errorf("unsupported platform")
- }
- if err != nil {
- log.Fatal(err)
- }
-
-}
diff --git a/backend/config/GereatePassword.go b/backend/config/GereatePassword.go
deleted file mode 100644
index d31879f..0000000
--- a/backend/config/GereatePassword.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package config
-
-import (
- "math/rand"
- "time"
-)
-
-const charset = "abcdefghijklmnopqrstuvwxyz" +
- "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
-
-var seededRand *rand.Rand = rand.New(
- rand.NewSource(time.Now().UnixNano()))
-
-func StringWithCharset(length int, charset string) string {
- b := make([]byte, length)
- for i := range b {
- b[i] = charset[seededRand.Intn(len(charset))]
- }
- return string(b)
-}
-
-func RandomPassword(length int) string {
- return StringWithCharset(length, charset)
-}
diff --git a/backend/datastore/AlbumStructure.go b/backend/datastore/AlbumStructure.go
deleted file mode 100644
index b449bc0..0000000
--- a/backend/datastore/AlbumStructure.go
+++ /dev/null
@@ -1,97 +0,0 @@
-package datastore
-
-import (
- "path"
- "sort"
- "strings"
-
- "github.com/robrotheram/gogallery/backend/config"
- "github.com/robrotheram/gogallery/backend/datastore/models"
-)
-
-type AlbumStrcure = map[string]models.Album
-
-func Sort(as AlbumStrcure) AlbumStrcure {
- keys := make([]string, 0, len(as))
- for k := range as {
- keys = append(keys, k)
- }
- data := make(map[string]models.Album)
- sort.Strings(keys)
- for _, k := range keys {
- data[k] = as[k]
- }
- return data
-}
-
-func SliceToTree(albms []models.Album, basepath string) AlbumStrcure {
- newalbms := make(map[string]models.Album)
- sort.Slice(albms, func(i, j int) bool {
- return albms[i].ParenetPath < albms[j].ParenetPath
- })
- for _, ab := range albms {
- if ab.ParenetPath == basepath {
- ab.ParenetPath = ""
- newalbms[ab.Name] = ab
- }
- }
- for _, ab := range albms {
- if (ab.ParenetPath != basepath) && (ab.Id != config.GetMD5Hash(basepath)) {
- s := strings.Split(strings.Replace(ab.ParenetPath, basepath, "", 1), "/")
- copy(s, s[1:])
- s = s[:len(s)-1]
- pth := basepath
- var alb models.Album
- for i, p := range s {
- if i == 0 {
- alb = newalbms[p]
- } else {
- alb = alb.Children[p]
- }
- pth = path.Join(pth, p)
- if i == len(s)-1 {
- if alb.Children != nil {
- ab.ParenetPath = ""
- alb.Children[ab.Name] = ab
- }
- }
- }
- }
- }
- return newalbms
-}
-
-func FindInAlbumStrcureById(ab models.Album, id string) models.Album {
- if ab.Id == id {
- return ab
- }
- for _, v := range ab.Children {
- a := FindInAlbumStrcureById(v, id)
- if a.Id == id {
- return a
- }
- }
- return models.Album{}
-}
-
-func GetAlbumFromStructure(as AlbumStrcure, id string) models.Album {
- album := models.Album{}
- for _, v := range as {
- album = FindInAlbumStrcureById(v, id)
- if album.Id != "" {
- return album
- }
- }
- return album
-}
-
-func (a *AlumnCollectioins) GetAlbumStructure(config config.GalleryConfiguration) AlbumStrcure {
- albums := []models.Album{}
- for _, alb := range a.GetAll() {
- if !IsAlbumInBlacklist(alb.Name) {
- albums = append(albums, alb)
- }
- }
- newalbms := SliceToTree(albums, config.Basepath)
- return newalbms
-}
diff --git a/backend/datastore/albumCollection.go b/backend/datastore/albumCollection.go
deleted file mode 100644
index fddc38d..0000000
--- a/backend/datastore/albumCollection.go
+++ /dev/null
@@ -1,83 +0,0 @@
-package datastore
-
-import (
- "fmt"
- "os"
- "path/filepath"
- "sync"
-
- "github.com/asdine/storm"
- "github.com/robrotheram/gogallery/backend/datastore/models"
-)
-
-type AlumnCollectioins struct {
- DB *storm.DB
- sync.Mutex
-}
-
-func (a *AlumnCollectioins) Save(pic *models.Album) {
- a.DB.Save(pic)
-}
-
-func (a *AlumnCollectioins) Update(alb *models.Album) {
- album := a.FindByID(alb.Id)
- album.Update(*alb)
- a.Save(&album)
-}
-
-func (a *AlumnCollectioins) GetAll() []models.Album {
- a.Lock()
- defer a.Unlock()
-
- var albums []models.Album
- a.DB.All(&albums)
- return albums
-}
-
-func (a *AlumnCollectioins) FindByID(id string) models.Album {
- var alb models.Album
- a.DB.One("Id", id, &alb)
- return alb
-}
-
-func (a *AlumnCollectioins) FindManyFeild(id string) []models.Album {
- var alb []models.Album
- a.FindByFeild("id", id, alb)
- return alb
-}
-
-func (a *AlumnCollectioins) FindByFeild(key string, val any, to any) {
- a.Lock()
- defer a.Unlock()
- a.DB.Find(key, val, &to)
-}
-
-func (a *AlumnCollectioins) UpdateField(id string, key string, val any) {
- a.Lock()
- defer a.Unlock()
- a.DB.UpdateField(&models.Album{Id: id}, key, val)
-}
-
-func (a *AlumnCollectioins) Delete(albums models.Album) error {
- a.Lock()
- defer a.Unlock()
-
- err := os.Remove(albums.Path)
- if err != nil {
- return err
- }
- a.DB.DeleteStruct(&albums)
- return nil
-}
-
-func (a *AlumnCollectioins) MovePictureToAlbum(picture *models.Picture, newAlbum string) error {
- album := a.FindByID(newAlbum)
- newName := fmt.Sprintf("%s/%s/%s%s", album.ParenetPath, album.Name, picture.Name, filepath.Ext(picture.Path))
- err := os.Rename(picture.Path, newName)
- if err != nil {
- return err
- }
- picture.Path = newName
- picture.Album = newAlbum
- return nil
-}
diff --git a/backend/datastore/datastore.go b/backend/datastore/datastore.go
deleted file mode 100644
index 447f34e..0000000
--- a/backend/datastore/datastore.go
+++ /dev/null
@@ -1,121 +0,0 @@
-package datastore
-
-import (
- "fmt"
- "log"
- "os"
- "path/filepath"
- "strings"
- "time"
-
- "github.com/asdine/storm"
- "github.com/robrotheram/gogallery/backend/config"
- "github.com/robrotheram/gogallery/backend/datastore/models"
-)
-
-type CRUD interface {
- Save(any)
- Delete(any)
- Update(any)
- GetById(string) []any
- GetByField(string string) []any
- GetAll() []any
-}
-
-type DataStore struct {
- db *storm.DB
- Pictures *PictureCollection
- Albums *AlumnCollectioins
- ImageCache *ImageCache
-}
-
-func Open(dbPath string) *DataStore {
- os.MkdirAll(dbPath, os.ModePerm)
- path := filepath.Join(dbPath, "gogallery.db")
- db, err := storm.Open(path)
- if err != nil {
- log.Fatalf("Unable to open db at: %s \n Error: %v", path, err)
- }
- return &DataStore{
- db: db,
- Pictures: &PictureCollection{DB: db},
- Albums: &AlumnCollectioins{DB: db},
- ImageCache: NewImageCache(),
- }
-}
-
-func (d *DataStore) Close() {
- d.db.Close()
-}
-
-func (d *DataStore) RestDB() {
- d.db.Drop(models.Picture{})
- d.db.Drop(models.Album{})
-}
-
-func (d *DataStore) ScanPath(path string) error {
- rubishPath := fmt.Sprintf("%s/%s", gConfig.Basepath, "rubish")
- if _, err := os.Stat(rubishPath); os.IsNotExist(err) {
- os.Mkdir(rubishPath, 0755)
- }
- if !contains(gConfig.AlbumBlacklist, "rubish") {
- gConfig.AlbumBlacklist = append(gConfig.AlbumBlacklist, "rubish")
- }
-
- log.Println("Scanning Folders at:" + path)
- IsScanning = true
-
- absRoot, err := filepath.Abs(path)
- if err != nil {
- return err
- }
- walkFunc := func(path string, info os.FileInfo, err error) error {
- if err != nil {
- return err
- }
- if checkEXT(path) && !info.IsDir() {
- albumName := filepath.Base(filepath.Dir(path))
- picName := strings.TrimSuffix(info.Name(), filepath.Ext(info.Name()))
- if !IsAlbumInBlacklist(albumName) && !IsPictureInBlacklist(picName) {
- p := models.Picture{
- Id: config.GetMD5Hash(path),
- Name: picName,
- Path: path,
- Ext: filepath.Ext(path),
- Album: config.GetMD5Hash(filepath.Dir(path)),
- AlbumName: albumName,
- Exif: models.Exif{},
- RootPath: gConfig.Basepath,
- Meta: models.PictureMeta{
- PostedToIG: false,
- Visibility: "PUBLIC",
- DateAdded: time.Now(),
- DateModified: time.Now()}}
- p.CreateExif()
- if !d.Pictures.Exist(p.Id) {
- d.Pictures.Save(&p)
- }
- d.Albums.UpdateField(config.GetMD5Hash(filepath.Dir(path)), "ProfileID", p.Id)
- }
- }
-
- if info.IsDir() {
- if !IsAlbumInBlacklist(info.Name()) {
- if filepath.Base(filepath.Dir(path)) != gConfig.Basepath {
- info := fileInfoFromInterface(info)
- d.Albums.Update(&models.Album{
- Id: config.GetMD5Hash(path),
- Name: info.Name,
- ModTime: info.ModTime,
- Parent: filepath.Base(filepath.Dir(path)),
- ParenetPath: (filepath.Dir(path))})
- }
- }
- }
- return nil
- }
- err = filepath.Walk(absRoot, walkFunc)
- log.Println("Scanning Complete")
- IsScanning = false
- return err
-}
diff --git a/backend/datastore/image_cache.go b/backend/datastore/image_cache.go
deleted file mode 100644
index 2d60593..0000000
--- a/backend/datastore/image_cache.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package datastore
-
-import (
- "fmt"
- "os"
- "path"
-)
-
-type ImageCache struct {
- base string
-}
-
-func NewImageCache() *ImageCache {
- path := path.Join(os.TempDir(), "gogallery")
- os.MkdirAll(path, 0755)
- return &ImageCache{
- base: path,
- }
-}
-
-func (ic *ImageCache) Get(name string, size string) (*os.File, error) {
- return os.Open(path.Join(ic.base, fmt.Sprintf("%s-%s.webp", name, size)))
-}
-func (ic *ImageCache) Writer(name string, size string) (*os.File, error) {
- return os.Create(path.Join(ic.base, fmt.Sprintf("%s-%s.webp", name, size)))
-}
diff --git a/backend/datastore/models/Album.go b/backend/datastore/models/Album.go
deleted file mode 100644
index b78ae11..0000000
--- a/backend/datastore/models/Album.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package models
-
-import "time"
-
-type Album struct {
- Id string `json:"id" storm:"id"`
- Name string `json:"name"`
- ModTime time.Time `json:"mod_time"`
- Parent string `json:"parent"`
- ParenetPath string `json:"parentPath,omitempty"`
- Path string `json:"path,omitempty"`
- ProfileID string `json:"profile_image"`
- Images []Picture `json:"images"`
- Children AlbumStrcure `json:"children"`
- GPS GPS `json:"gps"`
-}
-
-type AlbumStrcure = map[string]Album
-
-func (a *Album) Update(alb Album) {
-
- if a.Name != alb.Name && alb.Name != "" {
- a.Name = alb.Name
- }
- if a.Parent != alb.Parent && alb.Parent != "" {
- a.Parent = alb.Parent
- }
- if a.ParenetPath != alb.ParenetPath && alb.ParenetPath != "" {
- a.ParenetPath = alb.ParenetPath
- }
- if (a.ProfileID != alb.ProfileID) && (alb.ProfileID != "") {
- a.ProfileID = alb.ProfileID
- }
- if a.Children == nil {
- a.Children = make(map[string]Album)
- }
- if a.Id == "" {
- a.Id = alb.Id
- }
-}
diff --git a/backend/datastore/models/Exif.go b/backend/datastore/models/Exif.go
deleted file mode 100644
index 1bea23a..0000000
--- a/backend/datastore/models/Exif.go
+++ /dev/null
@@ -1,51 +0,0 @@
-package models
-
-import (
- "os"
- "strings"
- "time"
-
- "github.com/dsoprea/go-exif/v3"
-)
-
-type Exif struct {
- FStop string `json:"f_stop"`
- FocalLength string `json:"focal_length"`
- ShutterSpeed string `json:"shutter_speed"`
- ISO string `json:"iso"`
- Dimension string `json:"dimension"`
- Camera string `json:"camera"`
- LensModel string `json:"lens_model"`
- DateTaken time.Time `json:"date_taken"`
- GPS GPS `json:"gps"`
-}
-
-type GPS struct {
- Lat float64 `json:"latitude"`
- Lng float64 `json:"longitude"`
-}
-
-func GetRawExif(path string) ([]byte, error) {
- source, err := os.Open(path)
- if err != nil {
- return nil, err
- }
- defer func() {
- _ = source.Close()
- }()
- return exif.SearchAndExtractExifWithReader(source)
-}
-
-func GetExifTags(rawExif []byte) map[string]string {
-
- opt := exif.ScanOptions{}
- entries, _, _ := exif.GetFlatExifData(rawExif, &opt)
-
- data := make(map[string]string)
- for _, entry := range entries {
- if entry.TagName != "" && entry.Formatted != "" {
- data[entry.TagName] = strings.Split(entry.FormattedFirst, "\x00")[0]
- }
- }
- return data
-}
diff --git a/backend/datastore/models/Picture.go b/backend/datastore/models/Picture.go
deleted file mode 100644
index c3b7615..0000000
--- a/backend/datastore/models/Picture.go
+++ /dev/null
@@ -1,83 +0,0 @@
-package models
-
-import (
- "fmt"
- "image"
- _ "image/gif"
- _ "image/jpeg"
- _ "image/png"
- "os"
- "sort"
- "time"
-
- "github.com/evanoberholster/imagemeta"
- // Blind import for image.Decode
- _ "golang.org/x/image/webp"
-)
-
-type Picture struct {
- Id string `json:"id" storm:"id"`
- Name string `json:"name"`
- Caption string `json:"caption"`
- Path string `json:"path,omitempty"`
- Ext string `json:"extention,omitempty"`
- FormatTime string `json:"format_time"`
- Album string `json:"album"`
- AlbumName string `json:"album_name"`
- Exif Exif `json:"exif"`
- Meta PictureMeta `json:"meta,omitempty"`
- RootPath string `json:"root_path,omitempty"`
-}
-
-type PictureMeta struct {
- PostedToIG bool `json:"posted_to_IG,omitempty"`
- Visibility string `json:"visibility,omitempty"`
- DateAdded time.Time `json:"date_added,omitempty"`
- DateModified time.Time `json:"date_modified,omitempty"`
-}
-
-func (u *Picture) CreateExif() error {
- f, _ := os.Open(u.Path)
- defer f.Close()
- u.Exif = Exif{}
-
- meta, err := imagemeta.Decode(f)
- if err != nil {
- return err
- }
-
- u.Exif.FStop = meta.FNumber.String()
- u.Exif.FocalLength = meta.FocalLength.String()
- u.Exif.ShutterSpeed = meta.ExposureTime.String()
- u.Exif.ISO = fmt.Sprintf("%d", meta.ISOSpeed)
- u.Exif.Dimension = fmt.Sprintf("%d/%d", meta.ImageWidth, meta.ImageHeight)
- u.Exif.Camera = meta.CameraMake.String()
- u.Exif.LensModel = meta.LensModel
- u.Exif.DateTaken = meta.CreateDate()
- u.Exif.GPS = GPS{
- Lat: meta.GPS.Latitude(),
- Lng: meta.GPS.Latitude(),
- }
-
- return nil
-}
-
-func SortByTime(p []Picture) []Picture {
- sort.Slice(p, func(i, j int) bool {
- return p[i].Exif.DateTaken.Sub(p[j].Exif.DateTaken) > 0
- })
- return p
-}
-
-func (p *Picture) Load() (image.Image, error) {
- f, err := os.Open(p.Path)
- if err != nil {
- return nil, err
- }
- defer f.Close()
- img, _, err := image.Decode(f)
- if err != nil {
- return nil, fmt.Errorf("image %s, decode failed: %v", p.Path, err)
- }
- return img, nil
-}
diff --git a/backend/datastore/picturesCollection.go b/backend/datastore/picturesCollection.go
deleted file mode 100644
index 99b3fda..0000000
--- a/backend/datastore/picturesCollection.go
+++ /dev/null
@@ -1,97 +0,0 @@
-package datastore
-
-import (
- "os"
- "sync"
-
- "github.com/asdine/storm"
- "github.com/robrotheram/gogallery/backend/datastore/models"
-)
-
-type PictureCollection struct {
- DB *storm.DB
- sync.Mutex
-}
-
-func (p *PictureCollection) Save(pic *models.Picture) {
- p.DB.Save(pic)
-}
-
-func (p *PictureCollection) GetAll() []models.Picture {
- p.Lock()
- defer p.Unlock()
-
- var pics []models.Picture
- err := p.DB.All(&pics)
- if err != nil {
- return []models.Picture{}
- }
- return pics
-}
-
-func (p *PictureCollection) FindByID(id string) models.Picture {
- p.Lock()
- defer p.Unlock()
- var alb models.Picture
- p.DB.One("Id", id, &alb)
- return alb
-}
-
-func (p *PictureCollection) FindManyFeild(key string, val string) []models.Picture {
- var alb []models.Picture
- p.DB.Find(key, val, &alb)
- return alb
-}
-
-func (p *PictureCollection) Delete(picture models.Picture) error {
- p.Lock()
- defer p.Unlock()
- os.Remove(picture.Path)
- p.DB.DeleteStruct(&picture)
- return nil
-}
-
-func (p *PictureCollection) Exist(id string) bool {
- pic := p.FindByID(id)
- return pic.Id != ""
-}
-
-func (p *PictureCollection) GetByAlbumID(id string) []models.Picture {
- return models.SortByTime(p.FindManyFeild("Album", id))
-}
-
-func (p *PictureCollection) GetFilteredPictures(admin bool) []models.Picture {
- var filterPics []models.Picture
- for _, pic := range p.GetAll() {
- if admin {
- filterPics = append(filterPics, pic)
- } else if !IsAlbumInBlacklist(pic.Album) && pic.Meta.Visibility == "PUBLIC" {
- cleanpic := models.Picture{
- Id: pic.Id,
- Name: pic.Name,
- Caption: pic.Caption,
- Album: pic.Album,
- AlbumName: pic.AlbumName,
- FormatTime: pic.Exif.DateTaken.Format("01-02-2006 15:04:05"),
- Exif: pic.Exif,
- Meta: pic.Meta,
- Ext: pic.Ext,
- }
- filterPics = append(filterPics, cleanpic)
- }
- }
- return models.SortByTime(filterPics)
-}
-
-func (p *PictureCollection) GetLatestAlbum() string {
- pics := p.GetFilteredPictures(false)
- latests := pics[0].Exif.DateTaken
- album := pics[0].Album
- for _, p := range pics {
- if p.Exif.DateTaken.After(latests) {
- latests = p.Exif.DateTaken
- album = p.Album
- }
- }
- return album
-}
diff --git a/backend/embeds/embeds.go b/backend/embeds/embeds.go
deleted file mode 100644
index e9a7da5..0000000
--- a/backend/embeds/embeds.go
+++ /dev/null
@@ -1,111 +0,0 @@
-package embeds
-
-import (
- "archive/tar"
- "compress/gzip"
- "embed"
- "io"
- "io/fs"
- "os"
- "path/filepath"
- "strings"
-)
-
-var DashboardFS embed.FS
-var ThemeFS embed.FS
-
-func CopyTheme(templatePath string) {
- os.MkdirAll(templatePath, os.ModePerm)
- fs.WalkDir(ThemeFS, ".", func(path string, d fs.DirEntry, err error) error {
- newPath := filepath.Join(templatePath, path)
- if d.IsDir() {
- os.MkdirAll(newPath, os.ModePerm)
- } else {
- file, _ := ThemeFS.ReadFile(path)
- os.WriteFile(newPath, file, os.ModePerm)
- }
- return nil
- })
-
-}
-
-func CopyThemeAssets(templatePath string) {
- os.MkdirAll(templatePath, os.ModePerm)
- root := "themes/eastnor/assets"
- fs.WalkDir(ThemeFS, root, func(path string, d fs.DirEntry, err error) error {
- newPath := filepath.Join(templatePath, strings.Replace(path, root, "", -1))
- if d.IsDir() {
- os.MkdirAll(newPath, os.ModePerm)
- } else {
- file, _ := ThemeFS.ReadFile(path)
- os.WriteFile(newPath, file, os.ModePerm)
- }
- return nil
- })
-
-}
-
-func Untar(dst string, r io.Reader) error {
-
- gzr, err := gzip.NewReader(r)
- if err != nil {
- return err
- }
- defer gzr.Close()
-
- tr := tar.NewReader(gzr)
-
- for {
- header, err := tr.Next()
-
- switch {
-
- // if no more files are found return
- case err == io.EOF:
- return nil
-
- // return any other error
- case err != nil:
- return err
-
- // if the header is nil, just skip it (not sure how this happens)
- case header == nil:
- continue
- }
-
- // the target location where the dir/file should be created
- target := filepath.Join(dst, header.Name)
-
- // the following switch could also be done using fi.Mode(), not sure if there
- // a benefit of using one vs. the other.
- // fi := header.FileInfo()
-
- // check the file type
- switch header.Typeflag {
-
- // if its a dir and it doesn't exist create it
- case tar.TypeDir:
- if _, err := os.Stat(target); err != nil {
- if err := os.MkdirAll(target, 0755); err != nil {
- return err
- }
- }
-
- // if it's a file create it
- case tar.TypeReg:
- f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
- if err != nil {
- return err
- }
-
- // copy over contents
- if _, err := io.Copy(f, tr); err != nil {
- return err
- }
-
- // manually close here after each file operation; defering would cause each file close
- // to wait until all operations have completed.
- f.Close()
- }
- }
-}
diff --git a/backend/monitor/cmd_monitor .go b/backend/monitor/cmd_monitor .go
deleted file mode 100644
index f583def..0000000
--- a/backend/monitor/cmd_monitor .go
+++ /dev/null
@@ -1,74 +0,0 @@
-package monitor
-
-import (
- "fmt"
- "sort"
- "time"
-
- "github.com/gosuri/uiprogress"
- "github.com/gosuri/uiprogress/util/strutil"
-)
-
-type CmdMonitor struct {
- Tasks map[string]*ProgressStats
- Bars map[string]*uiprogress.Bar
- done chan bool
- ticker *time.Ticker
-}
-
-func NewCMDMonitor() *CmdMonitor {
- return &CmdMonitor{
- Tasks: make(map[string]*ProgressStats),
- Bars: make(map[string]*uiprogress.Bar),
- done: make(chan bool),
- }
-}
-
-func (t *CmdMonitor) NewTask(name string, total int) *ProgressStats {
- stat := NewProgressStats(name, total)
- t.Tasks[name] = stat
- t.Bars[name] = uiprogress.AddBar(total).AppendCompleted()
- t.Bars[name].PrependFunc(func(b *uiprogress.Bar) string {
- return strutil.Resize(name, 20)
- })
- t.Bars[name].AppendFunc(func(b *uiprogress.Bar) string {
- return fmt.Sprintf("%d/%d", uint(b.Current()), uint(b.Total))
- })
- return stat
-}
-
-func (t *CmdMonitor) GetTasks() []ProgressStats {
- keys := make([]string, 0, len(t.Tasks))
- values := make([]ProgressStats, 0, len(t.Tasks))
- for k := range t.Tasks {
- keys = append(keys, k)
- }
- sort.Strings(keys)
- for _, k := range keys {
- values = append(values, *t.Tasks[k])
- }
- return values
-}
-func (t *CmdMonitor) StartUpdater() {
- t.ticker = time.NewTicker(1 * time.Second)
- go func() {
- for {
- select {
- case <-t.done:
- t.UpdateProgress()
- return
- case <-t.ticker.C:
- t.UpdateProgress()
- }
- }
- }()
-}
-func (t *CmdMonitor) StopUpdater() {
- t.ticker.Stop()
- t.done <- true
-}
-func (t *CmdMonitor) UpdateProgress() {
- for name, stat := range t.Tasks {
- t.Bars[name].Set(stat.Proceesed)
- }
-}
diff --git a/backend/monitor/monitor.go b/backend/monitor/monitor.go
deleted file mode 100644
index 3a6f167..0000000
--- a/backend/monitor/monitor.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package monitor
-
-type Monitor interface {
- NewTask(name string, total int) *ProgressStats
- GetTasks() []ProgressStats
-}
diff --git a/backend/pipeline/AlbumPagePipeline.go b/backend/pipeline/AlbumPagePipeline.go
deleted file mode 100644
index 51bd496..0000000
--- a/backend/pipeline/AlbumPagePipeline.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package pipeline
-
-import (
- "bufio"
- "fmt"
- "os"
- "path/filepath"
-
- "github.com/gosimple/slug"
- "github.com/robrotheram/gogallery/backend/datastore"
- "github.com/robrotheram/gogallery/backend/datastore/models"
- templateengine "github.com/robrotheram/gogallery/backend/templateEngine"
-)
-
-func renderAlbumTemplate(db *datastore.DataStore) func(alb models.Album) error {
- return func(alb models.Album) error {
- alb_path := filepath.Join(albumDir, slug.Make(alb.Id))
- os.MkdirAll(alb_path, os.ModePerm)
-
- page := templateengine.NewPage(nil, db.Pictures.GetLatestAlbum())
- albums := db.Albums.GetAlbumStructure(page.Settings)
- album := datastore.GetAlbumFromStructure(albums, alb.Id)
-
- f, _ := os.Create(filepath.Join(alb_path, "index.html"))
- w := bufio.NewWriter(f)
- page.Album = album
- page.Images = db.Pictures.GetByAlbumID(alb.Id)
- page.Picture = templateengine.NewPagePicture(db.Pictures.FindByID(alb.ProfileID))
- page.SEO.Description = fmt.Sprintf("Album: %s", alb.Name)
- templateengine.Templates.RenderPage(w, templateengine.CollectionTemplate, page)
- f.Close()
- return nil
- }
-}
diff --git a/backend/pipeline/ImagePipeline.go b/backend/pipeline/ImagePipeline.go
deleted file mode 100644
index 7264b9a..0000000
--- a/backend/pipeline/ImagePipeline.go
+++ /dev/null
@@ -1,71 +0,0 @@
-package pipeline
-
-import (
- "image"
- "io"
- "os"
- "path/filepath"
-
- "github.com/bep/gowebp/libwebp"
- "github.com/bep/gowebp/libwebp/webpoptions"
- "github.com/disintegration/imaging"
- "github.com/robrotheram/gogallery/backend/config"
- "github.com/robrotheram/gogallery/backend/datastore/models"
- templateengine "github.com/robrotheram/gogallery/backend/templateEngine"
-)
-
-func resize(base image.Image, width int, height int) image.Image {
- if width == 0 && height == 0 {
- return imaging.Resize(base, int(float64(base.Bounds().Dx())), 0, imaging.Lanczos)
- }
- return imaging.Resize(base, width, height, imaging.Lanczos)
-}
-
-func ProcessImage(src image.Image, size int, w io.Writer) {
- resized := resize(src, size, 0)
- libwebp.Encode(w, resized, webpoptions.EncodingOptions{
- Quality: 100,
- EncodingPreset: webpoptions.EncodingPresetDefault,
- UseSharpYuv: true,
- })
-}
-
-func ImageGenV2(pic models.Picture) error {
- destPath := filepath.Join(imgDir, pic.Id)
- os.MkdirAll(destPath, os.ModePerm)
-
- toRender := map[string]int{}
- for key, size := range templateengine.ImageSizes {
- cachePath := filepath.Join(destPath, key+".webp")
- if !templateengine.FileExists(cachePath) {
- toRender[key] = size
- }
- }
-
- if config.Config.Gallery.UseOriginal {
- orginalPath := filepath.Join(destPath, "original"+pic.Ext)
- if !templateengine.FileExists(orginalPath) {
- templateengine.Copy(pic.Path, orginalPath)
- }
- }
-
- if len(toRender) == 0 {
- return nil
- }
-
- src, err := pic.Load()
- if err != nil {
- return err
- }
-
- for key, size := range toRender {
- cachePath := filepath.Join(destPath, key+".webp")
- fo, err := os.Create(cachePath)
- if err != nil {
- continue
- }
- defer fo.Close()
- ProcessImage(src, size, fo)
- }
- return nil
-}
diff --git a/backend/pipeline/IndexPagePipeluine.go b/backend/pipeline/IndexPagePipeluine.go
deleted file mode 100644
index 2e2aa3d..0000000
--- a/backend/pipeline/IndexPagePipeluine.go
+++ /dev/null
@@ -1,106 +0,0 @@
-package pipeline
-
-import (
- "bufio"
- "fmt"
- "os"
- "path/filepath"
-
- "github.com/robrotheram/gogallery/backend/config"
- "github.com/robrotheram/gogallery/backend/datastore"
- "github.com/robrotheram/gogallery/backend/datastore/models"
- "github.com/robrotheram/gogallery/backend/embeds"
- templateengine "github.com/robrotheram/gogallery/backend/templateEngine"
-)
-
-func renderIndex(db *datastore.DataStore, config *config.GalleryConfiguration) {
- imagesPerPage := 16 //defautl
- if config.ImagesPerPage > 0 {
- imagesPerPage = config.ImagesPerPage
- }
- latestAlbumID := db.Pictures.GetLatestAlbum()
- indexPage := templateengine.NewPage(nil, latestAlbumID)
- images := db.Pictures.GetFilteredPictures(false)
- pages := paginateImages(images, imagesPerPage)
- albums := datastore.Sort(db.Albums.GetAlbumStructure(indexPage.Settings))
-
- indexPage.Images = pages[0]
- indexPage.Albums = albums
- if len(images) > 0 {
- indexPage.SEO.SetImage(images[0])
- }
-
- f, _ := os.Create(filepath.Join(root, "index.html"))
- w := bufio.NewWriter(f)
- templateengine.Templates.RenderPage(w, templateengine.HomeTemplate, indexPage)
- renderPages(pages, latestAlbumID, albums)
- w.Flush()
- f.Close()
-
- f, _ = os.Create(filepath.Join(root, "manifest.json"))
- w = bufio.NewWriter(f)
- templateengine.ManifestWriter(w, config)
- w.Flush()
- f.Close()
-
- f, _ = os.Create(filepath.Join(root, "service-worker.js"))
- w = bufio.NewWriter(f)
- templateengine.ServiceWorkerWriter(w)
- w.Flush()
- f.Close()
-}
-
-func paginateImages(slice []models.Picture, chunkSize int) [][]models.Picture {
- var chunks [][]models.Picture
- for i := 0; i < len(slice); i += chunkSize {
- end := i + chunkSize
- if end > len(slice) {
- end = len(slice)
- }
- chunks = append(chunks, slice[i:end])
- }
- return chunks
-}
-
-func renderPages(pages [][]models.Picture, albumID string, albums models.AlbumStrcure) {
- pagesPath := filepath.Join(root, "page")
- os.MkdirAll(pagesPath, os.ModePerm)
- for page, pageImages := range pages {
- pagePath := filepath.Join(pagesPath, fmt.Sprint(page))
- os.MkdirAll(pagePath, os.ModePerm)
- page := templateengine.NewPage(nil, albumID)
- page.Images = pageImages
- page.Albums = albums
- if len(pageImages) > 0 {
- page.SEO.SetImage(pageImages[0])
- }
- f, _ := os.Create(filepath.Join(pagePath, "index.html"))
- w := bufio.NewWriter(f)
- templateengine.Templates.RenderPage(w, templateengine.PaginationTemplate, page)
- w.Flush()
- f.Close()
- }
-}
-
-func renderAlbums(db *datastore.DataStore) {
- os.MkdirAll(albumsDir, os.ModePerm)
- f, _ := os.Create(filepath.Join(albumsDir, "index.html"))
- w := bufio.NewWriter(f)
- page := templateengine.NewPage(nil, db.Pictures.GetLatestAlbum())
- page.Albums = datastore.Sort(db.Albums.GetAlbumStructure(page.Settings))
- templateengine.Templates.RenderPage(w, templateengine.AlbumTemplate, page)
- w.Flush()
- f.Close()
-}
-
-func build() {
- err := templateengine.Templates.Load(config.Config.Gallery.Theme)
- if err != nil {
- fmt.Println(err)
- }
- if config.Config.Gallery.Theme == "default" {
- embeds.CopyThemeAssets(filepath.Join(root, "assets"))
- } else {
- templateengine.Dir(filepath.Join(config.Config.Gallery.Theme, "assets"), filepath.Join(root, "assets"))
- }
-}
diff --git a/backend/pipeline/PhotoPagePipeline.go b/backend/pipeline/PhotoPagePipeline.go
deleted file mode 100644
index 6e9e0c9..0000000
--- a/backend/pipeline/PhotoPagePipeline.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package pipeline
-
-import (
- "os"
- "path/filepath"
-
- "github.com/gosimple/slug"
- "github.com/robrotheram/gogallery/backend/datastore"
- "github.com/robrotheram/gogallery/backend/datastore/models"
- templateengine "github.com/robrotheram/gogallery/backend/templateEngine"
-)
-
-func renderPhotoTemplate(db *datastore.DataStore) func(alb models.Picture) error {
- return func(pic models.Picture) error {
- pic_path := filepath.Join(photoDir, slug.Make(pic.Id))
- os.MkdirAll(pic_path, os.ModePerm)
- f, err := os.Create(filepath.Join(pic_path, "index.html"))
- if err != nil {
- return err
- }
- latestAlbumId := db.Pictures.GetLatestAlbum()
- templateengine.RenderPhoto(f, pic, db.Pictures.GetByAlbumID(pic.Album), templateengine.NewPage(nil, latestAlbumId))
- f.Close()
- return nil
- }
-}
diff --git a/backend/pipeline/Render.go b/backend/pipeline/Render.go
deleted file mode 100644
index 48e62bc..0000000
--- a/backend/pipeline/Render.go
+++ /dev/null
@@ -1,69 +0,0 @@
-package pipeline
-
-import (
- "os"
- "path/filepath"
-
- "github.com/robrotheram/gogallery/backend/config"
- "github.com/robrotheram/gogallery/backend/datastore"
- "github.com/robrotheram/gogallery/backend/datastore/models"
- "github.com/robrotheram/gogallery/backend/monitor"
-)
-
-var root = ""
-var imgDir string
-var photoDir string
-var albumsDir string
-var albumDir string
-
-type RenderPipeline struct {
- AlbumRender *BatchProcessing[models.Album]
- PageRender *BatchProcessing[models.Picture]
- ImageRender *BatchProcessing[models.Picture]
- monitor monitor.Monitor
- db *datastore.DataStore
- config *config.GalleryConfiguration
-}
-
-func NewRenderPipeline(config *config.GalleryConfiguration, db *datastore.DataStore, monitor monitor.Monitor) *RenderPipeline {
- root = config.Destpath
- imgDir = filepath.Join(root, "img")
- photoDir = filepath.Join(root, "photo")
- albumsDir = filepath.Join(root, "albums")
- albumDir = filepath.Join(root, "album")
-
- albums := db.Albums.GetAll()
- images := db.Pictures.GetAll()
-
- render := RenderPipeline{
- db: db,
- AlbumRender: NewBatchProcessing(renderAlbumTemplate(db), albums, monitor.NewTask("rendering albums", len(albums))),
- PageRender: NewBatchProcessing(renderPhotoTemplate(db), images, monitor.NewTask("rendering pages", len(images))),
- ImageRender: NewBatchProcessing(ImageGenV2, images, monitor.NewTask("optomizing images", len(images))),
- monitor: monitor,
- config: config,
- }
- return &render
-}
-
-func (r *RenderPipeline) CreateDir() {
- os.MkdirAll(root, os.ModePerm)
- os.MkdirAll(imgDir, os.ModePerm)
- os.MkdirAll(photoDir, os.ModePerm)
- os.MkdirAll(albumDir, os.ModePerm)
-}
-
-func (r *RenderPipeline) DeleteSite() {
- os.RemoveAll(root)
-}
-
-func (r *RenderPipeline) BuildSite() {
- db := r.db
- r.CreateDir()
- build()
- renderIndex(db, r.config)
- renderAlbums(db)
- r.AlbumRender.Run()
- r.PageRender.Run()
- r.ImageRender.Run()
-}
diff --git a/backend/pipeline/batch.go b/backend/pipeline/batch.go
deleted file mode 100644
index d4fd246..0000000
--- a/backend/pipeline/batch.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package pipeline
-
-import (
- "runtime"
- "sync"
-
- "github.com/robrotheram/gogallery/backend/monitor"
-)
-
-type BatchProcessing[T any] struct {
- items []T
- stat *monitor.ProgressStats
- work func(T) error
- chunkSize int
-}
-
-func chunkSlice[T any](slice []T, nchunks int) [][]T {
- var chunks [][]T
- chunkSize := (len(slice) / nchunks)
- for i := 0; i < len(slice); i += chunkSize {
- end := i + chunkSize
- if end > len(slice) {
- end = len(slice)
- }
- chunks = append(chunks, slice[i:end])
- }
- return chunks
-}
-
-func (batch *BatchProcessing[T]) Run() {
- batch.stat.Start()
- var wg sync.WaitGroup
- chunks := chunkSlice(batch.items, batch.chunkSize)
- for _, chunk := range chunks {
- wg.Add(1)
- go batch.processing(chunk, &wg)
- }
- wg.Wait()
- batch.stat.End()
-}
-
-func (poc *BatchProcessing[T]) processing(batch []T, wg *sync.WaitGroup) {
- for _, pic := range batch {
- poc.work(pic)
- poc.stat.Update()
- }
- wg.Done()
-}
-
-func NewBatchProcessing[T any](processing func(T) error, items []T, stat *monitor.ProgressStats) *BatchProcessing[T] {
- stat.Total = len(items)
- //save 1 core for the system
- chunsize := runtime.NumCPU() - 1
- if chunsize < 1 {
- chunsize = 1
- }
- return &BatchProcessing[T]{
- work: processing,
- chunkSize: chunsize,
- items: items,
- stat: stat,
- }
-}
diff --git a/cmd/benchmark.go b/cmd/benchmark.go
new file mode 100644
index 0000000..bdef853
--- /dev/null
+++ b/cmd/benchmark.go
@@ -0,0 +1,93 @@
+package cmd
+
+import (
+ "gogallery/pkg/config"
+ "gogallery/pkg/datastore"
+ "gogallery/pkg/monitor"
+ "log"
+ "os"
+ "runtime/pprof"
+ "time"
+
+ "github.com/spf13/cobra"
+)
+
+func init() {
+ rootCmd.AddCommand(benchmark)
+}
+
+var benchmark = &cobra.Command{
+ Use: "benchmark",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ cpuFile, _ := os.Create("cpu.prof")
+ pprof.StartCPUProfile(cpuFile)
+ defer pprof.StopCPUProfile()
+
+ memFile, _ := os.Create("mem.prof")
+ pprof.WriteHeapProfile(memFile)
+ defer memFile.Close()
+
+ benchmarkScanPath()
+ return nil
+ },
+}
+
+func benchmarkScanPath() {
+
+ start := time.Now()
+ config := config.LoadConfig()
+ config.Validate()
+ db, err := datastore.Open(config.Gallery.Basepath, monitor.NewCMDMonitor())
+ if err != nil {
+ log.Fatalf("Failed to open database: %v", err)
+ }
+
+ if err := db.ScanPath(config.Gallery.Basepath); err != nil {
+ log.Fatalf("Error scanning path: %v", err)
+ }
+
+ elapsed := time.Since(start)
+ log.Printf("Scan completed in %s", elapsed)
+}
+
+func benchmarkImage() {
+ var totalTime time.Duration
+ p := datastore.Picture{
+ Id: "benchmark",
+ Path: "/home/robert/Pictures/gallery/pictures/bergen/20250511_0010.jpg",
+ }
+ start := time.Now()
+ p.CreateExif()
+
+ elapsed := time.Since(start)
+ totalTime += elapsed
+ log.Printf("Benchmark completed in %s", elapsed)
+
+ // src, err := p.Load()
+ // if err != nil {
+ // log.Fatalf("Error loading benchmark image: %v", err)
+ // }
+ // destPath := "benchmark.webp"
+ // sizes := templateengine.ImageSizes
+ // for _, size := range sizes {
+ // if _, err := os.Stat(destPath); err == nil {
+ // if err := os.Remove(destPath); err != nil {
+ // log.Fatalf("Error deleting existing file: %v", err)
+ // }
+ // }
+ // fo, err := os.Create(destPath)
+ // if err != nil {
+ // log.Fatalf("Error creating file: %v", err)
+ // }
+ // defer fo.Close()
+
+ // start := time.Now()
+
+ // pipeline.ProcessImage(src, size.ImgWidth, fo)
+
+ // elapsed := time.Since(start)
+ // totalTime += elapsed
+ // log.Printf("Benchmark completed in %s", elapsed)
+ // }
+ log.Printf("Total benchmark time for all sizes: %s", totalTime)
+}
diff --git a/backend/cmd/build.go b/cmd/build.go
similarity index 62%
rename from backend/cmd/build.go
rename to cmd/build.go
index 263cd3d..4064124 100644
--- a/backend/cmd/build.go
+++ b/cmd/build.go
@@ -1,13 +1,13 @@
package cmd
import (
+ "gogallery/pkg/config"
+ "gogallery/pkg/datastore"
+ "gogallery/pkg/monitor"
+ "gogallery/pkg/pipeline"
"log"
"github.com/gosuri/uiprogress"
- "github.com/robrotheram/gogallery/backend/config"
- "github.com/robrotheram/gogallery/backend/datastore"
- "github.com/robrotheram/gogallery/backend/monitor"
- "github.com/robrotheram/gogallery/backend/pipeline"
"github.com/spf13/cobra"
)
@@ -23,15 +23,17 @@ var buildCMD = &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) error {
config := config.LoadConfig()
config.Validate()
- db := datastore.Open(config.Gallery.Basepath)
- defer db.Close()
+ db, err := datastore.Open(config.Gallery.Basepath, cmdMonitor)
+ if err != nil {
+ log.Fatalf("Failed to open database: %v", err)
+ }
+ cmdMonitor.StartUpdater()
db.ScanPath(config.Gallery.Basepath)
log.Println("Building Site at: " + config.Gallery.Destpath)
uiprogress.Start()
- render := pipeline.NewRenderPipeline(&config.Gallery, db, cmdMonitor)
- cmdMonitor.StartUpdater()
+ render := pipeline.NewRenderPipeline(&config.Gallery, db)
+
render.BuildSite()
- cmdMonitor.StopUpdater()
log.Println("Building Complete")
return nil
},
diff --git a/backend/cmd/deploy.go b/cmd/deploy.go
similarity index 74%
rename from backend/cmd/deploy.go
rename to cmd/deploy.go
index 8eba990..a724f8c 100644
--- a/backend/cmd/deploy.go
+++ b/cmd/deploy.go
@@ -2,10 +2,10 @@ package cmd
import (
"fmt"
+ "gogallery/pkg/config"
+ "gogallery/pkg/deploy"
+ "gogallery/pkg/monitor"
- "github.com/robrotheram/gogallery/backend/config"
- "github.com/robrotheram/gogallery/backend/deploy"
- "github.com/robrotheram/gogallery/backend/monitor"
"github.com/spf13/cobra"
)
diff --git a/backend/cmd/extract.go b/cmd/extract.go
similarity index 90%
rename from backend/cmd/extract.go
rename to cmd/extract.go
index 6323d74..b5ca133 100644
--- a/backend/cmd/extract.go
+++ b/cmd/extract.go
@@ -2,8 +2,8 @@ package cmd
import (
"fmt"
+ "gogallery/pkg/embeds"
- "github.com/robrotheram/gogallery/backend/embeds"
"github.com/spf13/cobra"
)
diff --git a/backend/cmd/root.go b/cmd/root.go
similarity index 97%
rename from backend/cmd/root.go
rename to cmd/root.go
index a88ed3e..d387de9 100644
--- a/backend/cmd/root.go
+++ b/cmd/root.go
@@ -7,6 +7,7 @@ package cmd
import (
"fmt"
+ "gogallery/pkg/ui"
"os"
homedir "github.com/mitchellh/go-homedir"
@@ -24,7 +25,7 @@ var rootCmd = &cobra.Command{
Long: `Generates a full static site that you can host all use the local provided server`,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
- return LaunchDashboard()
+ return ui.App()
}
return cmd.Help()
},
diff --git a/cmd/serve.go b/cmd/serve.go
new file mode 100644
index 0000000..21b0ce4
--- /dev/null
+++ b/cmd/serve.go
@@ -0,0 +1,37 @@
+package cmd
+
+import (
+ "gogallery/pkg/config"
+ "gogallery/pkg/datastore"
+ "gogallery/pkg/monitor"
+ "gogallery/pkg/preview"
+ "log"
+
+ "github.com/spf13/cobra"
+)
+
+func init() {
+ rootCmd.AddCommand(serveCMD)
+}
+
+var serveCMD = &cobra.Command{
+ Use: "serve",
+ Short: "Serve static site",
+ Long: "Serve static site",
+ Run: func(cmd *cobra.Command, args []string) {
+ config := config.LoadConfig()
+ db, err := datastore.Open(config.Gallery.Basepath, monitor.NewCMDMonitor())
+ if err != nil {
+ log.Fatalf("Failed to open database: %v", err)
+ }
+
+ server := preview.NewServer(db)
+ if err := server.Start(); err != nil {
+ log.Fatalf("Server failed to start: %v", err)
+ }
+ // Print the actual address after the server has started and acquired a port
+ log.Printf("Starting Preview Server http://%s", server.Addr())
+ // Wait for the server goroutine to exit (block until server stops)
+ select {}
+ },
+}
diff --git a/docker-compose.yml b/docker-compose.yml
deleted file mode 100644
index ca5a88a..0000000
--- a/docker-compose.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-version: "2"
-
-networks:
- gogallery:
- external: false
-
-services:
- server:
- image: ghcr.io/robrotheram/gogallery:master
- restart: always
- networks:
- - gogallery
- volumes:
- - "./config.yml:/app/config.yml"
- - "/path/to/gallery:/app/pictures"
- ports:
- - 8085:80
- environment:
- GLLRY_GALLERY_BASEPATH: "/app/pictures"
-
-
diff --git a/docs/album.png b/docs/album.png
new file mode 100644
index 0000000..8c70081
Binary files /dev/null and b/docs/album.png differ
diff --git a/docs/cli/gogallery.md b/docs/cli/gogallery.md
deleted file mode 100644
index df300ad..0000000
--- a/docs/cli/gogallery.md
+++ /dev/null
@@ -1,31 +0,0 @@
-## gogallery
-
-Photo Gallery Static Site generator
-
-### Synopsis
-
-Generates a full static site that you can host all use the local provided server
-
-```
-gogallery [flags]
-```
-
-### Options
-
-```
- --config string config file (default is $HOME/.gogallery.yaml)
- -h, --help help for gogallery
-```
-
-### SEE ALSO
-
-* [gogallery build](gogallery_build.md) - Build static site
-* [gogallery completion](gogallery_completion.md) - Generate the autocompletion script for the specified shell
-* [gogallery dashboard](gogallery_dashboard.md) - Launch UI
-* [gogallery deploy](gogallery_deploy.md) - Deploy static site
-* [gogallery docs](gogallery_docs.md) - Generate CLI documentation
-* [gogallery init](gogallery_init.md) - Create site
-* [gogallery serve](gogallery_serve.md) - Serve static site
-* [gogallery template](gogallery_template.md) - Extract template to directory
-
-###### Auto generated by spf13/cobra on 11-Sep-2022
diff --git a/docs/cli/gogallery_build.md b/docs/cli/gogallery_build.md
deleted file mode 100644
index d0bf4d2..0000000
--- a/docs/cli/gogallery_build.md
+++ /dev/null
@@ -1,29 +0,0 @@
-## gogallery build
-
-Build static site
-
-### Synopsis
-
-Build static site
-
-```
-gogallery build [flags]
-```
-
-### Options
-
-```
- -h, --help help for build
-```
-
-### Options inherited from parent commands
-
-```
- --config string config file (default is $HOME/.gogallery.yaml)
-```
-
-### SEE ALSO
-
-* [gogallery](gogallery.md) - Photo Gallery Static Site generator
-
-###### Auto generated by spf13/cobra on 11-Sep-2022
diff --git a/docs/cli/gogallery_completion.md b/docs/cli/gogallery_completion.md
deleted file mode 100644
index 51fdafc..0000000
--- a/docs/cli/gogallery_completion.md
+++ /dev/null
@@ -1,31 +0,0 @@
-## gogallery completion
-
-Generate the autocompletion script for the specified shell
-
-### Synopsis
-
-Generate the autocompletion script for gogallery for the specified shell.
-See each sub-command's help for details on how to use the generated script.
-
-
-### Options
-
-```
- -h, --help help for completion
-```
-
-### Options inherited from parent commands
-
-```
- --config string config file (default is $HOME/.gogallery.yaml)
-```
-
-### SEE ALSO
-
-* [gogallery](gogallery.md) - Photo Gallery Static Site generator
-* [gogallery completion bash](gogallery_completion_bash.md) - Generate the autocompletion script for bash
-* [gogallery completion fish](gogallery_completion_fish.md) - Generate the autocompletion script for fish
-* [gogallery completion powershell](gogallery_completion_powershell.md) - Generate the autocompletion script for powershell
-* [gogallery completion zsh](gogallery_completion_zsh.md) - Generate the autocompletion script for zsh
-
-###### Auto generated by spf13/cobra on 11-Sep-2022
diff --git a/docs/cli/gogallery_completion_bash.md b/docs/cli/gogallery_completion_bash.md
deleted file mode 100644
index ebb0297..0000000
--- a/docs/cli/gogallery_completion_bash.md
+++ /dev/null
@@ -1,50 +0,0 @@
-## gogallery completion bash
-
-Generate the autocompletion script for bash
-
-### Synopsis
-
-Generate the autocompletion script for the bash shell.
-
-This script depends on the 'bash-completion' package.
-If it is not installed already, you can install it via your OS's package manager.
-
-To load completions in your current shell session:
-
- source <(gogallery completion bash)
-
-To load completions for every new session, execute once:
-
-#### Linux:
-
- gogallery completion bash > /etc/bash_completion.d/gogallery
-
-#### macOS:
-
- gogallery completion bash > $(brew --prefix)/etc/bash_completion.d/gogallery
-
-You will need to start a new shell for this setup to take effect.
-
-
-```
-gogallery completion bash
-```
-
-### Options
-
-```
- -h, --help help for bash
- --no-descriptions disable completion descriptions
-```
-
-### Options inherited from parent commands
-
-```
- --config string config file (default is $HOME/.gogallery.yaml)
-```
-
-### SEE ALSO
-
-* [gogallery completion](gogallery_completion.md) - Generate the autocompletion script for the specified shell
-
-###### Auto generated by spf13/cobra on 11-Sep-2022
diff --git a/docs/cli/gogallery_completion_fish.md b/docs/cli/gogallery_completion_fish.md
deleted file mode 100644
index 3adcd70..0000000
--- a/docs/cli/gogallery_completion_fish.md
+++ /dev/null
@@ -1,41 +0,0 @@
-## gogallery completion fish
-
-Generate the autocompletion script for fish
-
-### Synopsis
-
-Generate the autocompletion script for the fish shell.
-
-To load completions in your current shell session:
-
- gogallery completion fish | source
-
-To load completions for every new session, execute once:
-
- gogallery completion fish > ~/.config/fish/completions/gogallery.fish
-
-You will need to start a new shell for this setup to take effect.
-
-
-```
-gogallery completion fish [flags]
-```
-
-### Options
-
-```
- -h, --help help for fish
- --no-descriptions disable completion descriptions
-```
-
-### Options inherited from parent commands
-
-```
- --config string config file (default is $HOME/.gogallery.yaml)
-```
-
-### SEE ALSO
-
-* [gogallery completion](gogallery_completion.md) - Generate the autocompletion script for the specified shell
-
-###### Auto generated by spf13/cobra on 11-Sep-2022
diff --git a/docs/cli/gogallery_completion_powershell.md b/docs/cli/gogallery_completion_powershell.md
deleted file mode 100644
index c57de5b..0000000
--- a/docs/cli/gogallery_completion_powershell.md
+++ /dev/null
@@ -1,38 +0,0 @@
-## gogallery completion powershell
-
-Generate the autocompletion script for powershell
-
-### Synopsis
-
-Generate the autocompletion script for powershell.
-
-To load completions in your current shell session:
-
- gogallery completion powershell | Out-String | Invoke-Expression
-
-To load completions for every new session, add the output of the above command
-to your powershell profile.
-
-
-```
-gogallery completion powershell [flags]
-```
-
-### Options
-
-```
- -h, --help help for powershell
- --no-descriptions disable completion descriptions
-```
-
-### Options inherited from parent commands
-
-```
- --config string config file (default is $HOME/.gogallery.yaml)
-```
-
-### SEE ALSO
-
-* [gogallery completion](gogallery_completion.md) - Generate the autocompletion script for the specified shell
-
-###### Auto generated by spf13/cobra on 11-Sep-2022
diff --git a/docs/cli/gogallery_completion_zsh.md b/docs/cli/gogallery_completion_zsh.md
deleted file mode 100644
index 53dfde1..0000000
--- a/docs/cli/gogallery_completion_zsh.md
+++ /dev/null
@@ -1,52 +0,0 @@
-## gogallery completion zsh
-
-Generate the autocompletion script for zsh
-
-### Synopsis
-
-Generate the autocompletion script for the zsh shell.
-
-If shell completion is not already enabled in your environment you will need
-to enable it. You can execute the following once:
-
- echo "autoload -U compinit; compinit" >> ~/.zshrc
-
-To load completions in your current shell session:
-
- source <(gogallery completion zsh); compdef _gogallery gogallery
-
-To load completions for every new session, execute once:
-
-#### Linux:
-
- gogallery completion zsh > "${fpath[1]}/_gogallery"
-
-#### macOS:
-
- gogallery completion zsh > $(brew --prefix)/share/zsh/site-functions/_gogallery
-
-You will need to start a new shell for this setup to take effect.
-
-
-```
-gogallery completion zsh [flags]
-```
-
-### Options
-
-```
- -h, --help help for zsh
- --no-descriptions disable completion descriptions
-```
-
-### Options inherited from parent commands
-
-```
- --config string config file (default is $HOME/.gogallery.yaml)
-```
-
-### SEE ALSO
-
-* [gogallery completion](gogallery_completion.md) - Generate the autocompletion script for the specified shell
-
-###### Auto generated by spf13/cobra on 11-Sep-2022
diff --git a/docs/cli/gogallery_dashboard.md b/docs/cli/gogallery_dashboard.md
deleted file mode 100644
index de43ea4..0000000
--- a/docs/cli/gogallery_dashboard.md
+++ /dev/null
@@ -1,29 +0,0 @@
-## gogallery dashboard
-
-Launch UI
-
-### Synopsis
-
-Launch UI
-
-```
-gogallery dashboard [flags]
-```
-
-### Options
-
-```
- -h, --help help for dashboard
-```
-
-### Options inherited from parent commands
-
-```
- --config string config file (default is $HOME/.gogallery.yaml)
-```
-
-### SEE ALSO
-
-* [gogallery](gogallery.md) - Photo Gallery Static Site generator
-
-###### Auto generated by spf13/cobra on 11-Sep-2022
diff --git a/docs/cli/gogallery_deploy.md b/docs/cli/gogallery_deploy.md
deleted file mode 100644
index f8dc47e..0000000
--- a/docs/cli/gogallery_deploy.md
+++ /dev/null
@@ -1,29 +0,0 @@
-## gogallery deploy
-
-Deploy static site
-
-### Synopsis
-
-Deploy static site
-
-```
-gogallery deploy [flags]
-```
-
-### Options
-
-```
- -h, --help help for deploy
-```
-
-### Options inherited from parent commands
-
-```
- --config string config file (default is $HOME/.gogallery.yaml)
-```
-
-### SEE ALSO
-
-* [gogallery](gogallery.md) - Photo Gallery Static Site generator
-
-###### Auto generated by spf13/cobra on 11-Sep-2022
diff --git a/docs/cli/gogallery_docs.md b/docs/cli/gogallery_docs.md
deleted file mode 100644
index 149f702..0000000
--- a/docs/cli/gogallery_docs.md
+++ /dev/null
@@ -1,29 +0,0 @@
-## gogallery docs
-
-Generate CLI documentation
-
-### Synopsis
-
-Generate CLI documentation
-
-```
-gogallery docs [flags]
-```
-
-### Options
-
-```
- -h, --help help for docs
-```
-
-### Options inherited from parent commands
-
-```
- --config string config file (default is $HOME/.gogallery.yaml)
-```
-
-### SEE ALSO
-
-* [gogallery](gogallery.md) - Photo Gallery Static Site generator
-
-###### Auto generated by spf13/cobra on 11-Sep-2022
diff --git a/docs/cli/gogallery_init.md b/docs/cli/gogallery_init.md
deleted file mode 100644
index 9ba4772..0000000
--- a/docs/cli/gogallery_init.md
+++ /dev/null
@@ -1,29 +0,0 @@
-## gogallery init
-
-Create site
-
-### Synopsis
-
-Create site
-
-```
-gogallery init [flags]
-```
-
-### Options
-
-```
- -h, --help help for init
-```
-
-### Options inherited from parent commands
-
-```
- --config string config file (default is $HOME/.gogallery.yaml)
-```
-
-### SEE ALSO
-
-* [gogallery](gogallery.md) - Photo Gallery Static Site generator
-
-###### Auto generated by spf13/cobra on 11-Sep-2022
diff --git a/docs/cli/gogallery_serve.md b/docs/cli/gogallery_serve.md
deleted file mode 100644
index e2dd0a8..0000000
--- a/docs/cli/gogallery_serve.md
+++ /dev/null
@@ -1,29 +0,0 @@
-## gogallery serve
-
-Serve static site
-
-### Synopsis
-
-Serve static site
-
-```
-gogallery serve [flags]
-```
-
-### Options
-
-```
- -h, --help help for serve
-```
-
-### Options inherited from parent commands
-
-```
- --config string config file (default is $HOME/.gogallery.yaml)
-```
-
-### SEE ALSO
-
-* [gogallery](gogallery.md) - Photo Gallery Static Site generator
-
-###### Auto generated by spf13/cobra on 11-Sep-2022
diff --git a/docs/cli/gogallery_template.md b/docs/cli/gogallery_template.md
deleted file mode 100644
index 3c152a0..0000000
--- a/docs/cli/gogallery_template.md
+++ /dev/null
@@ -1,29 +0,0 @@
-## gogallery template
-
-Extract template to directory
-
-### Synopsis
-
-Extract the internal template to any directory
-
-```
-gogallery template [flags]
-```
-
-### Options
-
-```
- -h, --help help for template
-```
-
-### Options inherited from parent commands
-
-```
- --config string config file (default is $HOME/.gogallery.yaml)
-```
-
-### SEE ALSO
-
-* [gogallery](gogallery.md) - Photo Gallery Static Site generator
-
-###### Auto generated by spf13/cobra on 11-Sep-2022
diff --git a/docs/dashboard1.jpg b/docs/dashboard1.jpg
deleted file mode 100644
index 3ae7b4f..0000000
Binary files a/docs/dashboard1.jpg and /dev/null differ
diff --git a/docs/dashboard2.png b/docs/dashboard2.png
deleted file mode 100644
index 5d38a62..0000000
Binary files a/docs/dashboard2.png and /dev/null differ
diff --git a/docs/dashboard3.png b/docs/dashboard3.png
deleted file mode 100644
index 6031cdd..0000000
Binary files a/docs/dashboard3.png and /dev/null differ
diff --git a/docs/homepage.png b/docs/homepage.png
new file mode 100644
index 0000000..baca826
Binary files /dev/null and b/docs/homepage.png differ
diff --git a/docs/img1.jpg b/docs/img1.jpg
deleted file mode 100644
index 6897c3f..0000000
Binary files a/docs/img1.jpg and /dev/null differ
diff --git a/docs/img2.jpg b/docs/img2.jpg
deleted file mode 100644
index e30c51d..0000000
Binary files a/docs/img2.jpg and /dev/null differ
diff --git a/docs/settings.png b/docs/settings.png
new file mode 100644
index 0000000..dad15cc
Binary files /dev/null and b/docs/settings.png differ
diff --git a/docs/sidebar.png b/docs/sidebar.png
new file mode 100644
index 0000000..e9192ae
Binary files /dev/null and b/docs/sidebar.png differ
diff --git a/docs/tasks.png b/docs/tasks.png
new file mode 100644
index 0000000..22ef7ec
Binary files /dev/null and b/docs/tasks.png differ
diff --git a/docs/website-collection.png b/docs/website-collection.png
new file mode 100644
index 0000000..52eb78a
Binary files /dev/null and b/docs/website-collection.png differ
diff --git a/docs/website-home.png b/docs/website-home.png
new file mode 100644
index 0000000..8171bf7
Binary files /dev/null and b/docs/website-home.png differ
diff --git a/docs/website-photo.png b/docs/website-photo.png
new file mode 100644
index 0000000..b309907
Binary files /dev/null and b/docs/website-photo.png differ
diff --git a/frontend/favicon.ico b/frontend/favicon.ico
deleted file mode 100644
index 0c67ce2..0000000
Binary files a/frontend/favicon.ico and /dev/null differ
diff --git a/frontend/index.html b/frontend/index.html
deleted file mode 100644
index 232e787..0000000
--- a/frontend/index.html
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
- gogallery
-
-
-
-
-
-
-
-
-
-
-
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
deleted file mode 100644
index cf67551..0000000
--- a/frontend/package-lock.json
+++ /dev/null
@@ -1,3612 +0,0 @@
-{
- "name": "frontend",
- "version": "0.0.0",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "frontend",
- "version": "0.0.0",
- "dependencies": {
- "antd": "^5.22.1",
- "axios": "^1.6.3",
- "leaflet": "^1.9.1",
- "mapbox-gl": "^2.10.0",
- "maplibre-gl": "^2.4.0",
- "moment": "^2.29.4",
- "react": "^18.0.0",
- "react-dom": "^18.0.0",
- "react-lazy-load-image-component": "^1.5.5",
- "react-leaflet": "^4.0.2",
- "react-map-gl": "^7.0.19",
- "react-redux": "^8.0.2",
- "react-router-dom": "^6.3.0",
- "redux": "^4.2.0",
- "redux-logger": "^3.0.6",
- "redux-thunk": "^2.4.1"
- },
- "devDependencies": {
- "@types/react": "^18.0.0",
- "@types/react-dom": "^18.0.0",
- "@vitejs/plugin-react": "^4.2.1",
- "vite": "^5.1.5"
- }
- },
- "node_modules/@ampproject/remapping": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
- "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.24"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@ant-design/colors": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.1.0.tgz",
- "integrity": "sha512-MMoDGWn1y9LdQJQSHiCC20x3uZ3CwQnv9QMz6pCmJOrqdgM9YxsoVVY0wtrdXbmfSgnV0KNk6zi09NAhMR2jvg==",
- "license": "MIT",
- "dependencies": {
- "@ctrl/tinycolor": "^3.6.1"
- }
- },
- "node_modules/@ant-design/cssinjs": {
- "version": "1.22.0",
- "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.22.0.tgz",
- "integrity": "sha512-W9XSFeRPR0mAN3OuxfuS/xhENCYKf+8s+QyNNER0FSWoK9OpISTag6CCweg6lq0hASQ/2Vcza0Z8/kGivCP0Ng==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.11.1",
- "@emotion/hash": "^0.8.0",
- "@emotion/unitless": "^0.7.5",
- "classnames": "^2.3.1",
- "csstype": "^3.1.3",
- "rc-util": "^5.35.0",
- "stylis": "^4.3.4"
- },
- "peerDependencies": {
- "react": ">=16.0.0",
- "react-dom": ">=16.0.0"
- }
- },
- "node_modules/@ant-design/cssinjs-utils": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.1.tgz",
- "integrity": "sha512-2HAiyGGGnM0es40SxdszeQAU5iWp41wBIInq+ONTCKjlSKOrzQfnw4JDtB8IBmqE6tQaEKwmzTP2LGdt5DSwYQ==",
- "license": "MIT",
- "dependencies": {
- "@ant-design/cssinjs": "^1.21.0",
- "@babel/runtime": "^7.23.2",
- "rc-util": "^5.38.0"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/@ant-design/fast-color": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz",
- "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.24.7"
- },
- "engines": {
- "node": ">=8.x"
- }
- },
- "node_modules/@ant-design/icons": {
- "version": "5.5.1",
- "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.5.1.tgz",
- "integrity": "sha512-0UrM02MA2iDIgvLatWrj6YTCYe0F/cwXvVE0E2SqGrL7PZireQwgEKTKBisWpZyal5eXZLvuM98kju6YtYne8w==",
- "license": "MIT",
- "dependencies": {
- "@ant-design/colors": "^7.0.0",
- "@ant-design/icons-svg": "^4.4.0",
- "@babel/runtime": "^7.24.8",
- "classnames": "^2.2.6",
- "rc-util": "^5.31.1"
- },
- "engines": {
- "node": ">=8"
- },
- "peerDependencies": {
- "react": ">=16.0.0",
- "react-dom": ">=16.0.0"
- }
- },
- "node_modules/@ant-design/icons-svg": {
- "version": "4.4.2",
- "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz",
- "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==",
- "license": "MIT"
- },
- "node_modules/@ant-design/react-slick": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.1.2.tgz",
- "integrity": "sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.10.4",
- "classnames": "^2.2.5",
- "json2mq": "^0.2.0",
- "resize-observer-polyfill": "^1.5.1",
- "throttle-debounce": "^5.0.0"
- },
- "peerDependencies": {
- "react": ">=16.9.0"
- }
- },
- "node_modules/@babel/code-frame": {
- "version": "7.26.2",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
- "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-validator-identifier": "^7.25.9",
- "js-tokens": "^4.0.0",
- "picocolors": "^1.0.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/compat-data": {
- "version": "7.26.2",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.2.tgz",
- "integrity": "sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/core": {
- "version": "7.26.0",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz",
- "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@ampproject/remapping": "^2.2.0",
- "@babel/code-frame": "^7.26.0",
- "@babel/generator": "^7.26.0",
- "@babel/helper-compilation-targets": "^7.25.9",
- "@babel/helper-module-transforms": "^7.26.0",
- "@babel/helpers": "^7.26.0",
- "@babel/parser": "^7.26.0",
- "@babel/template": "^7.25.9",
- "@babel/traverse": "^7.25.9",
- "@babel/types": "^7.26.0",
- "convert-source-map": "^2.0.0",
- "debug": "^4.1.0",
- "gensync": "^1.0.0-beta.2",
- "json5": "^2.2.3",
- "semver": "^6.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/babel"
- }
- },
- "node_modules/@babel/generator": {
- "version": "7.26.2",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz",
- "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.26.2",
- "@babel/types": "^7.26.0",
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.25",
- "jsesc": "^3.0.2"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-compilation-targets": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz",
- "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/compat-data": "^7.25.9",
- "@babel/helper-validator-option": "^7.25.9",
- "browserslist": "^4.24.0",
- "lru-cache": "^5.1.1",
- "semver": "^6.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-module-imports": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz",
- "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/traverse": "^7.25.9",
- "@babel/types": "^7.25.9"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-module-transforms": {
- "version": "7.26.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz",
- "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-module-imports": "^7.25.9",
- "@babel/helper-validator-identifier": "^7.25.9",
- "@babel/traverse": "^7.25.9"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
- }
- },
- "node_modules/@babel/helper-plugin-utils": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz",
- "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-string-parser": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
- "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-validator-identifier": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
- "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-validator-option": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz",
- "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helpers": {
- "version": "7.26.0",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz",
- "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/template": "^7.25.9",
- "@babel/types": "^7.26.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/parser": {
- "version": "7.26.2",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz",
- "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^7.26.0"
- },
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@babel/plugin-transform-react-jsx-self": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz",
- "integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-react-jsx-source": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz",
- "integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/runtime": {
- "version": "7.26.0",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz",
- "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
- "license": "MIT",
- "dependencies": {
- "regenerator-runtime": "^0.14.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/template": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
- "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.25.9",
- "@babel/parser": "^7.25.9",
- "@babel/types": "^7.25.9"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/traverse": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz",
- "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.25.9",
- "@babel/generator": "^7.25.9",
- "@babel/parser": "^7.25.9",
- "@babel/template": "^7.25.9",
- "@babel/types": "^7.25.9",
- "debug": "^4.3.1",
- "globals": "^11.1.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/types": {
- "version": "7.26.0",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz",
- "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-string-parser": "^7.25.9",
- "@babel/helper-validator-identifier": "^7.25.9"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@ctrl/tinycolor": {
- "version": "3.6.1",
- "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
- "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
- "license": "MIT",
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@emotion/hash": {
- "version": "0.8.0",
- "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
- "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==",
- "license": "MIT"
- },
- "node_modules/@emotion/unitless": {
- "version": "0.7.5",
- "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
- "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==",
- "license": "MIT"
- },
- "node_modules/@esbuild/aix-ppc64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
- "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/android-arm": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
- "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/android-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
- "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/android-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
- "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/darwin-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
- "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/darwin-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
- "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/freebsd-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
- "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/freebsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
- "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-arm": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
- "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
- "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-ia32": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
- "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-loong64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
- "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-mips64el": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
- "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-ppc64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
- "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-riscv64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
- "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-s390x": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
- "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
- "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
- "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
- "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/sunos-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
- "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/win32-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
- "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/win32-ia32": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
- "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/win32-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
- "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
- "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/set-array": "^1.2.1",
- "@jridgewell/sourcemap-codec": "^1.4.10",
- "@jridgewell/trace-mapping": "^0.3.24"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/resolve-uri": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
- "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/set-array": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
- "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
- "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.25",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
- "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/resolve-uri": "^3.1.0",
- "@jridgewell/sourcemap-codec": "^1.4.14"
- }
- },
- "node_modules/@mapbox/geojson-rewind": {
- "version": "0.5.2",
- "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz",
- "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==",
- "license": "ISC",
- "dependencies": {
- "get-stream": "^6.0.1",
- "minimist": "^1.2.6"
- },
- "bin": {
- "geojson-rewind": "geojson-rewind"
- }
- },
- "node_modules/@mapbox/jsonlint-lines-primitives": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz",
- "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/@mapbox/mapbox-gl-supported": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-2.0.1.tgz",
- "integrity": "sha512-HP6XvfNIzfoMVfyGjBckjiAOQK9WfX0ywdLubuPMPv+Vqf5fj0uCbgBQYpiqcWZT6cbyyRnTSXDheT1ugvF6UQ==",
- "license": "BSD-3-Clause"
- },
- "node_modules/@mapbox/point-geometry": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz",
- "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==",
- "license": "ISC"
- },
- "node_modules/@mapbox/tiny-sdf": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.6.tgz",
- "integrity": "sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA==",
- "license": "BSD-2-Clause"
- },
- "node_modules/@mapbox/unitbezier": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz",
- "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==",
- "license": "BSD-2-Clause"
- },
- "node_modules/@mapbox/vector-tile": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz",
- "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@mapbox/point-geometry": "~0.1.0"
- }
- },
- "node_modules/@mapbox/whoots-js": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz",
- "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==",
- "license": "ISC",
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@maplibre/maplibre-gl-style-spec": {
- "version": "19.3.3",
- "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-19.3.3.tgz",
- "integrity": "sha512-cOZZOVhDSulgK0meTsTkmNXb1ahVvmTmWmfx9gRBwc6hq98wS9JP35ESIoNq3xqEan+UN+gn8187Z6E4NKhLsw==",
- "license": "ISC",
- "dependencies": {
- "@mapbox/jsonlint-lines-primitives": "~2.0.2",
- "@mapbox/unitbezier": "^0.0.1",
- "json-stringify-pretty-compact": "^3.0.0",
- "minimist": "^1.2.8",
- "rw": "^1.3.3",
- "sort-object": "^3.0.3"
- },
- "bin": {
- "gl-style-format": "dist/gl-style-format.mjs",
- "gl-style-migrate": "dist/gl-style-migrate.mjs",
- "gl-style-validate": "dist/gl-style-validate.mjs"
- }
- },
- "node_modules/@rc-component/async-validator": {
- "version": "5.0.4",
- "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.0.4.tgz",
- "integrity": "sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.24.4"
- },
- "engines": {
- "node": ">=14.x"
- }
- },
- "node_modules/@rc-component/color-picker": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-2.0.1.tgz",
- "integrity": "sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q==",
- "license": "MIT",
- "dependencies": {
- "@ant-design/fast-color": "^2.0.6",
- "@babel/runtime": "^7.23.6",
- "classnames": "^2.2.6",
- "rc-util": "^5.38.1"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/@rc-component/context": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-1.4.0.tgz",
- "integrity": "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.10.1",
- "rc-util": "^5.27.0"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/@rc-component/mini-decimal": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.0.tgz",
- "integrity": "sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.18.0"
- },
- "engines": {
- "node": ">=8.x"
- }
- },
- "node_modules/@rc-component/mutate-observer": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz",
- "integrity": "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.18.0",
- "classnames": "^2.3.2",
- "rc-util": "^5.24.4"
- },
- "engines": {
- "node": ">=8.x"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/@rc-component/portal": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz",
- "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.18.0",
- "classnames": "^2.3.2",
- "rc-util": "^5.24.4"
- },
- "engines": {
- "node": ">=8.x"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/@rc-component/qrcode": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.0.0.tgz",
- "integrity": "sha512-L+rZ4HXP2sJ1gHMGHjsg9jlYBX/SLN2D6OxP9Zn3qgtpMWtO2vUfxVFwiogHpAIqs54FnALxraUy/BCO1yRIgg==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.24.7",
- "classnames": "^2.3.2",
- "rc-util": "^5.38.0"
- },
- "engines": {
- "node": ">=8.x"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/@rc-component/tour": {
- "version": "1.15.1",
- "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.15.1.tgz",
- "integrity": "sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.18.0",
- "@rc-component/portal": "^1.0.0-9",
- "@rc-component/trigger": "^2.0.0",
- "classnames": "^2.3.2",
- "rc-util": "^5.24.4"
- },
- "engines": {
- "node": ">=8.x"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/@rc-component/trigger": {
- "version": "2.2.5",
- "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.2.5.tgz",
- "integrity": "sha512-F1EJ4KjFpGAHAjuKvOyZB/6IZDkVx0bHl0M4fQM5wXcmm7lgTgVSSnR3bXwdmS6jOJGHOqfDxIJW3WUvwMIXhQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.23.2",
- "@rc-component/portal": "^1.1.0",
- "classnames": "^2.3.2",
- "rc-motion": "^2.0.0",
- "rc-resize-observer": "^1.3.1",
- "rc-util": "^5.38.0"
- },
- "engines": {
- "node": ">=8.x"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/@react-leaflet/core": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz",
- "integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==",
- "license": "Hippocratic-2.1",
- "peerDependencies": {
- "leaflet": "^1.9.0",
- "react": "^18.0.0",
- "react-dom": "^18.0.0"
- }
- },
- "node_modules/@remix-run/router": {
- "version": "1.21.0",
- "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.21.0.tgz",
- "integrity": "sha512-xfSkCAchbdG5PnbrKqFWwia4Bi61nH+wm8wLEqfHDyp7Y3dZzgqS2itV8i4gAq9pC2HsTpwyBC6Ds8VHZ96JlA==",
- "license": "MIT",
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.27.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.27.4.tgz",
- "integrity": "sha512-2Y3JT6f5MrQkICUyRVCw4oa0sutfAsgaSsb0Lmmy1Wi2y7X5vT9Euqw4gOsCyy0YfKURBg35nhUKZS4mDcfULw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@rollup/rollup-android-arm64": {
- "version": "4.27.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.27.4.tgz",
- "integrity": "sha512-wzKRQXISyi9UdCVRqEd0H4cMpzvHYt1f/C3CoIjES6cG++RHKhrBj2+29nPF0IB5kpy9MS71vs07fvrNGAl/iA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.27.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.27.4.tgz",
- "integrity": "sha512-PlNiRQapift4LNS8DPUHuDX/IdXiLjf8mc5vdEmUR0fF/pyy2qWwzdLjB+iZquGr8LuN4LnUoSEvKRwjSVYz3Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.27.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.27.4.tgz",
- "integrity": "sha512-o9bH2dbdgBDJaXWJCDTNDYa171ACUdzpxSZt+u/AAeQ20Nk5x+IhA+zsGmrQtpkLiumRJEYef68gcpn2ooXhSQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.27.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.27.4.tgz",
- "integrity": "sha512-NBI2/i2hT9Q+HySSHTBh52da7isru4aAAo6qC3I7QFVsuhxi2gM8t/EI9EVcILiHLj1vfi+VGGPaLOUENn7pmw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ]
- },
- "node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.27.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.27.4.tgz",
- "integrity": "sha512-wYcC5ycW2zvqtDYrE7deary2P2UFmSh85PUpAx+dwTCO9uw3sgzD6Gv9n5X4vLaQKsrfTSZZ7Z7uynQozPVvWA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.27.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.27.4.tgz",
- "integrity": "sha512-9OwUnK/xKw6DyRlgx8UizeqRFOfi9mf5TYCw1uolDaJSbUmBxP85DE6T4ouCMoN6pXw8ZoTeZCSEfSaYo+/s1w==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.27.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.27.4.tgz",
- "integrity": "sha512-Vgdo4fpuphS9V24WOV+KwkCVJ72u7idTgQaBoLRD0UxBAWTF9GWurJO9YD9yh00BzbkhpeXtm6na+MvJU7Z73A==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.27.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.27.4.tgz",
- "integrity": "sha512-pleyNgyd1kkBkw2kOqlBx+0atfIIkkExOTiifoODo6qKDSpnc6WzUY5RhHdmTdIJXBdSnh6JknnYTtmQyobrVg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.27.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.27.4.tgz",
- "integrity": "sha512-caluiUXvUuVyCHr5DxL8ohaaFFzPGmgmMvwmqAITMpV/Q+tPoaHZ/PWa3t8B2WyoRcIIuu1hkaW5KkeTDNSnMA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
- "version": "4.27.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.27.4.tgz",
- "integrity": "sha512-FScrpHrO60hARyHh7s1zHE97u0KlT/RECzCKAdmI+LEoC1eDh/RDji9JgFqyO+wPDb86Oa/sXkily1+oi4FzJQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.27.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.27.4.tgz",
- "integrity": "sha512-qyyprhyGb7+RBfMPeww9FlHwKkCXdKHeGgSqmIXw9VSUtvyFZ6WZRtnxgbuz76FK7LyoN8t/eINRbPUcvXB5fw==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.27.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.27.4.tgz",
- "integrity": "sha512-PFz+y2kb6tbh7m3A7nA9++eInGcDVZUACulf/KzDtovvdTizHpZaJty7Gp0lFwSQcrnebHOqxF1MaKZd7psVRg==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.27.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.27.4.tgz",
- "integrity": "sha512-Ni8mMtfo+o/G7DVtweXXV/Ol2TFf63KYjTtoZ5f078AUgJTmaIJnj4JFU7TK/9SVWTaSJGxPi5zMDgK4w+Ez7Q==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.27.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.27.4.tgz",
- "integrity": "sha512-5AeeAF1PB9TUzD+3cROzFTnAJAcVUGLuR8ng0E0WXGkYhp6RD6L+6szYVX+64Rs0r72019KHZS1ka1q+zU/wUw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.27.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.27.4.tgz",
- "integrity": "sha512-yOpVsA4K5qVwu2CaS3hHxluWIK5HQTjNV4tWjQXluMiiiu4pJj4BN98CvxohNCpcjMeTXk/ZMJBRbgRg8HBB6A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.27.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.27.4.tgz",
- "integrity": "sha512-KtwEJOaHAVJlxV92rNYiG9JQwQAdhBlrjNRp7P9L8Cb4Rer3in+0A+IPhJC9y68WAi9H0sX4AiG2NTsVlmqJeQ==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.27.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.27.4.tgz",
- "integrity": "sha512-3j4jx1TppORdTAoBJRd+/wJRGCPC0ETWkXOecJ6PPZLj6SptXkrXcNqdj0oclbKML6FkQltdz7bBA3rUSirZug==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@types/babel__core": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
- "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.20.7",
- "@babel/types": "^7.20.7",
- "@types/babel__generator": "*",
- "@types/babel__template": "*",
- "@types/babel__traverse": "*"
- }
- },
- "node_modules/@types/babel__generator": {
- "version": "7.6.8",
- "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz",
- "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^7.0.0"
- }
- },
- "node_modules/@types/babel__template": {
- "version": "7.4.4",
- "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
- "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.1.0",
- "@babel/types": "^7.0.0"
- }
- },
- "node_modules/@types/babel__traverse": {
- "version": "7.20.6",
- "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz",
- "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^7.20.7"
- }
- },
- "node_modules/@types/estree": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
- "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/geojson": {
- "version": "7946.0.14",
- "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz",
- "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==",
- "license": "MIT"
- },
- "node_modules/@types/hoist-non-react-statics": {
- "version": "3.3.5",
- "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz",
- "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==",
- "license": "MIT",
- "dependencies": {
- "@types/react": "*",
- "hoist-non-react-statics": "^3.3.0"
- }
- },
- "node_modules/@types/mapbox__point-geometry": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz",
- "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==",
- "license": "MIT"
- },
- "node_modules/@types/mapbox__vector-tile": {
- "version": "1.3.4",
- "resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz",
- "integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==",
- "license": "MIT",
- "dependencies": {
- "@types/geojson": "*",
- "@types/mapbox__point-geometry": "*",
- "@types/pbf": "*"
- }
- },
- "node_modules/@types/mapbox-gl": {
- "version": "3.4.1",
- "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-3.4.1.tgz",
- "integrity": "sha512-NsGKKtgW93B+UaLPti6B7NwlxYlES5DpV5Gzj9F75rK5ALKsqSk15CiEHbOnTr09RGbr6ZYiCdI+59NNNcAImg==",
- "license": "MIT",
- "dependencies": {
- "@types/geojson": "*"
- }
- },
- "node_modules/@types/pbf": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz",
- "integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==",
- "license": "MIT"
- },
- "node_modules/@types/prop-types": {
- "version": "15.7.13",
- "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
- "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==",
- "license": "MIT"
- },
- "node_modules/@types/react": {
- "version": "18.3.12",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz",
- "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==",
- "license": "MIT",
- "dependencies": {
- "@types/prop-types": "*",
- "csstype": "^3.0.2"
- }
- },
- "node_modules/@types/react-dom": {
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz",
- "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@types/react": "*"
- }
- },
- "node_modules/@types/use-sync-external-store": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
- "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==",
- "license": "MIT"
- },
- "node_modules/@vitejs/plugin-react": {
- "version": "4.3.4",
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz",
- "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/core": "^7.26.0",
- "@babel/plugin-transform-react-jsx-self": "^7.25.9",
- "@babel/plugin-transform-react-jsx-source": "^7.25.9",
- "@types/babel__core": "^7.20.5",
- "react-refresh": "^0.14.2"
- },
- "engines": {
- "node": "^14.18.0 || >=16.0.0"
- },
- "peerDependencies": {
- "vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
- }
- },
- "node_modules/antd": {
- "version": "5.22.2",
- "resolved": "https://registry.npmjs.org/antd/-/antd-5.22.2.tgz",
- "integrity": "sha512-vihhiJbm9VG3d6boUeD1q2MXMax+qBrXhgqCEC+45v8iGUF6m4Ct+lFiCW4oWaN3EABOsbVA6Svy3Rj/QkQFKw==",
- "license": "MIT",
- "dependencies": {
- "@ant-design/colors": "^7.1.0",
- "@ant-design/cssinjs": "^1.21.1",
- "@ant-design/cssinjs-utils": "^1.1.1",
- "@ant-design/icons": "^5.5.1",
- "@ant-design/react-slick": "~1.1.2",
- "@babel/runtime": "^7.25.7",
- "@ctrl/tinycolor": "^3.6.1",
- "@rc-component/color-picker": "~2.0.1",
- "@rc-component/mutate-observer": "^1.1.0",
- "@rc-component/qrcode": "~1.0.0",
- "@rc-component/tour": "~1.15.1",
- "@rc-component/trigger": "^2.2.5",
- "classnames": "^2.5.1",
- "copy-to-clipboard": "^3.3.3",
- "dayjs": "^1.11.11",
- "rc-cascader": "~3.30.0",
- "rc-checkbox": "~3.3.0",
- "rc-collapse": "~3.9.0",
- "rc-dialog": "~9.6.0",
- "rc-drawer": "~7.2.0",
- "rc-dropdown": "~4.2.0",
- "rc-field-form": "~2.5.1",
- "rc-image": "~7.11.0",
- "rc-input": "~1.6.3",
- "rc-input-number": "~9.3.0",
- "rc-mentions": "~2.17.0",
- "rc-menu": "~9.16.0",
- "rc-motion": "^2.9.3",
- "rc-notification": "~5.6.2",
- "rc-pagination": "~4.3.0",
- "rc-picker": "~4.8.1",
- "rc-progress": "~4.0.0",
- "rc-rate": "~2.13.0",
- "rc-resize-observer": "^1.4.0",
- "rc-segmented": "~2.5.0",
- "rc-select": "~14.16.3",
- "rc-slider": "~11.1.7",
- "rc-steps": "~6.0.1",
- "rc-switch": "~4.1.0",
- "rc-table": "~7.48.1",
- "rc-tabs": "~15.4.0",
- "rc-textarea": "~1.8.2",
- "rc-tooltip": "~6.2.1",
- "rc-tree": "~5.10.1",
- "rc-tree-select": "~5.24.4",
- "rc-upload": "~4.8.1",
- "rc-util": "^5.43.0",
- "scroll-into-view-if-needed": "^3.1.0",
- "throttle-debounce": "^5.0.2"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/ant-design"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/arr-union": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz",
- "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/assign-symbols": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
- "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/asynckit": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
- "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
- "license": "MIT"
- },
- "node_modules/axios": {
- "version": "1.7.8",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.8.tgz",
- "integrity": "sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw==",
- "license": "MIT",
- "dependencies": {
- "follow-redirects": "^1.15.6",
- "form-data": "^4.0.0",
- "proxy-from-env": "^1.1.0"
- }
- },
- "node_modules/browserslist": {
- "version": "4.24.2",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz",
- "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/browserslist"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "caniuse-lite": "^1.0.30001669",
- "electron-to-chromium": "^1.5.41",
- "node-releases": "^2.0.18",
- "update-browserslist-db": "^1.1.1"
- },
- "bin": {
- "browserslist": "cli.js"
- },
- "engines": {
- "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
- }
- },
- "node_modules/bytewise": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/bytewise/-/bytewise-1.1.0.tgz",
- "integrity": "sha512-rHuuseJ9iQ0na6UDhnrRVDh8YnWVlU6xM3VH6q/+yHDeUH2zIhUzP+2/h3LIrhLDBtTqzWpE3p3tP/boefskKQ==",
- "license": "MIT",
- "dependencies": {
- "bytewise-core": "^1.2.2",
- "typewise": "^1.0.3"
- }
- },
- "node_modules/bytewise-core": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/bytewise-core/-/bytewise-core-1.2.3.tgz",
- "integrity": "sha512-nZD//kc78OOxeYtRlVk8/zXqTB4gf/nlguL1ggWA8FuchMyOxcyHR4QPQZMUmA7czC+YnaBrPUCubqAWe50DaA==",
- "license": "MIT",
- "dependencies": {
- "typewise-core": "^1.2"
- }
- },
- "node_modules/caniuse-lite": {
- "version": "1.0.30001684",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001684.tgz",
- "integrity": "sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "CC-BY-4.0"
- },
- "node_modules/classnames": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
- "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
- "license": "MIT"
- },
- "node_modules/combined-stream": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
- "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
- "license": "MIT",
- "dependencies": {
- "delayed-stream": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/compute-scroll-into-view": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz",
- "integrity": "sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==",
- "license": "MIT"
- },
- "node_modules/convert-source-map": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
- "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/copy-to-clipboard": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
- "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==",
- "license": "MIT",
- "dependencies": {
- "toggle-selection": "^1.0.6"
- }
- },
- "node_modules/csscolorparser": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz",
- "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==",
- "license": "MIT"
- },
- "node_modules/csstype": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
- "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "license": "MIT"
- },
- "node_modules/dayjs": {
- "version": "1.11.13",
- "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
- "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
- "license": "MIT"
- },
- "node_modules/debug": {
- "version": "4.3.7",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
- "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/deep-diff": {
- "version": "0.3.8",
- "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-0.3.8.tgz",
- "integrity": "sha512-yVn6RZmHiGnxRKR9sJb3iVV2XTF1Ghh2DiWRZ3dMnGc43yUdWWF/kX6lQyk3+P84iprfWKU/8zFTrlkvtFm1ug==",
- "license": "MIT"
- },
- "node_modules/delayed-stream": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
- "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.4.0"
- }
- },
- "node_modules/earcut": {
- "version": "2.2.4",
- "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz",
- "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==",
- "license": "ISC"
- },
- "node_modules/electron-to-chromium": {
- "version": "1.5.66",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.66.tgz",
- "integrity": "sha512-pI2QF6+i+zjPbqRzJwkMvtvkdI7MjVbSh2g8dlMguDJIXEPw+kwasS1Jl+YGPEBfGVxsVgGUratAKymPdPo2vQ==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/esbuild": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
- "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=12"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.21.5",
- "@esbuild/android-arm": "0.21.5",
- "@esbuild/android-arm64": "0.21.5",
- "@esbuild/android-x64": "0.21.5",
- "@esbuild/darwin-arm64": "0.21.5",
- "@esbuild/darwin-x64": "0.21.5",
- "@esbuild/freebsd-arm64": "0.21.5",
- "@esbuild/freebsd-x64": "0.21.5",
- "@esbuild/linux-arm": "0.21.5",
- "@esbuild/linux-arm64": "0.21.5",
- "@esbuild/linux-ia32": "0.21.5",
- "@esbuild/linux-loong64": "0.21.5",
- "@esbuild/linux-mips64el": "0.21.5",
- "@esbuild/linux-ppc64": "0.21.5",
- "@esbuild/linux-riscv64": "0.21.5",
- "@esbuild/linux-s390x": "0.21.5",
- "@esbuild/linux-x64": "0.21.5",
- "@esbuild/netbsd-x64": "0.21.5",
- "@esbuild/openbsd-x64": "0.21.5",
- "@esbuild/sunos-x64": "0.21.5",
- "@esbuild/win32-arm64": "0.21.5",
- "@esbuild/win32-ia32": "0.21.5",
- "@esbuild/win32-x64": "0.21.5"
- }
- },
- "node_modules/escalade": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
- "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
- "license": "MIT",
- "dependencies": {
- "is-extendable": "^0.1.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/follow-redirects": {
- "version": "1.15.9",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
- "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
- "funding": [
- {
- "type": "individual",
- "url": "https://github.com/sponsors/RubenVerborgh"
- }
- ],
- "license": "MIT",
- "engines": {
- "node": ">=4.0"
- },
- "peerDependenciesMeta": {
- "debug": {
- "optional": true
- }
- }
- },
- "node_modules/form-data": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
- "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
- "license": "MIT",
- "dependencies": {
- "asynckit": "^0.4.0",
- "combined-stream": "^1.0.8",
- "mime-types": "^2.1.12"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
- "node_modules/gensync": {
- "version": "1.0.0-beta.2",
- "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
- "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/geojson-vt": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz",
- "integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==",
- "license": "ISC"
- },
- "node_modules/get-stream": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
- "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/get-value": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
- "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/gl-matrix": {
- "version": "3.4.3",
- "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz",
- "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==",
- "license": "MIT"
- },
- "node_modules/global-prefix": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz",
- "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==",
- "license": "MIT",
- "dependencies": {
- "ini": "^1.3.5",
- "kind-of": "^6.0.2",
- "which": "^1.3.1"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/globals": {
- "version": "11.12.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
- "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/grid-index": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz",
- "integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==",
- "license": "ISC"
- },
- "node_modules/hoist-non-react-statics": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
- "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "react-is": "^16.7.0"
- }
- },
- "node_modules/hoist-non-react-statics/node_modules/react-is": {
- "version": "16.13.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "license": "MIT"
- },
- "node_modules/ieee754": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
- "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "BSD-3-Clause"
- },
- "node_modules/ini": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
- "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
- "license": "ISC"
- },
- "node_modules/is-extendable": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
- "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-plain-object": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
- "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
- "license": "MIT",
- "dependencies": {
- "isobject": "^3.0.1"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/isexe": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "license": "ISC"
- },
- "node_modules/isobject": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
- "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "license": "MIT"
- },
- "node_modules/jsesc": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
- "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "jsesc": "bin/jsesc"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/json-stringify-pretty-compact": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-3.0.0.tgz",
- "integrity": "sha512-Rc2suX5meI0S3bfdZuA7JMFBGkJ875ApfVyq2WHELjBiiG22My/l7/8zPpH/CfFVQHuVLd8NLR0nv6vi0BYYKA==",
- "license": "MIT"
- },
- "node_modules/json2mq": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz",
- "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==",
- "license": "MIT",
- "dependencies": {
- "string-convert": "^0.2.0"
- }
- },
- "node_modules/json5": {
- "version": "2.2.3",
- "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
- "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "json5": "lib/cli.js"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/kdbush": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
- "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==",
- "license": "ISC"
- },
- "node_modules/kind-of": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
- "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/leaflet": {
- "version": "1.9.4",
- "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
- "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
- "license": "BSD-2-Clause"
- },
- "node_modules/lodash.debounce": {
- "version": "4.0.8",
- "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
- "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
- "license": "MIT"
- },
- "node_modules/lodash.throttle": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
- "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==",
- "license": "MIT"
- },
- "node_modules/loose-envify": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
- "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
- "license": "MIT",
- "dependencies": {
- "js-tokens": "^3.0.0 || ^4.0.0"
- },
- "bin": {
- "loose-envify": "cli.js"
- }
- },
- "node_modules/lru-cache": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
- "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "yallist": "^3.0.2"
- }
- },
- "node_modules/mapbox-gl": {
- "version": "2.15.0",
- "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-2.15.0.tgz",
- "integrity": "sha512-fjv+aYrd5TIHiL7wRa+W7KjtUqKWziJMZUkK5hm8TvJ3OLeNPx4NmW/DgfYhd/jHej8wWL+QJBDbdMMAKvNC0A==",
- "license": "SEE LICENSE IN LICENSE.txt",
- "dependencies": {
- "@mapbox/geojson-rewind": "^0.5.2",
- "@mapbox/jsonlint-lines-primitives": "^2.0.2",
- "@mapbox/mapbox-gl-supported": "^2.0.1",
- "@mapbox/point-geometry": "^0.1.0",
- "@mapbox/tiny-sdf": "^2.0.6",
- "@mapbox/unitbezier": "^0.0.1",
- "@mapbox/vector-tile": "^1.3.1",
- "@mapbox/whoots-js": "^3.1.0",
- "csscolorparser": "~1.0.3",
- "earcut": "^2.2.4",
- "geojson-vt": "^3.2.1",
- "gl-matrix": "^3.4.3",
- "grid-index": "^1.1.0",
- "kdbush": "^4.0.1",
- "murmurhash-js": "^1.0.0",
- "pbf": "^3.2.1",
- "potpack": "^2.0.0",
- "quickselect": "^2.0.0",
- "rw": "^1.3.3",
- "supercluster": "^8.0.0",
- "tinyqueue": "^2.0.3",
- "vt-pbf": "^3.1.3"
- }
- },
- "node_modules/maplibre-gl": {
- "version": "2.4.0",
- "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-2.4.0.tgz",
- "integrity": "sha512-csNFylzntPmHWidczfgCZpvbTSmhaWvLRj9e1ezUDBEPizGgshgm3ea1T5TCNEEBq0roauu7BPuRZjA3wO4KqA==",
- "hasInstallScript": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "@mapbox/geojson-rewind": "^0.5.2",
- "@mapbox/jsonlint-lines-primitives": "^2.0.2",
- "@mapbox/mapbox-gl-supported": "^2.0.1",
- "@mapbox/point-geometry": "^0.1.0",
- "@mapbox/tiny-sdf": "^2.0.5",
- "@mapbox/unitbezier": "^0.0.1",
- "@mapbox/vector-tile": "^1.3.1",
- "@mapbox/whoots-js": "^3.1.0",
- "@types/geojson": "^7946.0.10",
- "@types/mapbox__point-geometry": "^0.1.2",
- "@types/mapbox__vector-tile": "^1.3.0",
- "@types/pbf": "^3.0.2",
- "csscolorparser": "~1.0.3",
- "earcut": "^2.2.4",
- "geojson-vt": "^3.2.1",
- "gl-matrix": "^3.4.3",
- "global-prefix": "^3.0.0",
- "murmurhash-js": "^1.0.0",
- "pbf": "^3.2.1",
- "potpack": "^1.0.2",
- "quickselect": "^2.0.0",
- "supercluster": "^7.1.5",
- "tinyqueue": "^2.0.3",
- "vt-pbf": "^3.1.3"
- }
- },
- "node_modules/maplibre-gl/node_modules/kdbush": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz",
- "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==",
- "license": "ISC"
- },
- "node_modules/maplibre-gl/node_modules/potpack": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz",
- "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==",
- "license": "ISC"
- },
- "node_modules/maplibre-gl/node_modules/supercluster": {
- "version": "7.1.5",
- "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.5.tgz",
- "integrity": "sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==",
- "license": "ISC",
- "dependencies": {
- "kdbush": "^3.0.0"
- }
- },
- "node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mime-types": {
- "version": "2.1.35",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "license": "MIT",
- "dependencies": {
- "mime-db": "1.52.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/minimist": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
- "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/moment": {
- "version": "2.30.1",
- "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
- "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
- "license": "MIT",
- "engines": {
- "node": "*"
- }
- },
- "node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/murmurhash-js": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
- "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==",
- "license": "MIT"
- },
- "node_modules/nanoid": {
- "version": "3.3.8",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
- "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.cjs"
- },
- "engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
- }
- },
- "node_modules/node-releases": {
- "version": "2.0.18",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
- "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/pbf": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz",
- "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "ieee754": "^1.1.12",
- "resolve-protobuf-schema": "^2.1.0"
- },
- "bin": {
- "pbf": "bin/pbf"
- }
- },
- "node_modules/picocolors": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
- "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/postcss": {
- "version": "8.4.49",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
- "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "nanoid": "^3.3.7",
- "picocolors": "^1.1.1",
- "source-map-js": "^1.2.1"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
- "node_modules/potpack": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz",
- "integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==",
- "license": "ISC"
- },
- "node_modules/protocol-buffers-schema": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
- "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==",
- "license": "MIT"
- },
- "node_modules/proxy-from-env": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
- "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
- "license": "MIT"
- },
- "node_modules/quickselect": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz",
- "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==",
- "license": "ISC"
- },
- "node_modules/rc-cascader": {
- "version": "3.30.0",
- "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.30.0.tgz",
- "integrity": "sha512-rrzSbk1Bdqbu+pDwiLCLHu72+lwX9BZ28+JKzoi0DWZ4N29QYFeip8Gctl33QVd2Xg3Rf14D3yAOG76ElJw16w==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.25.7",
- "classnames": "^2.3.1",
- "rc-select": "~14.16.2",
- "rc-tree": "~5.10.1",
- "rc-util": "^5.43.0"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/rc-checkbox": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.3.0.tgz",
- "integrity": "sha512-Ih3ZaAcoAiFKJjifzwsGiT/f/quIkxJoklW4yKGho14Olulwn8gN7hOBve0/WGDg5o/l/5mL0w7ff7/YGvefVw==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.10.1",
- "classnames": "^2.3.2",
- "rc-util": "^5.25.2"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/rc-collapse": {
- "version": "3.9.0",
- "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.9.0.tgz",
- "integrity": "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.10.1",
- "classnames": "2.x",
- "rc-motion": "^2.3.4",
- "rc-util": "^5.27.0"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/rc-dialog": {
- "version": "9.6.0",
- "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.6.0.tgz",
- "integrity": "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.10.1",
- "@rc-component/portal": "^1.0.0-8",
- "classnames": "^2.2.6",
- "rc-motion": "^2.3.0",
- "rc-util": "^5.21.0"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/rc-drawer": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.2.0.tgz",
- "integrity": "sha512-9lOQ7kBekEJRdEpScHvtmEtXnAsy+NGDXiRWc2ZVC7QXAazNVbeT4EraQKYwCME8BJLa8Bxqxvs5swwyOepRwg==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.23.9",
- "@rc-component/portal": "^1.1.1",
- "classnames": "^2.2.6",
- "rc-motion": "^2.6.1",
- "rc-util": "^5.38.1"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/rc-dropdown": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-4.2.0.tgz",
- "integrity": "sha512-odM8Ove+gSh0zU27DUj5cG1gNKg7mLWBYzB5E4nNLrLwBmYEgYP43vHKDGOVZcJSVElQBI0+jTQgjnq0NfLjng==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.18.3",
- "@rc-component/trigger": "^2.0.0",
- "classnames": "^2.2.6",
- "rc-util": "^5.17.0"
- },
- "peerDependencies": {
- "react": ">=16.11.0",
- "react-dom": ">=16.11.0"
- }
- },
- "node_modules/rc-field-form": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-2.5.1.tgz",
- "integrity": "sha512-33hunXwynQJyeae7LS3hMGTXNeRBjiPyPYgB0824EbmLHiXC1EBGyUwRh6xjLRy9c+en5WARYN0gJz5+JAqwig==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.18.0",
- "@rc-component/async-validator": "^5.0.3",
- "rc-util": "^5.32.2"
- },
- "engines": {
- "node": ">=8.x"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/rc-image": {
- "version": "7.11.0",
- "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-7.11.0.tgz",
- "integrity": "sha512-aZkTEZXqeqfPZtnSdNUnKQA0N/3MbgR7nUnZ+/4MfSFWPFHZau4p5r5ShaI0KPEMnNjv4kijSCFq/9wtJpwykw==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.11.2",
- "@rc-component/portal": "^1.0.2",
- "classnames": "^2.2.6",
- "rc-dialog": "~9.6.0",
- "rc-motion": "^2.6.2",
- "rc-util": "^5.34.1"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/rc-input": {
- "version": "1.6.3",
- "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.6.3.tgz",
- "integrity": "sha512-wI4NzuqBS8vvKr8cljsvnTUqItMfG1QbJoxovCgL+DX4eVUcHIjVwharwevIxyy7H/jbLryh+K7ysnJr23aWIA==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.11.1",
- "classnames": "^2.2.1",
- "rc-util": "^5.18.1"
- },
- "peerDependencies": {
- "react": ">=16.0.0",
- "react-dom": ">=16.0.0"
- }
- },
- "node_modules/rc-input-number": {
- "version": "9.3.0",
- "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.3.0.tgz",
- "integrity": "sha512-JQ363ywqRyxwgVxpg2z2kja3CehTpYdqR7emJ/6yJjRdbvo+RvfE83fcpBCIJRq3zLp8SakmEXq60qzWyZ7Usw==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.10.1",
- "@rc-component/mini-decimal": "^1.0.1",
- "classnames": "^2.2.5",
- "rc-input": "~1.6.0",
- "rc-util": "^5.40.1"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/rc-mentions": {
- "version": "2.17.0",
- "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.17.0.tgz",
- "integrity": "sha512-sfHy+qLvc+p8jx8GUsujZWXDOIlIimp6YQz7N5ONQ6bHsa2kyG+BLa5k2wuxgebBbH97is33wxiyq5UkiXRpHA==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.22.5",
- "@rc-component/trigger": "^2.0.0",
- "classnames": "^2.2.6",
- "rc-input": "~1.6.0",
- "rc-menu": "~9.16.0",
- "rc-textarea": "~1.8.0",
- "rc-util": "^5.34.1"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/rc-menu": {
- "version": "9.16.0",
- "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.16.0.tgz",
- "integrity": "sha512-vAL0yqPkmXWk3+YKRkmIR8TYj3RVdEt3ptG2jCJXWNAvQbT0VJJdRyHZ7kG/l1JsZlB+VJq/VcYOo69VR4oD+w==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.10.1",
- "@rc-component/trigger": "^2.0.0",
- "classnames": "2.x",
- "rc-motion": "^2.4.3",
- "rc-overflow": "^1.3.1",
- "rc-util": "^5.27.0"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/rc-motion": {
- "version": "2.9.3",
- "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.3.tgz",
- "integrity": "sha512-rkW47ABVkic7WEB0EKJqzySpvDqwl60/tdkY7hWP7dYnh5pm0SzJpo54oW3TDUGXV5wfxXFmMkxrzRRbotQ0+w==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.11.1",
- "classnames": "^2.2.1",
- "rc-util": "^5.43.0"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/rc-notification": {
- "version": "5.6.2",
- "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.6.2.tgz",
- "integrity": "sha512-Id4IYMoii3zzrG0lB0gD6dPgJx4Iu95Xu0BQrhHIbp7ZnAZbLqdqQ73aIWH0d0UFcElxwaKjnzNovTjo7kXz7g==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.10.1",
- "classnames": "2.x",
- "rc-motion": "^2.9.0",
- "rc-util": "^5.20.1"
- },
- "engines": {
- "node": ">=8.x"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/rc-overflow": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/rc-overflow/-/rc-overflow-1.3.2.tgz",
- "integrity": "sha512-nsUm78jkYAoPygDAcGZeC2VwIg/IBGSodtOY3pMof4W3M9qRJgqaDYm03ZayHlde3I6ipliAxbN0RUcGf5KOzw==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.11.1",
- "classnames": "^2.2.1",
- "rc-resize-observer": "^1.0.0",
- "rc-util": "^5.37.0"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/rc-pagination": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-4.3.0.tgz",
- "integrity": "sha512-UubEWA0ShnroQ1tDa291Fzw6kj0iOeF26IsUObxYTpimgj4/qPCWVFl18RLZE+0Up1IZg0IK4pMn6nB3mjvB7g==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.10.1",
- "classnames": "^2.3.2",
- "rc-util": "^5.38.0"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/rc-picker": {
- "version": "4.8.2",
- "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.8.2.tgz",
- "integrity": "sha512-I6Nn4ngkRskSD//rsXDvjlEQ8CzX9kPQrUIb7+qTY49erJaa3/oKJWmi6JIxo/A7gy59phNmPTdhKosAa/NrQQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.24.7",
- "@rc-component/trigger": "^2.0.0",
- "classnames": "^2.2.1",
- "rc-overflow": "^1.3.2",
- "rc-resize-observer": "^1.4.0",
- "rc-util": "^5.43.0"
- },
- "engines": {
- "node": ">=8.x"
- },
- "peerDependencies": {
- "date-fns": ">= 2.x",
- "dayjs": ">= 1.x",
- "luxon": ">= 3.x",
- "moment": ">= 2.x",
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- },
- "peerDependenciesMeta": {
- "date-fns": {
- "optional": true
- },
- "dayjs": {
- "optional": true
- },
- "luxon": {
- "optional": true
- },
- "moment": {
- "optional": true
- }
- }
- },
- "node_modules/rc-progress": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-4.0.0.tgz",
- "integrity": "sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.10.1",
- "classnames": "^2.2.6",
- "rc-util": "^5.16.1"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/rc-rate": {
- "version": "2.13.0",
- "resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.13.0.tgz",
- "integrity": "sha512-oxvx1Q5k5wD30sjN5tqAyWTvJfLNNJn7Oq3IeS4HxWfAiC4BOXMITNAsw7u/fzdtO4MS8Ki8uRLOzcnEuoQiAw==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.10.1",
- "classnames": "^2.2.5",
- "rc-util": "^5.0.1"
- },
- "engines": {
- "node": ">=8.x"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/rc-resize-observer": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.0.tgz",
- "integrity": "sha512-PnMVyRid9JLxFavTjeDXEXo65HCRqbmLBw9xX9gfC4BZiSzbLXKzW3jPz+J0P71pLbD5tBMTT+mkstV5gD0c9Q==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.20.7",
- "classnames": "^2.2.1",
- "rc-util": "^5.38.0",
- "resize-observer-polyfill": "^1.5.1"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/rc-segmented": {
- "version": "2.5.0",
- "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.5.0.tgz",
- "integrity": "sha512-B28Fe3J9iUFOhFJET3RoXAPFJ2u47QvLSYcZWC4tFYNGPEjug5LAxEasZlA/PpAxhdOPqGWsGbSj7ftneukJnw==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.11.1",
- "classnames": "^2.2.1",
- "rc-motion": "^2.4.4",
- "rc-util": "^5.17.0"
- },
- "peerDependencies": {
- "react": ">=16.0.0",
- "react-dom": ">=16.0.0"
- }
- },
- "node_modules/rc-select": {
- "version": "14.16.3",
- "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.3.tgz",
- "integrity": "sha512-51+j6s3fJJJXB7E+B6W1hM4Tjzv1B/Decooz9ilgegDBt3ZAth1b/xMwYCTrT5BbG2e53XACQsyDib2+3Ro1fg==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.10.1",
- "@rc-component/trigger": "^2.1.1",
- "classnames": "2.x",
- "rc-motion": "^2.0.1",
- "rc-overflow": "^1.3.1",
- "rc-util": "^5.16.1",
- "rc-virtual-list": "^3.5.2"
- },
- "engines": {
- "node": ">=8.x"
- },
- "peerDependencies": {
- "react": "*",
- "react-dom": "*"
- }
- },
- "node_modules/rc-slider": {
- "version": "11.1.7",
- "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.7.tgz",
- "integrity": "sha512-ytYbZei81TX7otdC0QvoYD72XSlxvTihNth5OeZ6PMXyEDq/vHdWFulQmfDGyXK1NwKwSlKgpvINOa88uT5g2A==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.10.1",
- "classnames": "^2.2.5",
- "rc-util": "^5.36.0"
- },
- "engines": {
- "node": ">=8.x"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/rc-steps": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/rc-steps/-/rc-steps-6.0.1.tgz",
- "integrity": "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.16.7",
- "classnames": "^2.2.3",
- "rc-util": "^5.16.1"
- },
- "engines": {
- "node": ">=8.x"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/rc-switch": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/rc-switch/-/rc-switch-4.1.0.tgz",
- "integrity": "sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.21.0",
- "classnames": "^2.2.1",
- "rc-util": "^5.30.0"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/rc-table": {
- "version": "7.48.1",
- "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.48.1.tgz",
- "integrity": "sha512-Z4mDKjWg+xz/Ezdw6ivWcbqRpaJ0QfCORRoRrlrw65KSGZLK8OcTdacH22/fyGb8L4It/0/9qcMm8VrVAk/WBw==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.10.1",
- "@rc-component/context": "^1.4.0",
- "classnames": "^2.2.5",
- "rc-resize-observer": "^1.1.0",
- "rc-util": "^5.41.0",
- "rc-virtual-list": "^3.14.2"
- },
- "engines": {
- "node": ">=8.x"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/rc-tabs": {
- "version": "15.4.0",
- "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.4.0.tgz",
- "integrity": "sha512-llKuyiAVqmXm2z7OrmhX5cNb2ueZaL8ZyA2P4R+6/72NYYcbEgOXibwHiQCFY2RiN3swXl53SIABi2CumUS02g==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.11.2",
- "classnames": "2.x",
- "rc-dropdown": "~4.2.0",
- "rc-menu": "~9.16.0",
- "rc-motion": "^2.6.2",
- "rc-resize-observer": "^1.0.0",
- "rc-util": "^5.34.1"
- },
- "engines": {
- "node": ">=8.x"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/rc-textarea": {
- "version": "1.8.2",
- "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.8.2.tgz",
- "integrity": "sha512-UFAezAqltyR00a8Lf0IPAyTd29Jj9ee8wt8DqXyDMal7r/Cg/nDt3e1OOv3Th4W6mKaZijjgwuPXhAfVNTN8sw==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.10.1",
- "classnames": "^2.2.1",
- "rc-input": "~1.6.0",
- "rc-resize-observer": "^1.0.0",
- "rc-util": "^5.27.0"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/rc-tooltip": {
- "version": "6.2.1",
- "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.2.1.tgz",
- "integrity": "sha512-rws0duD/3sHHsD905Nex7FvoUGy2UBQRhTkKxeEvr2FB+r21HsOxcDJI0TzyO8NHhnAA8ILr8pfbSBg5Jj5KBg==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.11.2",
- "@rc-component/trigger": "^2.0.0",
- "classnames": "^2.3.1"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/rc-tree": {
- "version": "5.10.1",
- "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.10.1.tgz",
- "integrity": "sha512-FPXb3tT/u39mgjr6JNlHaUTYfHkVGW56XaGDahDpEFLGsnPxGcVLNTjcqoQb/GNbSCycl7tD7EvIymwOTP0+Yw==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.10.1",
- "classnames": "2.x",
- "rc-motion": "^2.0.1",
- "rc-util": "^5.16.1",
- "rc-virtual-list": "^3.5.1"
- },
- "engines": {
- "node": ">=10.x"
- },
- "peerDependencies": {
- "react": "*",
- "react-dom": "*"
- }
- },
- "node_modules/rc-tree-select": {
- "version": "5.24.5",
- "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.24.5.tgz",
- "integrity": "sha512-PnyR8LZJWaiEFw0SHRqo4MNQWyyZsyMs8eNmo68uXZWjxc7QqeWcjPPoONN0rc90c3HZqGF9z+Roz+GLzY5GXA==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.25.7",
- "classnames": "2.x",
- "rc-select": "~14.16.2",
- "rc-tree": "~5.10.1",
- "rc-util": "^5.43.0"
- },
- "peerDependencies": {
- "react": "*",
- "react-dom": "*"
- }
- },
- "node_modules/rc-upload": {
- "version": "4.8.1",
- "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.8.1.tgz",
- "integrity": "sha512-toEAhwl4hjLAI1u8/CgKWt30BR06ulPa4iGQSMvSXoHzO88gPCslxqV/mnn4gJU7PDoltGIC9Eh+wkeudqgHyw==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.18.3",
- "classnames": "^2.2.5",
- "rc-util": "^5.2.0"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/rc-util": {
- "version": "5.43.0",
- "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.43.0.tgz",
- "integrity": "sha512-AzC7KKOXFqAdIBqdGWepL9Xn7cm3vnAmjlHqUnoQaTMZYhM4VlXGLkkHHxj/BZ7Td0+SOPKB4RGPboBVKT9htw==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.18.3",
- "react-is": "^18.2.0"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/rc-virtual-list": {
- "version": "3.15.0",
- "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.15.0.tgz",
- "integrity": "sha512-dF2YQztqrU3ijAeWOqscTshCEr7vpimzSqAVjO1AyAmaqcHulaXpnGR0ptK5PXfxTUy48VkJOiglMIxlkYGs0w==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.20.0",
- "classnames": "^2.2.6",
- "rc-resize-observer": "^1.0.0",
- "rc-util": "^5.36.0"
- },
- "engines": {
- "node": ">=8.x"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
- "node_modules/react": {
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
- "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
- "license": "MIT",
- "dependencies": {
- "loose-envify": "^1.1.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/react-dom": {
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
- "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
- "license": "MIT",
- "dependencies": {
- "loose-envify": "^1.1.0",
- "scheduler": "^0.23.2"
- },
- "peerDependencies": {
- "react": "^18.3.1"
- }
- },
- "node_modules/react-is": {
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
- "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
- "license": "MIT"
- },
- "node_modules/react-lazy-load-image-component": {
- "version": "1.6.2",
- "resolved": "https://registry.npmjs.org/react-lazy-load-image-component/-/react-lazy-load-image-component-1.6.2.tgz",
- "integrity": "sha512-dAdH5PsRgvDMlHC7QpZRA9oRzEZl1kPFwowmR9Mt0IUUhxk2wwq43PB6Ffwv84HFYuPmsxDUCka0E9KVXi8roQ==",
- "license": "MIT",
- "dependencies": {
- "lodash.debounce": "^4.0.8",
- "lodash.throttle": "^4.1.1"
- },
- "peerDependencies": {
- "react": "^15.x.x || ^16.x.x || ^17.x.x || ^18.x.x"
- }
- },
- "node_modules/react-leaflet": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz",
- "integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==",
- "license": "Hippocratic-2.1",
- "dependencies": {
- "@react-leaflet/core": "^2.1.0"
- },
- "peerDependencies": {
- "leaflet": "^1.9.0",
- "react": "^18.0.0",
- "react-dom": "^18.0.0"
- }
- },
- "node_modules/react-map-gl": {
- "version": "7.1.7",
- "resolved": "https://registry.npmjs.org/react-map-gl/-/react-map-gl-7.1.7.tgz",
- "integrity": "sha512-mwjc0obkBJOXCcoXQr3VoLqmqwo9vS4bXfbGsdxXzEgVCv/PM0v+1QggL7W0d/ccIy+VCjbXNlGij+PENz6VNg==",
- "license": "MIT",
- "dependencies": {
- "@maplibre/maplibre-gl-style-spec": "^19.2.1",
- "@types/mapbox-gl": ">=1.0.0"
- },
- "peerDependencies": {
- "mapbox-gl": ">=1.13.0",
- "maplibre-gl": ">=1.13.0",
- "react": ">=16.3.0",
- "react-dom": ">=16.3.0"
- },
- "peerDependenciesMeta": {
- "mapbox-gl": {
- "optional": true
- },
- "maplibre-gl": {
- "optional": true
- }
- }
- },
- "node_modules/react-redux": {
- "version": "8.1.3",
- "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz",
- "integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.12.1",
- "@types/hoist-non-react-statics": "^3.3.1",
- "@types/use-sync-external-store": "^0.0.3",
- "hoist-non-react-statics": "^3.3.2",
- "react-is": "^18.0.0",
- "use-sync-external-store": "^1.0.0"
- },
- "peerDependencies": {
- "@types/react": "^16.8 || ^17.0 || ^18.0",
- "@types/react-dom": "^16.8 || ^17.0 || ^18.0",
- "react": "^16.8 || ^17.0 || ^18.0",
- "react-dom": "^16.8 || ^17.0 || ^18.0",
- "react-native": ">=0.59",
- "redux": "^4 || ^5.0.0-beta.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- },
- "react-dom": {
- "optional": true
- },
- "react-native": {
- "optional": true
- },
- "redux": {
- "optional": true
- }
- }
- },
- "node_modules/react-refresh": {
- "version": "0.14.2",
- "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
- "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/react-router": {
- "version": "6.28.0",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.28.0.tgz",
- "integrity": "sha512-HrYdIFqdrnhDw0PqG/AKjAqEqM7AvxCz0DQ4h2W8k6nqmc5uRBYDag0SBxx9iYz5G8gnuNVLzUe13wl9eAsXXg==",
- "license": "MIT",
- "dependencies": {
- "@remix-run/router": "1.21.0"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "peerDependencies": {
- "react": ">=16.8"
- }
- },
- "node_modules/react-router-dom": {
- "version": "6.28.0",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.28.0.tgz",
- "integrity": "sha512-kQ7Unsl5YdyOltsPGl31zOjLrDv+m2VcIEcIHqYYD3Lp0UppLjrzcfJqDJwXxFw3TH/yvapbnUvPlAj7Kx5nbg==",
- "license": "MIT",
- "dependencies": {
- "@remix-run/router": "1.21.0",
- "react-router": "6.28.0"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "peerDependencies": {
- "react": ">=16.8",
- "react-dom": ">=16.8"
- }
- },
- "node_modules/redux": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
- "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.9.2"
- }
- },
- "node_modules/redux-logger": {
- "version": "3.0.6",
- "resolved": "https://registry.npmjs.org/redux-logger/-/redux-logger-3.0.6.tgz",
- "integrity": "sha512-JoCIok7bg/XpqA1JqCqXFypuqBbQzGQySrhFzewB7ThcnysTO30l4VCst86AuB9T9tuT03MAA56Jw2PNhRSNCg==",
- "license": "MIT",
- "dependencies": {
- "deep-diff": "^0.3.5"
- }
- },
- "node_modules/redux-thunk": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz",
- "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==",
- "license": "MIT",
- "peerDependencies": {
- "redux": "^4"
- }
- },
- "node_modules/regenerator-runtime": {
- "version": "0.14.1",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
- "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
- "license": "MIT"
- },
- "node_modules/resize-observer-polyfill": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
- "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
- "license": "MIT"
- },
- "node_modules/resolve-protobuf-schema": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
- "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
- "license": "MIT",
- "dependencies": {
- "protocol-buffers-schema": "^3.3.1"
- }
- },
- "node_modules/rollup": {
- "version": "4.27.4",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.27.4.tgz",
- "integrity": "sha512-RLKxqHEMjh/RGLsDxAEsaLO3mWgyoU6x9w6n1ikAzet4B3gI2/3yP6PWY2p9QzRTh6MfEIXB3MwsOY0Iv3vNrw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/estree": "1.0.6"
- },
- "bin": {
- "rollup": "dist/bin/rollup"
- },
- "engines": {
- "node": ">=18.0.0",
- "npm": ">=8.0.0"
- },
- "optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.27.4",
- "@rollup/rollup-android-arm64": "4.27.4",
- "@rollup/rollup-darwin-arm64": "4.27.4",
- "@rollup/rollup-darwin-x64": "4.27.4",
- "@rollup/rollup-freebsd-arm64": "4.27.4",
- "@rollup/rollup-freebsd-x64": "4.27.4",
- "@rollup/rollup-linux-arm-gnueabihf": "4.27.4",
- "@rollup/rollup-linux-arm-musleabihf": "4.27.4",
- "@rollup/rollup-linux-arm64-gnu": "4.27.4",
- "@rollup/rollup-linux-arm64-musl": "4.27.4",
- "@rollup/rollup-linux-powerpc64le-gnu": "4.27.4",
- "@rollup/rollup-linux-riscv64-gnu": "4.27.4",
- "@rollup/rollup-linux-s390x-gnu": "4.27.4",
- "@rollup/rollup-linux-x64-gnu": "4.27.4",
- "@rollup/rollup-linux-x64-musl": "4.27.4",
- "@rollup/rollup-win32-arm64-msvc": "4.27.4",
- "@rollup/rollup-win32-ia32-msvc": "4.27.4",
- "@rollup/rollup-win32-x64-msvc": "4.27.4",
- "fsevents": "~2.3.2"
- }
- },
- "node_modules/rw": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
- "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
- "license": "BSD-3-Clause"
- },
- "node_modules/scheduler": {
- "version": "0.23.2",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
- "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
- "license": "MIT",
- "dependencies": {
- "loose-envify": "^1.1.0"
- }
- },
- "node_modules/scroll-into-view-if-needed": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",
- "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==",
- "license": "MIT",
- "dependencies": {
- "compute-scroll-into-view": "^3.0.2"
- }
- },
- "node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
- "node_modules/set-value": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
- "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
- "license": "MIT",
- "dependencies": {
- "extend-shallow": "^2.0.1",
- "is-extendable": "^0.1.1",
- "is-plain-object": "^2.0.3",
- "split-string": "^3.0.1"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/sort-asc": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/sort-asc/-/sort-asc-0.2.0.tgz",
- "integrity": "sha512-umMGhjPeHAI6YjABoSTrFp2zaBtXBej1a0yKkuMUyjjqu6FJsTF+JYwCswWDg+zJfk/5npWUUbd33HH/WLzpaA==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/sort-desc": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/sort-desc/-/sort-desc-0.2.0.tgz",
- "integrity": "sha512-NqZqyvL4VPW+RAxxXnB8gvE1kyikh8+pR+T+CXLksVRN9eiQqkQlPwqWYU0mF9Jm7UnctShlxLyAt1CaBOTL1w==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/sort-object": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/sort-object/-/sort-object-3.0.3.tgz",
- "integrity": "sha512-nK7WOY8jik6zaG9CRwZTaD5O7ETWDLZYMM12pqY8htll+7dYeqGfEUPcUBHOpSJg2vJOrvFIY2Dl5cX2ih1hAQ==",
- "license": "MIT",
- "dependencies": {
- "bytewise": "^1.1.0",
- "get-value": "^2.0.2",
- "is-extendable": "^0.1.1",
- "sort-asc": "^0.2.0",
- "sort-desc": "^0.2.0",
- "union-value": "^1.0.1"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/source-map-js": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
- "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
- "dev": true,
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/split-string": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
- "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==",
- "license": "MIT",
- "dependencies": {
- "extend-shallow": "^3.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/split-string/node_modules/extend-shallow": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
- "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==",
- "license": "MIT",
- "dependencies": {
- "assign-symbols": "^1.0.0",
- "is-extendable": "^1.0.1"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/split-string/node_modules/is-extendable": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
- "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
- "license": "MIT",
- "dependencies": {
- "is-plain-object": "^2.0.4"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/string-convert": {
- "version": "0.2.1",
- "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz",
- "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==",
- "license": "MIT"
- },
- "node_modules/stylis": {
- "version": "4.3.4",
- "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.4.tgz",
- "integrity": "sha512-osIBl6BGUmSfDkyH2mB7EFvCJntXDrLhKjHTRj/rK6xLH0yuPrHULDRQzKokSOD4VoorhtKpfcfW1GAntu8now==",
- "license": "MIT"
- },
- "node_modules/supercluster": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz",
- "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==",
- "license": "ISC",
- "dependencies": {
- "kdbush": "^4.0.2"
- }
- },
- "node_modules/throttle-debounce": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
- "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==",
- "license": "MIT",
- "engines": {
- "node": ">=12.22"
- }
- },
- "node_modules/tinyqueue": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz",
- "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==",
- "license": "ISC"
- },
- "node_modules/toggle-selection": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
- "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==",
- "license": "MIT"
- },
- "node_modules/typewise": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/typewise/-/typewise-1.0.3.tgz",
- "integrity": "sha512-aXofE06xGhaQSPzt8hlTY+/YWQhm9P0jYUp1f2XtmW/3Bk0qzXcyFWAtPoo2uTGQj1ZwbDuSyuxicq+aDo8lCQ==",
- "license": "MIT",
- "dependencies": {
- "typewise-core": "^1.2.0"
- }
- },
- "node_modules/typewise-core": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/typewise-core/-/typewise-core-1.2.0.tgz",
- "integrity": "sha512-2SCC/WLzj2SbUwzFOzqMCkz5amXLlxtJqDKTICqg30x+2DZxcfZN2MvQZmGfXWKNWaKK9pBPsvkcwv8bF/gxKg==",
- "license": "MIT"
- },
- "node_modules/union-value": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
- "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==",
- "license": "MIT",
- "dependencies": {
- "arr-union": "^3.1.0",
- "get-value": "^2.0.6",
- "is-extendable": "^0.1.1",
- "set-value": "^2.0.1"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/update-browserslist-db": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
- "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/browserslist"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "escalade": "^3.2.0",
- "picocolors": "^1.1.0"
- },
- "bin": {
- "update-browserslist-db": "cli.js"
- },
- "peerDependencies": {
- "browserslist": ">= 4.21.0"
- }
- },
- "node_modules/use-sync-external-store": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz",
- "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==",
- "license": "MIT",
- "peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
- }
- },
- "node_modules/vite": {
- "version": "5.4.11",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
- "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "esbuild": "^0.21.3",
- "postcss": "^8.4.43",
- "rollup": "^4.20.0"
- },
- "bin": {
- "vite": "bin/vite.js"
- },
- "engines": {
- "node": "^18.0.0 || >=20.0.0"
- },
- "funding": {
- "url": "https://github.com/vitejs/vite?sponsor=1"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.3"
- },
- "peerDependencies": {
- "@types/node": "^18.0.0 || >=20.0.0",
- "less": "*",
- "lightningcss": "^1.21.0",
- "sass": "*",
- "sass-embedded": "*",
- "stylus": "*",
- "sugarss": "*",
- "terser": "^5.4.0"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- },
- "less": {
- "optional": true
- },
- "lightningcss": {
- "optional": true
- },
- "sass": {
- "optional": true
- },
- "sass-embedded": {
- "optional": true
- },
- "stylus": {
- "optional": true
- },
- "sugarss": {
- "optional": true
- },
- "terser": {
- "optional": true
- }
- }
- },
- "node_modules/vt-pbf": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz",
- "integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==",
- "license": "MIT",
- "dependencies": {
- "@mapbox/point-geometry": "0.1.0",
- "@mapbox/vector-tile": "^1.3.1",
- "pbf": "^3.2.1"
- }
- },
- "node_modules/which": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
- "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
- "license": "ISC",
- "dependencies": {
- "isexe": "^2.0.0"
- },
- "bin": {
- "which": "bin/which"
- }
- },
- "node_modules/yallist": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
- "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
- "dev": true,
- "license": "ISC"
- }
- }
-}
diff --git a/frontend/package.json b/frontend/package.json
deleted file mode 100644
index c28c545..0000000
--- a/frontend/package.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "name": "frontend",
- "private": true,
- "version": "0.0.0",
- "scripts": {
- "dev": "vite",
- "build": "vite build",
- "preview": "vite preview"
- },
- "dependencies": {
- "antd": "^5.22.1",
- "axios": "^1.6.3",
- "leaflet": "^1.9.1",
- "mapbox-gl": "^2.10.0",
- "maplibre-gl": "^2.4.0",
- "moment": "^2.29.4",
- "react": "^18.0.0",
- "react-dom": "^18.0.0",
- "react-lazy-load-image-component": "^1.5.5",
- "react-leaflet": "^4.0.2",
- "react-map-gl": "^7.0.19",
- "react-redux": "^8.0.2",
- "react-router-dom": "^6.3.0",
- "redux": "^4.2.0",
- "redux-logger": "^3.0.6",
- "redux-thunk": "^2.4.1"
- },
- "devDependencies": {
- "@types/react": "^18.0.0",
- "@types/react-dom": "^18.0.0",
- "@vitejs/plugin-react": "^4.2.1",
- "vite": "^5.1.5"
- }
-}
diff --git a/frontend/package.json.md5 b/frontend/package.json.md5
deleted file mode 100755
index 52d3822..0000000
--- a/frontend/package.json.md5
+++ /dev/null
@@ -1 +0,0 @@
-85c9b773b391c3dc08c46bfe300878af
\ No newline at end of file
diff --git a/frontend/src/components/Gallery.jsx b/frontend/src/components/Gallery.jsx
deleted file mode 100644
index 0ee9adf..0000000
--- a/frontend/src/components/Gallery.jsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import React from 'react';
-import { LazyLoadImage, trackWindowScroll }
- from 'react-lazy-load-image-component';
-import { Row, Col } from 'antd';
-import "./loading/loading.css"
-import Loading from './loading/loading.svg';
-import { config } from '../store';
-const Gallery = ({ images, imageSize, addElementRef, selectPhoto, getStyle, scrollPosition }) => {
-
- const spanWidth = (size) => {
- console.log("CHANGE_SIZE", size)
- switch (size) {
- case "xsmall":
- return 2;
- case "small":
- return 4;
- case "medium":
- return 6;
- case "large":
- return 12;
- case "xlarge":
- return 24;
- default: return 2;
- }
- }
- return (
-
- {images.map((image, index) =>
-
-
- selectPhoto(e, image)}>
-
-
-
-
- )}
-
- );
-}
-// Wrap Gallery with trackWindowScroll HOC so it receives
-// a scrollPosition prop to pass down to the images
-export default trackWindowScroll(Gallery);
-
diff --git a/frontend/src/components/Map/Map.css b/frontend/src/components/Map/Map.css
deleted file mode 100644
index 53ab9ad..0000000
--- a/frontend/src/components/Map/Map.css
+++ /dev/null
@@ -1,30 +0,0 @@
-.map-container {
- width: 100%;
- height: 100%;
-}
-
-.sidebarStyle {
- display: inline-block;
- position: absolute;
- top: 0;
- left: 0;
- margin: 12px;
- background-color: #404040;
- color: #ffffff;
- z-index: 1 !important;
- padding: 6px;
- font-weight: bold;
-}
-
-.map-model > * >.ant-modal-body {
- padding: 0px;
- margin: 6px;
-}
-.map-model > * > .ant-modal-header {
- padding: 0px;
- margin-bottom: 10px;
-
-}
-.map-model > * > .ant-modal-close {
- padding: 2px;
-}
\ No newline at end of file
diff --git a/frontend/src/components/Map/Map.jsx b/frontend/src/components/Map/Map.jsx
deleted file mode 100644
index e994ef9..0000000
--- a/frontend/src/components/Map/Map.jsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { useRef } from 'react';
-import { MapContainer, Marker, Popup, TileLayer, useMap, useMapEvents } from 'react-leaflet'
-
-const LocationMarker = ({onLocation, center}) => {
- const [position, setPosition] = useState(center)
- const map = useMapEvents({
- click(e) {
- console.log("Map click evebt", e.latlng)
- setPosition(e.latlng)
- onLocation(e.latlng)
- }
- })
-
- return position === null || (position.lat === 0 && position.lng === 0) ? null : (
-
- You are here
-
- )
-}
-
-const ComponentResize = () => {
- const map = useMap()
-
- setTimeout(() => {
- console.log("MAP CALLED")
- map.invalidateSize()
- }, 0)
-
- return null
-}
-
-const MapView = (props) => {
- let center = [props.lat, props.lng]
- if (props.lng === 0 && props.lat === 0) {
- center = [51.505, -0.09]
- }
- return (
-
-
-
-
-
- )
-};
-
-export default MapView;
diff --git a/frontend/src/components/Map/index.jsx b/frontend/src/components/Map/index.jsx
deleted file mode 100644
index 164b422..0000000
--- a/frontend/src/components/Map/index.jsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import React, {useState} from 'react';
-import { Modal, Input, Button} from 'antd';
-import Map from './Map'
-import "./Map.css"
-
-const defaultLat = 52.56815737826566
-const defaultLng =-1.4654394633258416
-
-const InputGroup = Input.Group;
-export const LocationModal = (props) =>{
- const [visable, setVisable] = useState(false);
-
- const [lng, setLng] = useState(defaultLng);
- const [lat, setLat] = useState(defaultLat);
-
- React.useEffect(() => {
- if (props.lat !== undefined){
- setLat(props.lat);
- }
- }, [props.lat]);
-
- React.useEffect(() => {
- if (props.lng !== undefined){
- setLng(props.lng);
- }
- }, [props.lng]);
-
-
- const onUpdate = () => {
- props.onUpdate(lat,lng);
- setVisable(false)
- }
- const onCancel = () => {setVisable(false)}
-
- const onLocationChange = ({lat, lng}) => {
- setLng(lng)
- setLat(lat)
- }
-
- return (
-
- )
-}
-export {default as Map} from "./Map"
\ No newline at end of file
diff --git a/frontend/src/components/addCollection.jsx b/frontend/src/components/addCollection.jsx
deleted file mode 100644
index bef582a..0000000
--- a/frontend/src/components/addCollection.jsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import React from 'react';
-import { Modal, Input, TreeSelect, Form } from 'antd';
-import {galleryActions, collectionActions} from '../store/actions'
-import { useDispatch, useSelector } from 'react-redux';
-
-const AddCollection = () => {
- const {addCollectionModalVisable} = useSelector(state => state.GalleryReducer);
- const {collections} = useSelector(state => state.CollectionsReducer)
-
- const [form] = Form.useForm();
- const dispatch = useDispatch()
-
- const handleCancel = () => {
- dispatch(galleryActions.hideAdd())
- };
-
- const handleCreate = () => {
- form.validateFields().then(values => {
- console.log('Received values of form: ', values);
- form.resetFields();
- dispatch(collectionActions.create(values))
- dispatch(galleryActions.hideAdd())
- })
- .catch(err => {
- console.log("error:",err)
- })
- };
-
- return (
-
-
-
-
-
-
-
-
-
- );
-}
-export default (AddCollection)
diff --git a/frontend/src/components/header.jsx b/frontend/src/components/header.jsx
deleted file mode 100644
index 8c3b764..0000000
--- a/frontend/src/components/header.jsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import React from 'react';
-import { DesktopOutlined, SearchOutlined, SettingOutlined } from '@ant-design/icons';
-// as an array
-import { Layout, Menu, Input, Avatar } from 'antd';
-import { userActions } from '../store/actions';
-import { Link } from "react-router-dom";
-import { useDispatch, useSelector } from 'react-redux';
-import { useLocation } from "react-router-dom";
-
-const { Header } = Layout;
-const { SubMenu } = Menu;
-
-const HeaderComponent = ({ search }) => {
- const dispatch = useDispatch()
- const location = useLocation();
- const { email } = useSelector(state => state.UserReducer)
- const logout = () => { dispatch(userActions.logout()) }
- const handleSearch = (e) => { search({ key: e.target.value }) }
-
- const get_gravatar = (size) => {
- var MD5 = function (s) { function L(k, d) { return (k << d) | (k >>> (32 - d)) } function K(G, k) { var I, d, F, H, x; F = (G & 2147483648); H = (k & 2147483648); I = (G & 1073741824); d = (k & 1073741824); x = (G & 1073741823) + (k & 1073741823); if (I & d) { return (x ^ 2147483648 ^ F ^ H) } if (I | d) { if (x & 1073741824) { return (x ^ 3221225472 ^ F ^ H) } else { return (x ^ 1073741824 ^ F ^ H) } } else { return (x ^ F ^ H) } } function r(d, F, k) { return (d & F) | ((~d) & k) } function q(d, F, k) { return (d & k) | (F & (~k)) } function p(d, F, k) { return (d ^ F ^ k) } function n(d, F, k) { return (F ^ (d | (~k))) } function u(G, F, aa, Z, k, H, I) { G = K(G, K(K(r(F, aa, Z), k), I)); return K(L(G, H), F) } function f(G, F, aa, Z, k, H, I) { G = K(G, K(K(q(F, aa, Z), k), I)); return K(L(G, H), F) } function D(G, F, aa, Z, k, H, I) { G = K(G, K(K(p(F, aa, Z), k), I)); return K(L(G, H), F) } function t(G, F, aa, Z, k, H, I) { G = K(G, K(K(n(F, aa, Z), k), I)); return K(L(G, H), F) } function e(G) { var Z; var F = G.length; var x = F + 8; var k = (x - (x % 64)) / 64; var I = (k + 1) * 16; var aa = Array(I - 1); var d = 0; var H = 0; while (H < F) { Z = (H - (H % 4)) / 4; d = (H % 4) * 8; aa[Z] = (aa[Z] | (G.charCodeAt(H) << d)); H++ } Z = (H - (H % 4)) / 4; d = (H % 4) * 8; aa[Z] = aa[Z] | (128 << d); aa[I - 2] = F << 3; aa[I - 1] = F >>> 29; return aa } function B(x) { var k = "", F = "", G, d; for (d = 0; d <= 3; d++) { G = (x >>> (d * 8)) & 255; F = "0" + G.toString(16); k = k + F.substr(F.length - 2, 2) } return k } function J(k) { k = k.replace(/rn/g, "n"); var d = ""; for (var F = 0; F < k.length; F++) { var x = k.charCodeAt(F); if (x < 128) { d += String.fromCharCode(x) } else { if ((x > 127) && (x < 2048)) { d += String.fromCharCode((x >> 6) | 192); d += String.fromCharCode((x & 63) | 128) } else { d += String.fromCharCode((x >> 12) | 224); d += String.fromCharCode(((x >> 6) & 63) | 128); d += String.fromCharCode((x & 63) | 128) } } } return d } var C = []; var P, h, E, v, g, Y, X, W, V; var S = 7, Q = 12, N = 17, M = 22; var A = 5, z = 9, y = 14, w = 20; var o = 4, m = 11, l = 16, j = 23; var U = 6, T = 10, R = 15, O = 21; s = J(s); C = e(s); Y = 1732584193; X = 4023233417; W = 2562383102; V = 271733878; for (P = 0; P < C.length; P += 16) { h = Y; E = X; v = W; g = V; Y = u(Y, X, W, V, C[P + 0], S, 3614090360); V = u(V, Y, X, W, C[P + 1], Q, 3905402710); W = u(W, V, Y, X, C[P + 2], N, 606105819); X = u(X, W, V, Y, C[P + 3], M, 3250441966); Y = u(Y, X, W, V, C[P + 4], S, 4118548399); V = u(V, Y, X, W, C[P + 5], Q, 1200080426); W = u(W, V, Y, X, C[P + 6], N, 2821735955); X = u(X, W, V, Y, C[P + 7], M, 4249261313); Y = u(Y, X, W, V, C[P + 8], S, 1770035416); V = u(V, Y, X, W, C[P + 9], Q, 2336552879); W = u(W, V, Y, X, C[P + 10], N, 4294925233); X = u(X, W, V, Y, C[P + 11], M, 2304563134); Y = u(Y, X, W, V, C[P + 12], S, 1804603682); V = u(V, Y, X, W, C[P + 13], Q, 4254626195); W = u(W, V, Y, X, C[P + 14], N, 2792965006); X = u(X, W, V, Y, C[P + 15], M, 1236535329); Y = f(Y, X, W, V, C[P + 1], A, 4129170786); V = f(V, Y, X, W, C[P + 6], z, 3225465664); W = f(W, V, Y, X, C[P + 11], y, 643717713); X = f(X, W, V, Y, C[P + 0], w, 3921069994); Y = f(Y, X, W, V, C[P + 5], A, 3593408605); V = f(V, Y, X, W, C[P + 10], z, 38016083); W = f(W, V, Y, X, C[P + 15], y, 3634488961); X = f(X, W, V, Y, C[P + 4], w, 3889429448); Y = f(Y, X, W, V, C[P + 9], A, 568446438); V = f(V, Y, X, W, C[P + 14], z, 3275163606); W = f(W, V, Y, X, C[P + 3], y, 4107603335); X = f(X, W, V, Y, C[P + 8], w, 1163531501); Y = f(Y, X, W, V, C[P + 13], A, 2850285829); V = f(V, Y, X, W, C[P + 2], z, 4243563512); W = f(W, V, Y, X, C[P + 7], y, 1735328473); X = f(X, W, V, Y, C[P + 12], w, 2368359562); Y = D(Y, X, W, V, C[P + 5], o, 4294588738); V = D(V, Y, X, W, C[P + 8], m, 2272392833); W = D(W, V, Y, X, C[P + 11], l, 1839030562); X = D(X, W, V, Y, C[P + 14], j, 4259657740); Y = D(Y, X, W, V, C[P + 1], o, 2763975236); V = D(V, Y, X, W, C[P + 4], m, 1272893353); W = D(W, V, Y, X, C[P + 7], l, 4139469664); X = D(X, W, V, Y, C[P + 10], j, 3200236656); Y = D(Y, X, W, V, C[P + 13], o, 681279174); V = D(V, Y, X, W, C[P + 0], m, 3936430074); W = D(W, V, Y, X, C[P + 3], l, 3572445317); X = D(X, W, V, Y, C[P + 6], j, 76029189); Y = D(Y, X, W, V, C[P + 9], o, 3654602809); V = D(V, Y, X, W, C[P + 12], m, 3873151461); W = D(W, V, Y, X, C[P + 15], l, 530742520); X = D(X, W, V, Y, C[P + 2], j, 3299628645); Y = t(Y, X, W, V, C[P + 0], U, 4096336452); V = t(V, Y, X, W, C[P + 7], T, 1126891415); W = t(W, V, Y, X, C[P + 14], R, 2878612391); X = t(X, W, V, Y, C[P + 5], O, 4237533241); Y = t(Y, X, W, V, C[P + 12], U, 1700485571); V = t(V, Y, X, W, C[P + 3], T, 2399980690); W = t(W, V, Y, X, C[P + 10], R, 4293915773); X = t(X, W, V, Y, C[P + 1], O, 2240044497); Y = t(Y, X, W, V, C[P + 8], U, 1873313359); V = t(V, Y, X, W, C[P + 15], T, 4264355552); W = t(W, V, Y, X, C[P + 6], R, 2734768916); X = t(X, W, V, Y, C[P + 13], O, 1309151649); Y = t(Y, X, W, V, C[P + 4], U, 4149444226); V = t(V, Y, X, W, C[P + 11], T, 3174756917); W = t(W, V, Y, X, C[P + 2], R, 718787259); X = t(X, W, V, Y, C[P + 9], O, 3951481745); Y = K(Y, h); X = K(X, E); W = K(W, v); V = K(V, g) } var i = B(Y) + B(X) + B(W) + B(V); return i.toLowerCase() };
- size = size || 80;
- return 'https://www.gravatar.com/avatar/' + MD5(email || "") + '.jpg?s=' + size;
- }
- return (
-
- );
-}
-
-export default HeaderComponent
-
diff --git a/frontend/src/components/loading/loading.css b/frontend/src/components/loading/loading.css
deleted file mode 100644
index 715c1f9..0000000
--- a/frontend/src/components/loading/loading.css
+++ /dev/null
@@ -1,39 +0,0 @@
- .container {
- --uib-size: 35px;
- --uib-color: black;
- --uib-speed: 1.2s;
- --uib-bg-opacity: .1;
- height: var(--uib-size);
- width: var(--uib-size);
- transform-origin: center;
- will-change: transform;
- overflow: visible;
- }
-
- .car {
- fill: none;
- stroke: var(--uib-color);
- stroke-dasharray: 25, 75;
- stroke-dashoffset: 0;
- animation: travel var(--uib-speed) linear infinite;
- will-change: stroke-dasharray, stroke-dashoffset;
- transition: stroke 0.5s ease;
- }
-
- .track {
- fill: none;
- stroke: var(--uib-color);
- opacity: var(--uib-bg-opacity);
- transition: stroke 0.5s ease;
- }
-
- @keyframes travel {
- 0% {
- stroke-dashoffset: 0;
- }
-
- 100% {
- stroke-dashoffset: -100;
- }
- }
-
diff --git a/frontend/src/components/loading/loading.jsx b/frontend/src/components/loading/loading.jsx
deleted file mode 100644
index 37cbaf8..0000000
--- a/frontend/src/components/loading/loading.jsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import "./loading.css"
-
-export const Loading = () => (
-
-
-
-
-)
\ No newline at end of file
diff --git a/frontend/src/components/loading/loading.svg b/frontend/src/components/loading/loading.svg
deleted file mode 100644
index 94a3172..0000000
--- a/frontend/src/components/loading/loading.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/frontend/src/components/modal.jsx b/frontend/src/components/modal.jsx
deleted file mode 100644
index cfda359..0000000
--- a/frontend/src/components/modal.jsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import React, { useState } from 'react';
-import { ContainerOutlined, DeleteOutlined } from '@ant-design/icons';
-import { Modal, Button, TreeSelect,Form } from 'antd';
-
-import { useDispatch, useSelector } from 'react-redux';
-import { collectionActions } from '../store/actions';
-
-const { confirm } = Modal;
-const ButtonGroup = Button.Group;
-
-const MoveModal = ({selectedPhotos}) => {
- const [visible, setVisable] = useState(false)
- const [form] = Form.useForm();
- const dispatch = useDispatch()
-
- const { photos } = useSelector(state => state.PhotoReducer)
- const {collections} = useSelector(state => state.CollectionsReducer);
-
- const showModal = () => {
- setVisable(true)
- };
-
- const handleCancel = () => {
- setVisable(false)
- };
-
- const showDeleteConfirm = () =>{
- confirm({
- title: 'Are you sure delete these photos?',
- content: '',
- okText: 'Yes',
- okType: 'danger',
- cancelText: 'No',
- onOk() {
- selectedPhotos.forEach(pos => {
- let photo = photos[pos]
- dispatch(collectionActions.remove(photo.id))
- })
-
- console.log('OK');
- },
- onCancel() {
- console.log('Cancel');
- },
- });
- }
-
- const handleCreate = () => {
- form.validateFields().then(values => {
- values["photos"] = selectedPhotos.map(pos => photos[pos])
- console.log('Received values of Move form: ', values);
- dispatch(collectionActions.move(values))
- setVisable(false)
- })
- .catch(err => {})
- };
-
- return (
-
-
- move
- delete
-
-
-
-
-
-
-
-
- );
-}
-
-
-export default MoveModal
diff --git a/frontend/src/components/settings/AlbumSettings.jsx b/frontend/src/components/settings/AlbumSettings.jsx
deleted file mode 100644
index 917032b..0000000
--- a/frontend/src/components/settings/AlbumSettings.jsx
+++ /dev/null
@@ -1,168 +0,0 @@
-import React, {useState, useEffect} from 'react';
-import { Form } from "antd"
-import { Input, Divider, Button, Tree, Row, Col, Select } from 'antd';
-import { useDispatch, useSelector } from 'react-redux';
-import { collectionActions } from '../../store/actions';
-import {notify} from '../../store/actions';
-import {LocationModal} from '../Map'
-
-const { Option } = Select;
-const formItemLayout = {
- labelCol: {
- xs: { span: 24 },
- sm: { span: 5 },
- },
- wrapperCol: {
- xs: { span: 24 },
- sm: { span: 19 },
- },
-};
-const tailFormItemLayout = {
- wrapperCol: {
- xs: {
- span: 24,
- offset: 0,
- },
- sm: {
- span: 8,
- offset: 8,
- },
- },
-};
-
-const AlbumSettings = () => {
-
- const photos = useSelector(state =>state.PhotoReducer.photos);
- const collections = useSelector(state =>state.CollectionsReducer.collections);
- const dispatch = useDispatch();
-
- const [albumName, setAlbumName] = useState("")
- const [albumPic, setAlbumPic] = useState("")
- const [albumID, setAlbumID] = useState("")
-
- const [GPS, setGPS] = useState({
- latitude:0,
- longitude:0
- })
-
- const [form] = Form.useForm();
- useEffect(() => {
- form.setFieldsValue({
- albumName: albumName,
- albumPic: albumPic,
- albumID: albumID,
- });
- }, [form, albumName, albumPic,albumID]);
-
-
-
-
-
- const handleSubmit = () => {
- if(albumID === ""){
- notify("warning", "Please Select album")
- return;
- }
- dispatch(collectionActions.update({
- id: albumID,
- name: albumName,
- profile_image: albumPic,
- GPS: GPS
- }))
- };
-
- const onTreeSelect = (selectedKeys, info) => {
- let alb = findInTree(collections, selectedKeys[0])
- console.log("TREE_SELECT", alb, selectedKeys)
- if(alb === undefined){
- return
- }
- setAlbumID(alb.id)
- setAlbumPic(alb.profile_image)
- setAlbumName(alb.name)
- setGPS(alb.gps)
-
- };
-
- const onChange = (value) => {
- setAlbumPic(value)
- }
-
- const updateAlbumName = (evt) => {
- setAlbumName(evt.target.value)
- }
-
- const updateGPS = (lat, lng) => {
- setGPS(
- {
- latitude:lat,
- longitude:lng
- }
- )
- }
-
-
- const findInTree = (tree, id) => {
- let el
- const proceesNode = (node) => {
- if (node.id === id) {
- el = node
- return
- }
- return node.children.map(n => proceesNode(n))
- }
- tree = Object.values(tree)
- tree.map(node => proceesNode(node))
- return el
- }
-
-
- console.log("COLLECTIONS:", collections)
- return (
-
-
-
-
-
-
-
-
-
-
- option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
- }
- >
- {photos.map((el, index) => ({el.name} ))}
-
-
-
-
-
-
-
-
-
-
- Update
-
-
-
-
-
- );
- }
-
-export default AlbumSettings;
diff --git a/frontend/src/components/settings/DeploymentForm.jsx b/frontend/src/components/settings/DeploymentForm.jsx
deleted file mode 100644
index 527fd61..0000000
--- a/frontend/src/components/settings/DeploymentForm.jsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import React, { useEffect } from 'react';
-import { Form, Switch } from 'antd';
-import { Input, Select, InputNumber, Divider, Button } from 'antd';
-import EditableTagGroup from './EditableTagGroup';
-
-import { useDispatch, useSelector } from 'react-redux';
-import { settingsActions } from '../../store/actions';
-
-const { Option } = Select;
-const formItemLayout = {
- labelCol: {
- xs: { span: 24 },
- sm: { span: 5 },
- },
- wrapperCol: {
- xs: { span: 24 },
- sm: { span: 19 },
- },
-};
-const tailFormItemLayout = {
- wrapperCol: {
- xs: {
- span: 24,
- offset: 0,
- },
- sm: {
- span: 8,
- offset: 8,
- },
- },
-};
-
-
-const DeploymentForm = () => {
- const [form] = Form.useForm();
- const deploy = useSelector(state => state.SettingsReducer.deploy);
- const dispatch = useDispatch();
-
- useEffect(() => {
- console.log("DEPLOY:",deploy);
- form.setFieldsValue({
- SiteId: deploy.SiteId,
- AuthToken: deploy.AuthToken,
- Draft: deploy.Draft
- });
- }, [form,deploy]);
-
- const handleSubmit = () => {
-
- form.validateFields().then(values => {
- console.log('Received values of form: ', values);
- dispatch(settingsActions.setDeploy(values))
- });
- };
-
- return (
-
-
-
-
-
-
- Save
-
-
-
-
- )
-}
-
-export default DeploymentForm
\ No newline at end of file
diff --git a/frontend/src/components/settings/EditableTagGroup.jsx b/frontend/src/components/settings/EditableTagGroup.jsx
deleted file mode 100644
index 62740e6..0000000
--- a/frontend/src/components/settings/EditableTagGroup.jsx
+++ /dev/null
@@ -1,99 +0,0 @@
-import React from 'react';
-import { PlusOutlined } from '@ant-design/icons';
-import { Tag, Input, Tooltip } from 'antd';
-
-export default class EditableTagGroup extends React.Component {
- constructor(props){
- super(props)
- this.state = {
- tags: [],
- removedTags: [],
- inputVisible: false,
- inputValue: '',
- };
- }
-
- static getDerivedStateFromProps(nextProps, prevState){
- if(nextProps.value === undefined || nextProps.value === null){return null}
- let intersection = nextProps.value.filter(x => !prevState.removedTags.includes(x));
- console.log("NEW PROPS", nextProps.value, prevState.removedTags, intersection)
-
- return { tags: intersection }
-
- }
-
- handleClose = removedTag => {
- const tags = this.state.tags.filter(tag => tag !== removedTag);
- console.log("CLOS TAG", tags);
- this.setState({ tags,
- removedTags: this.state.removedTags.concat(removedTag)
- });
- this.props.onChange(tags)
- };
-
- showInput = () => {
- this.setState({ inputVisible: true }, () => this.input.focus());
- };
-
- handleInputChange = e => {
- this.setState({ inputValue: e.target.value });
-
- };
-
- handleInputConfirm = () => {
- const { inputValue } = this.state;
- let { tags } = this.state;
- if (inputValue && tags.indexOf(inputValue) === -1) {
- tags = [...tags, inputValue];
- }
- this.setState({
- tags,
- inputVisible: false,
- inputValue: '',
- });
- console.log("Received values of form",tags, this.state)
- this.props.onChange(tags)
- };
-
- saveInputRef = input => (this.input = input);
-
- render() {
- const { tags, inputVisible, inputValue } = this.state;
- return (
-
- {tags.map((tag, index) => {
- const isLongTag = tag.length > 20;
- const tagElem = (
-
this.handleClose(tag)}>
- {isLongTag ? `${tag.slice(0, 20)}...` : tag}
-
- );
- return isLongTag ? (
-
- {tagElem}
-
- ) : (
- tagElem
- );
- })}
- {inputVisible && (
-
- )}
- {!inputVisible && (
-
- New Tag
-
- )}
-
- );
- }
-}
\ No newline at end of file
diff --git a/frontend/src/components/settings/ProfileForm.jsx b/frontend/src/components/settings/ProfileForm.jsx
deleted file mode 100644
index 8ac47eb..0000000
--- a/frontend/src/components/settings/ProfileForm.jsx
+++ /dev/null
@@ -1,85 +0,0 @@
-import React, {useEffect} from 'react';
-import { Form } from "antd";
-import { Input, Divider, Button } from 'antd';
-import { useDispatch, useSelector } from 'react-redux';
-import { settingsActions } from '../../store/actions';
-
-const formItemLayout = {
- labelCol: {
- xs: { span: 24 },
- sm: { span: 5 },
- },
- wrapperCol: {
- xs: { span: 24 },
- sm: { span: 19 },
- },
-};
-const tailFormItemLayout = {
- wrapperCol: {
- xs: {
- span: 24,
- offset: 0,
- },
- sm: {
- span: 8,
- offset: 8,
- },
- },
-};
-
-const ProfileForm = () => {
-
- const settings = useSelector(state => state.SettingsReducer.profile);
- const dispatch = useDispatch();
-
- const [form] = Form.useForm();
- useEffect(() => {
- console.log("SETTINGS:", settings);
- form.setFieldsValue({
- ProfilePhoto: settings.ProfilePhoto,
- BackgroundPhoto: settings.BackgroundPhoto,
- Description: settings.Description,
- Footer: settings.Footer,
- Twitter: settings.Twitter,
- Instagram: settings.Instagram,
- Website: settings.Website,
- });
- }, [form, settings]);
-
-
-
-
- const handleSubmit = e => {
- form.validateFields().then(values => {
- console.log('Received values of form: ', values);
- dispatch(settingsActions.setProfile(values))
- });
- };
-
-
-
- return (
-
-
- );
- }
-export default ProfileForm;
diff --git a/frontend/src/components/settings/RegistrationForm.jsx b/frontend/src/components/settings/RegistrationForm.jsx
deleted file mode 100644
index aab0821..0000000
--- a/frontend/src/components/settings/RegistrationForm.jsx
+++ /dev/null
@@ -1,132 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { Form } from 'antd';
-import { Input, Divider, Button } from 'antd';
-import { useDispatch, useSelector } from 'react-redux';
-import { userActions } from '../../store/actions';
-
-const formItemLayout = {
- labelCol: {
- xs: { span: 24 },
- sm: { span: 5 },
- },
- wrapperCol: {
- xs: { span: 24 },
- sm: { span: 19 },
- },
-};
-const tailFormItemLayout = {
- wrapperCol: {
- xs: {
- span: 24,
- offset: 0,
- },
- sm: {
- span: 8,
- offset: 8,
- },
- },
-};
-
-
-const RegistrationForm = (props) => {
- const auth = useSelector(state => state.UserReducer);
- const dispatch = useDispatch();
-
- const [confirmDirty, setConfirmDirty] = useState(false)
- const [form] = Form.useForm();
-
- useEffect(() => {
- console.log("SETTINGS:", auth);
- form.setFieldsValue({
- username: auth.username,
- email: auth.email,
- });
- }, [form, auth]);
-
- const handleSubmit = () => {
- form.validateFields().then(values => {
- console.log('Received values of form: ', values);
- if (values.password !== undefined) {
- dispatch(userActions.update({
- username: values.username,
- email: values.email,
- password: values.password
- }))
- } else {
- dispatch(userActions.update({
- username: values.username,
- email: values.email
- }))
- }
- });
- };
-
- const handleConfirmBlur = e => {
- const { value } = e.target;
- setConfirmDirty(confirmDirty || !!value)
- };
-
-
-
-
- return (
-
-
-
-
-
-
- ({
- validator(_, value) {
- if (!value || getFieldValue('password') === value) {
- return Promise.resolve();
- }
- return Promise.reject(new Error('The two passwords that you entered do not match!'));
- },
- }),
- ]
- }>
-
-
-
-
- Save
-
-
-
- )
-}
-export default RegistrationForm
diff --git a/frontend/src/components/settings/SettingsForm.jsx b/frontend/src/components/settings/SettingsForm.jsx
deleted file mode 100644
index 0181373..0000000
--- a/frontend/src/components/settings/SettingsForm.jsx
+++ /dev/null
@@ -1,92 +0,0 @@
-import React, { useEffect } from 'react';
-import { Form, Switch } from 'antd';
-import { Input, Select, InputNumber, Divider, Button } from 'antd';
-import EditableTagGroup from './EditableTagGroup';
-
-import { useDispatch, useSelector } from 'react-redux';
-import { settingsActions } from '../../store/actions';
-
-const { Option } = Select;
-const formItemLayout = {
- labelCol: {
- xs: { span: 24 },
- sm: { span: 5 },
- },
- wrapperCol: {
- xs: { span: 24 },
- sm: { span: 19 },
- },
-};
-const tailFormItemLayout = {
- wrapperCol: {
- xs: {
- span: 24,
- offset: 0,
- },
- sm: {
- span: 8,
- offset: 8,
- },
- },
-};
-
-
-const SettingsForm = () => {
- const [form] = Form.useForm();
- const settings = useSelector(state => state.SettingsReducer.gallery);
- const dispatch = useDispatch();
-
- useEffect(() => {
- console.log("SETTINGS:",settings);
- form.setFieldsValue({
- Name: settings.Name,
- Basepath: settings.Basepath,
- Url: settings.Url,
- Theme: settings.Theme,
- Destpath: settings.Destpath,
- ImagesPerPage: settings.ImagesPerPage,
- PictureBlacklist: settings.PictureBlacklist || [],
- AlbumBlacklist: settings.AlbumBlacklist || [],
- Renderer: settings.Renderer,
- });
- }, [form,settings]);
-
- const handleSubmit = () => {
-
- form.validateFields().then(values => {
- console.log('Received values of form: ', values);
- dispatch(settingsActions.setGallery(values))
- });
- };
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Save
-
-
-
-
- )
-}
-
-export default SettingsForm
\ No newline at end of file
diff --git a/frontend/src/components/settings/TaskViewer.jsx b/frontend/src/components/settings/TaskViewer.jsx
deleted file mode 100644
index 3839f0d..0000000
--- a/frontend/src/components/settings/TaskViewer.jsx
+++ /dev/null
@@ -1,94 +0,0 @@
-import { Badge, Space, Table, Tag } from "antd"
-import axios from "axios";
-import React, { useEffect, useState } from 'react';
-import { config } from "../../store";
-import { notify } from "../../store/actions";
-const { Column, ColumnGroup } = Table;
-const test = [
- {
- "name": "albums",
- "done": true,
- "start": "2022-09-10T19:47:53.842094663+01:00",
- "end": "2022-09-10T19:47:54.092477954+01:00",
- "duration": 250383285
- },
- {
- "name": "images",
- "done": false,
- "start": "2022-09-10T19:47:58.417235735+01:00",
- "end": "2022-09-10T19:47:58.423342357+01:00",
- "duration": 6106621
- },
- {
- "name": "photos",
- "done": true,
- "start": "2022-09-10T19:47:54.106178396+01:00",
- "end": "2022-09-10T19:47:58.391295965+01:00",
- "duration": 4285117574
- }
- ];
-
-const formatDuration = ms => {
- if (ms < 0) ms = -ms;
- const time = {
- day: Math.floor(ms / 86400000),
- hour: Math.floor(ms / 3600000) % 24,
- minute: Math.floor(ms / 60000) % 60,
- second: Math.floor(ms / 1000) % 60,
- // millisecond: Math.floor(ms) % 1000
- };
- let msg = Object.entries(time)
- .filter(val => val[1] !== 0)
- .map(([key, val]) => `${val} ${key}${val !== 1 ? 's' : ''}`)
- .join(', ');
-
- if (msg === "") return "-"
- return msg
- };
-
-export const TaskViewer = () => {
- const [data, setData] = useState([])
-
- const getTasks = () => {
- axios.get(config.baseUrl+"/tasks").then((resp)=>{
- setData(resp.data)
- }).catch((err)=>{
- notify("warning", "Error from server: "+err)
- })
- }
-
-
- useEffect(() => {
- let interval = setInterval(() => {
- getTasks()
- }, 1000);
- return () => {
- clearInterval(interval);
- };
- }, []);
-
- const sortTasks = (tasks) => {
- tasks = tasks.filter(a => a.state!=="inactive")
- return tasks.sort((a,b) => {return new Date(Date.parse(b.start)) - new Date(Date.parse(a.start));});
- }
-
- return (
-
-
- {
- return state==="complete" ? <> Complete > : <> In progress >
- }}/>
- {
- let date = new Date(Date.parse(timeStr));
- return date.toLocaleString();
- }
- } />
- {
- return formatDuration(timeStr/1000000);
- }
- } />
-
-
-
- )
-}
\ No newline at end of file
diff --git a/frontend/src/components/settings/maintenance.jsx b/frontend/src/components/settings/maintenance.jsx
deleted file mode 100644
index 5864cb3..0000000
--- a/frontend/src/components/settings/maintenance.jsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import React, { useState } from 'react';
-import { DeleteOutlined, DownloadOutlined, SyncOutlined, UploadOutlined, BuildOutlined } from '@ant-design/icons';
-import { message, Row, Col, Divider, Upload, Button, Modal } from 'antd';
-import { useDispatch } from 'react-redux';
-import { taskActions } from '../../store/actions';
-import { config } from "../../store";
-import { TaskViewer } from './TaskViewer';
-
-
-const Maintenance = () => {
- const dispatch = useDispatch();
- const [fileList, setFileList] = useState([])
-
- const props = {
- name: 'file',
- action: config.baseUrl + "/tasks/upload",
- headers: { Authorization: localStorage.getItem('token') },
- className: "maintenance",
-
- onChange(info) {
- if (info.file.status !== 'uploading') {
- console.log(info.file, info.fileList);
- }
- if (info.file.status === 'done') {
- message.success(`${info.file.name} file uploaded successfully`);
- info.fileList = []
- } else if (info.file.status === 'error') {
- message.error(`${info.file.name} file upload failed.`);
- }
- let fileList = [...info.fileList];
- fileList = fileList.slice(-2);
- fileList = fileList.map(file => {
- if (file.response) {
- file.url = file.response.url;
- }
- return file;
- });
- setFileList(fileList);
- },
- };
-
- return (
- <>
-
-
- } size="large" style={{ "width": "100%" }} onClick={() => { dispatch(taskActions.rescan()) }}> Resacan image folder
-
-
- } size="large" style={{ "width": "100%" }} onClick={() => { dispatch(taskActions.purge()) }} > Delete Site
-
-
- } size="large" style={{ "width": "100%" }} onClick={() => dispatch(taskActions.backup())}> Backup Database
-
-
-
-
- Restore Database
-
-
-
-
- } size="large" style={{ "width": "100%" }} onClick={() => dispatch(taskActions.templateBuild())}> Build Site
-
-
- } size="large" style={{ "width": "100%" }} onClick={() => dispatch(taskActions.templateDeploy())}> Deploy Site
-
-
- Tasks
-
-
- >
- );
-}
-
-
-export default Maintenance;
\ No newline at end of file
diff --git a/frontend/src/components/sidebar.jsx b/frontend/src/components/sidebar.jsx
deleted file mode 100644
index af8ba6f..0000000
--- a/frontend/src/components/sidebar.jsx
+++ /dev/null
@@ -1,146 +0,0 @@
-import React, { useCallback, useEffect, useState } from 'react';
-import { AutoComplete, Form } from 'antd';
-// as an array
-import { Layout, Input, Select, TreeSelect, Typography } from 'antd';
-import { Collapse } from 'antd';
-
-import { useDispatch, useSelector } from 'react-redux';
-import { config } from '../store';
-import { getOptions, photoActions } from '../store/actions';
-import { LocationModal } from './Map'
-import axios from 'axios';
-
-const { Paragraph } = Typography;
-const { Panel } = Collapse;
-const { Sider } = Layout;
-const { Option } = Select;
-
-const SideBar = ({ photo }) => {
-
- const { collections } = useSelector(state => state.CollectionsReducer);
- const dispatch = useDispatch()
- const [form] = Form.useForm();
- const [data, setData] = useState({})
- const [captionOptions, setCaptionOptions] = useState([])
-
- const editPhoto = () => {
- let formData = form.getFieldValue()
- let newPhoto = { ...data, ...formData }
- if (JSON.stringify(newPhoto) !== JSON.stringify(data)) {
- dispatch(photoActions.edit(newPhoto))
- }
-
- }
-
- const getCaptionList = useCallback(() => {
- axios.get(`${config.baseUrl}/photo/${photo.id}/caption`, getOptions()).then(res => {
- console.log("CAPTIONS", res)
- if (res.data.status === "ok") {
- let options = res.data.predictions.map( prediction => { return {value: prediction.caption}})
- setCaptionOptions(options)
- }
- }).catch(err => console.log(err))
- }, [photo]);
-
- useEffect(() => {
- if (photo) {
- console.log("Photo Update")
- setData(photo)
- //getCaptionList()
- form.setFieldsValue(photo)
- } else {
- setData({})
- }
- }, [photo, form, getCaptionList])
-
- const updateGPS = (lat, lng) => {
- data.exif.GPS = {
- latitude: lat,
- longitude: lng
- }
- console.log("UPDATE GPS", data)
- dispatch(photoActions.edit(data))
- }
-
- const formItemLayout = {
- labelCol: {
- xs: { span: 24 },
- sm: { span: 8 },
- },
- wrapperCol: {
- xs: { span: 24 },
- sm: { span: 16 },
- },
- };
-
- const formatDate = (datestr) => {
- console.log("DATE:",datestr)
- const date = new Date(Date.parse(datestr));
- return date.toLocaleString();
- }
-
- return (
-
-
-
-
- );
-}
-export default SideBar;
diff --git a/frontend/src/components/upload.jsx b/frontend/src/components/upload.jsx
deleted file mode 100644
index 5610980..0000000
--- a/frontend/src/components/upload.jsx
+++ /dev/null
@@ -1,108 +0,0 @@
-import React from 'react';
-import { InboxOutlined } from '@ant-design/icons';
-import { Form } from 'antd';
-import { Modal, TreeSelect } from 'antd';
-import {config} from "../store";
-import axios from "axios"
-import {notify} from '../store/actions';
-import { useDispatch, useSelector } from 'react-redux';
-import {galleryActions, collectionActions, getOptions} from '../store/actions'
-import { useState } from 'react';
-
-import { Upload, message } from 'antd';
-const { Dragger } = Upload;
-
-const UploadCollection = () => {
- const {uploadModalVisable} = useSelector(state => state.GalleryReducer);
- const {collections} = useSelector(state => state.CollectionsReducer)
-
- const [upload, setEnableUpload] = useState(false)
- const [files, setFiles] = useState([])
-
- const [form] = Form.useForm();
- const dispatch = useDispatch()
-
- const enableUpload = () =>{
- setEnableUpload(true);
- }
-
- const customRequest = (options ) => {
- const data= new FormData()
- console.log("FILE UPLOAD", options);
- data.append('file', options.file)
- let config = getOptions();
- config.headers["content-type"] = 'multipart/form-data; boundary=----WebKitFormBoundaryqTqJIxvkWFYqvP5s';
-
- axios.post(options.action, data, config).then((res) => {
- options.onSuccess(res.data, options.file)
- message.success(`${options.file.name} file uploaded successfully.`);
- setFiles(files => [...files, options.file.name]);
- }).catch(() => {
- message.error(`${options.file.name} file upload failed.`);
- })
- }
- const onCancel = () => {
- dispatch(galleryActions.hideUpload())
- }
-
- const onCreate = () => {
- form.validateFields().then(values => {
- console.log('Received values of form: ', values, files);
- form.resetFields();
- dispatch(collectionActions.upload({
- album: values.select,
- photos: files
- }))
- dispatch(galleryActions.hideUpload())
-
- }).catch(() => {
- notify("warning", "Invaild data, could not upload")
- return;
- })
- }
-
- return (
-
-
-
-
-
-
-
-
- Click or drag file to this area to upload
-
- Support for a single or bulk upload. Strictly prohibit from uploading company data or other
- band files
-
-
-
-
-
- );
-}
-export default UploadCollection
-
diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx
deleted file mode 100644
index 45d8f67..0000000
--- a/frontend/src/main.jsx
+++ /dev/null
@@ -1,62 +0,0 @@
-// import 'antd/dist/antd.compact.min.css';
-// import 'antd/dist/antd.dark.min.css';
-// import 'antd/dist/reset.css';
-import React from 'react';
-import {createRoot} from 'react-dom/client';
-
-import { Provider } from 'react-redux';
-import { BrowserRouter } from 'react-router-dom';
-import store from './store'
-import {
- Routes,
- Route,
- Navigate
-} from 'react-router-dom'
-
-import Main from './pages/Main';
-import Settings from './pages/Settings'
-import { Button, ConfigProvider, Layout, theme } from 'antd';
-import Preview from './pages/Preview';
-
-const NoMatch = ({ location }) => (
-
-)
-
-const AppComponent = () => {
- return(
-
-
- )} />
- )} />
- )} />
-
-
-
- )
-}
-const container = document.getElementById('app_root');
-const root = createRoot(container);
-const { Header, Footer, Sider, Content } = Layout;
-root.render(
-
-
-
-
-
-);
\ No newline at end of file
diff --git a/frontend/src/pages/Main.css b/frontend/src/pages/Main.css
deleted file mode 100644
index 40ccc38..0000000
--- a/frontend/src/pages/Main.css
+++ /dev/null
@@ -1,245 +0,0 @@
-html, body {
- height: 100%;
- color: white;
- overflow: hidden;
-}
-.App {
- text-align: center;
- height: 100%;
-}
-#root{
- height: 100%;
-}
-
-.App-logo {
- height: 40vmin;
- pointer-events: none;
-}
-
-@media (prefers-reduced-motion: no-preference) {
- .App-logo {
- animation: App-logo-spin infinite 20s linear;
- }
-}
-
-* {
- animation-duration: 0s !important;
-}
-
-.App-header {
-
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- font-size: calc(10px + 2vmin);
- color: white;
-}
-
-.App-link {
- color: #61dafb;
-}
-
-@keyframes App-logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
-
-.item-container {
- cursor:crosshair;
-}
-.App {
- text-align: center;
-}
-
-.App-logo {
- animation: App-logo-spin infinite 20s linear;
- height: 40vmin;
- pointer-events: none;
-}
-
-.App-header {
- /* background-color: #282c34; */
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- font-size: calc(10px + 2vmin);
- color: white;
-}
-
-.App-link {
- color: #61dafb;
-}
-
-@keyframes App-logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
-
-.ant-modal-body {
- padding: 5px 24px 16px 24px;
-}
-
-.ant-modal-header {
- padding: 16px 24px 5px 24px;
-}
-
-.brand {
- float: left;
- font-size: 24px;
- color: white;
- border-right: 1px solid #282828;
- border-color: rgba(253, 253, 253, 0.12);
- width: 200px;
- text-align: center;
-}
-
-
-.brand:hover {
- color: rgba(255, 255, 255, 0.65);
-}
-
-.galleryImg {
- position: absolute;
- top:0;
- left:0;
- width:100%;
- height: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.ant-row.ant-form-item{
- margin-bottom: 10px;
-}
-
-.disable-select {
- user-select: none; /* supported by Chrome and Opera */
- -webkit-user-select: none; /* Safari */
- -khtml-user-select: none; /* Konqueror HTML */
- -moz-user-select: none; /* Firefox */
- -ms-user-select: none; /* Internet Explorer/Edge */
-}
-
-.item {
- cursor: pointer;
- user-select: none; /* supported by Chrome and Opera */
- -webkit-user-select: none; /* Safari */
- -khtml-user-select: none; /* Konqueror HTML */
- -moz-user-select: none; /* Firefox */
- -ms-user-select: none; /* Internet Explorer/Edge */
-
- width:100%;
- height:0;
- padding-bottom: calc(100%*4/6);
- position: relative;
-}
-/* Blur + Gray Scale */
-.item figure img {
- -webkit-filter: grayscale(0);
- filter: grayscale(0);
- -webkit-transition: .3s ease-in-out;
- transition: .3s ease-in-out;
- cursor: pointer;
-}
-.item figure:hover img {
- -webkit-filter: grayscale(100%);
- filter: grayscale(100%);
- cursor: pointer;
-
-}
-.item figure {
- border: 3px solid #000;
- margin: 0;
- text-align: center;
-}
-
-
-
-.ant-input-search .ant-input {
- border: 0px;
- height: 42px;
-
-}
-.ant-input-search .ant-input:focus {
- outline-color: transparent;
- outline-style: none;
- box-shadow: none;
-}
-
-.ant-input-search {
- font-size: larger;
- margin-top: 10px;
-}
-.ant-input-search .ant-input-suffix {
- left: 10px;
- right: auto;
-}
-
-
-
-/* .tick {
- -webkit-filter: grayscale(100%);
- filter: grayscale(100%);
-
-} */
-
-.maintenance .ant-upload {
- width: 100%;
-}
-
-.ant-menu-submenu ul {
- max-height: 500px;
- overflow-y: auto;
- overflow-x: hidden;
- margin-top: -5px;
- margin-right: -20px;
-}
-
-/* .ant-menu-submenu-popup::-webkit-scrollbar {
- display: none;
-}
-.ant-menu-submenu-popup{
- max-height: 500px;
- overflow-y: auto;
- padding-right: 10px;
-
- scrollbar-width: none;
-} */
-
-
-.menu-tree > .ant-tree {
- font-family: "Roboto", sans-serif;
- font-size: 14px;
- font-weight: 300;
- min-width:300px
-}
-
-.menu-tree .ant-tree-treenode {
- padding: 5px 10px;
-}
-
-
-.main {
- width: 100%;
-}
-
-::-webkit-scrollbar { width: 8px; height: 0px;}
-::-webkit-scrollbar-track { background-color: #646464;}
-::-webkit-scrollbar-track-piece { background-color: #000;}
-::-webkit-scrollbar-thumb { height: 0px; background-color: #666; border-radius: 0px;}
-::-webkit-scrollbar-corner { background-color: #646464;}
-::-webkit-resizer { background-color: #666;}
-
diff --git a/frontend/src/pages/Main.jsx b/frontend/src/pages/Main.jsx
deleted file mode 100644
index cf70978..0000000
--- a/frontend/src/pages/Main.jsx
+++ /dev/null
@@ -1,294 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import './Main.css';
-
-import {
- CalendarOutlined,
- CameraOutlined,
- ClockCircleOutlined,
- PictureOutlined,
- PlusOutlined,
- UploadOutlined,
-} from '@ant-design/icons';
-
-import { Layout, Menu, Pagination, Radio, Tree } from 'antd';
-import moment from 'moment';
-import { useDispatch, useSelector } from 'react-redux';
-import { collectionActions, photoActions } from '../store/actions';
-
-/*****************************/
-//import actions from "../store/actions";
-import SideBar from '../components/sidebar'
-import MoveModal from '../components/modal'
-import Header from '../components/header'
-import AddCollection from '../components/addCollection.jsx'
-import UploadPhotos from '../components/upload'
-import { galleryActions } from '../store/actions/gallery';
-import { IDFromTree } from '../store';
-import Gallery from '../components/Gallery';
-
-const { Content, Sider, Footer } = Layout;
-const { SubMenu } = Menu;
-const { DirectoryTree } = Tree;
-
-
-const Main = () => {
-
- const dispatch = useDispatch()
-
- const { photos } = useSelector(state => state.PhotoReducer)
- const { dates, collections } = useSelector(state => state.CollectionsReducer)
- const { imageSize } = useSelector(state => state.GalleryReducer)
-
- const [selectedElements, setSelectedElements] = useState([])
- const [collapsed, setCollapsed] = useState(true)
- const [selectedPhoto, setSelectedPhoto] = useState({})
- const [filter, setFilter] = useState("")
- const [uploaded_filter, setUploadedFilter] = useState("")
-
- const [page, setPage] = useState(1);
- const [pageSize, setPageSize] = useState(36);
-
-
- useEffect(() => {
- dispatch(photoActions.getAll());
- dispatch(collectionActions.getAll());
- }, [dispatch])
-
- const selectPhoto = (e, photo) => {
- e.stopPropagation();
- setSelectedPhoto(photos.findIndex(x => x.id === photo.id))
- setSelectedElements([photos.findIndex(x => x.id === photo.id)])
-
- console.log("Set Selected", selectedPhoto)
- }
-
- const clearSelection = () => {
- setSelectedPhoto({})
- console.log("Clear Selected", selectedPhoto)
- }
-
- const onCollapse = collapsed => {
- setCollapsed(collapsed)
- };
-
- const paginate = (items, page = 1, perPage = 10) => {
- const offset = perPage * (page - 1);
- const totalPages = Math.ceil(items.length / perPage);
- const paginatedItems = items.slice(offset, perPage * page);
-
- return {
- previousPage: page - 1 ? page - 1 : null,
- nextPage: (totalPages > page) ? page + 1 : null,
- total: items.length,
- totalPages: totalPages,
- items: paginatedItems
- };
- };
-
- const getStyle = (index) => {
- let elements = selectedElements.map(s => photos[s]);
- if (elements.find(e => e.id === index) !== undefined) {
- // Selected state
- return {
- border: '3px solid #2185d0',
- margin: 0,
- textAlign: "center"
- };
- }
- return {
- textAlign: "center"
- };
- };
-
- const handleSizeChange = (e) => {
- switch (e.target.value) {
- case "xsmall":
- setPageSize(120);
- break;
- case "small":
- setPageSize(36);
- break;
- case "medium":
- setPageSize(16);
- break;
- case "large":
- setPageSize(4);
- break;
- case "xlarge":
- setPageSize(1);
- break;
- }
- dispatch(galleryActions.changeImageSize(e.target.value))
- }
-
- const onTreeSelect = (selectedKeys, info) => {
- console.log('selected', selectedKeys, info); //.node.props.id);
- filterPhotos({
- key: selectedKeys[0]
- })
- };
-
- const filterPhotos = (item, datesList) => {
- setPage(1);
- switch (item.key) {
- case "all":
- setFilter("")
- setUploadedFilter("")
- break;
- case "add":
- setFilter("")
- setUploadedFilter("")
- break;
- case "upload":
- setFilter("")
- setUploadedFilter("")
- break;
- case "uploaded":
- setFilter(datesList[0])
- setUploadedFilter("")
- break;
- default:
- if (item.key !== undefined) {
- setFilter(item.key)
- setUploadedFilter("")
- } else {
- let name = IDFromTree(collections, item["0"])
- setFilter(name.id)
- setUploadedFilter("")
- }
- break;
- }
- }
- const lowercasedFilter = filter.toLowerCase();
-
- const filteredData = photos.filter(item => {
- return search(item, uploaded_filter, lowercasedFilter)
- });
-
- function formatDate(date) {
- let formattedDate = moment(date, "YYYY-MM-DD").format("DD-MM-YYYY");
- return formattedDate;
- }
-
- function search(item, uploaded_filter, lowercasedFilter) {
- if (uploaded_filter !== "") {
- return moment(item.meta["DateAdded"], "YYYY-MM-DD'T'HH:mm:SSZ").format("YYYY-MM-DD HH:mm") === uploaded_filter
- } else {
- if (item["name"].toLowerCase().includes(lowercasedFilter)) { return true }
- if (item["album"].toLowerCase() === (lowercasedFilter)) { return true }
- if (item.exif.date_taken.toLowerCase().includes(lowercasedFilter)) { return true }
- }
- return false
- }
-
- let selectMessage = filteredData.length + " photos"
- if (selectedElements.length > 0) {
- selectMessage = selectedElements.length + " out of " + filteredData.length + " selected"
- }
-
- let datesList = dates.sort((a, b) => {
- var dateA = new Date(a), dateB = new Date(b);
- return dateA - dateB;
- }).reverse();
-
-
- let results = paginate(filteredData, page, pageSize)
- return (
-
-
-
-
-
- filterPhotos(item, datesList)}>
- All Content
- Last Uploaded
-
-
- Date Captured
-
- }
- >
- {datesList.map((el, index) => ({formatDate(el)} ))}
-
-
-
- Collections
-
- }
- >
-
-
-
-
-
-
-
-
-
- dispatch(galleryActions.showAdd())} key="add" style={{ backgroundColor: "@popover-background", position: "absolute", bottom: 50 }}> Add Collection
- dispatch(galleryActions.showUpload())} key="upload" style={{ backgroundColor: "@popover-background", position: "absolute", bottom: 100 }}> Upload
-
-
-
-
-
-
-
-
-
- {selectedElements.length > 0 && }
-
- {
- setPage(page);
- }} />
-
-
- Tiny
- Small
- Medium
- Large
-
-
-
-
-
-
- );
-
-}
-
-export default Main;
\ No newline at end of file
diff --git a/frontend/src/pages/Preview.jsx b/frontend/src/pages/Preview.jsx
deleted file mode 100644
index c853695..0000000
--- a/frontend/src/pages/Preview.jsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import './Main.css';
-
-import { Layout } from 'antd';
-import { useDispatch } from 'react-redux';
-import Header from '../components/header'
-const { Content } = Layout;
-
-const Preview = () => {
- return (
-
-
-
-
-
-
-
- );
-
-}
-export default Preview;
\ No newline at end of file
diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx
deleted file mode 100644
index fdb081b..0000000
--- a/frontend/src/pages/Settings.jsx
+++ /dev/null
@@ -1,125 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import './Main.css';
-
-import {
- DeleteOutlined,
- FolderOpenOutlined,
- PictureOutlined,
- ProfileOutlined,
- SettingOutlined,
- RocketOutlined,
- CloudUploadOutlined,
-} from '@ant-design/icons';
-
-import { Layout, Statistic, Card, Row, Col } from 'antd';
-import { useDispatch, useSelector } from 'react-redux';
-import Header from '../components/header'
-import RegistrationForm from '../components/settings/RegistrationForm'
-import ProfileForm from '../components/settings/ProfileForm'
-import SettingsForm from '../components/settings/SettingsForm'
-import Maintenance from '../components/settings/maintenance'
-import AlbumSettings from '../components/settings/AlbumSettings'
-import { settingsActions } from '../store/actions/settings';
-import DeploymentForm from '../components/settings/DeploymentForm';
-
-const { Content } = Layout;
-const tabListNoTitle = [
- {
- key: 'profile',
- tab: Profile Settings ,
- },
- {
- key: 'settings',
- tab: Site Settings ,
- },
- {
- key: 'album',
- tab: Album Settings ,
- },
- {
- key: 'deployment',
- tab: Deployment Settings ,
- },
- {
- key: 'maintenance',
- tab: Tasks ,
- },
-];
-
-const contentListNoTitle = {
- profile: ,
- settings: ,
- album: ,
- deployment: ,
- maintenance:
-};
-
-const Settings = () => {
- const stats = useSelector(state => state.SettingsReducer.stats);
- const [tab, setTab] = useState({ key: 'tab1', noTitleKey: 'settings' });
- const dispatch = useDispatch()
-
- useEffect(() => {
- dispatch(settingsActions.all());
- }, [dispatch])
-
- const onTabChange = (key, type) => {
- console.log(key, type);
- setTab({ [type]: key });
- };
-
- return (
-
-
-
-
-
-
-
- }
- />
-
-
-
-
- }
- />
-
-
-
-
- }
- />
-
-
-
- {
- onTabChange(key, 'noTitleKey');
- }}
- >
- {contentListNoTitle[tab.noTitleKey]}
-
-
-
-
- );
-
-}
-export default Settings;
\ No newline at end of file
diff --git a/frontend/src/store/actions/collections.js b/frontend/src/store/actions/collections.js
deleted file mode 100644
index c48f3f9..0000000
--- a/frontend/src/store/actions/collections.js
+++ /dev/null
@@ -1,98 +0,0 @@
-
-import axios from 'axios';
-import {getOptions, notify} from './index';
-import {config} from '../index';
-
-import {photoActions} from './photos'
-export const collectionActions = {
- getAll,
- create,
- move,
- remove,
- upload,
- update
-};
-
-function getAll(){
- return dispatch => {
- dispatch(collectionUpdating());
- axios.get(config.baseUrl+"/collections", getOptions()).then((response)=>{
- console.log(response.data);
- dispatch(setPhotoDetails(response.data));
- }).catch((err)=>{
- notify("warning", "Error from server: "+err)
- })
- }
-};
-
-function update (collection){
- return dispatch => {
- axios.post(config.baseUrl + '/collection/'+collection.id, collection, getOptions()).then(result => {
- dispatch(getAll());
- notify("success", "Collections updated successfully")
- }).catch((err)=>{
- notify("warning", "Error from server: "+err)
- })
- }
-}
-
-function create (collection) {
- return dispatch => {
- axios.post(config.baseUrl + '/collection', collection, getOptions()).then(result => {
- dispatch(getAll());
- notify("success", "Collections created successfully")
- }).catch((err)=>{
- notify("warning", "Error from server: "+err)
- })
- }
-}
-
-function upload(collection) {
- return dispatch => {
- axios.post(config.baseUrl + '/collection/upload', collection, getOptions()).then(result => {
- dispatch(photoActions.getAll());
- notify("success", "Collections uploaded successfully")
- }).catch((err)=>{
- notify("warning", "Error from server: "+err)
- })
- }
-}
-
-function move (collection){
- return dispatch => {
- axios.post(config.baseUrl + '/collection/move', collection, getOptions()).then(result => {
- dispatch(photoActions.getAll());
- notify("success", "Photo deleted successfully")
- }).catch((err)=>{
- notify("warning", "Error from server: "+err)
- })
- }
-}
-
-function remove (photoID){
- return dispatch => {
- axios.delete(config.baseUrl + '/photo/'+photoID, getOptions()).then(result => {
- dispatch(photoActions.getAll());
- notify("success", "Collections moved successfully")
- }).catch((err)=>{
- notify("warning", "Error from server: "+err)
- })
- }
-}
-
-
-function collectionUpdating(){
- return{
- type: "COLLECTION_FETCHING"
- }
-}
-
-function setPhotoDetails(data){
- return{
- type: "COLLECTIONS_RECEIVED",
- collections: data.albums,
- dates: [...data.dates],
- uploadDates: [...data.uploadDates]
- }
-}
-
diff --git a/frontend/src/store/actions/gallery.js b/frontend/src/store/actions/gallery.js
deleted file mode 100644
index 3c2fa34..0000000
--- a/frontend/src/store/actions/gallery.js
+++ /dev/null
@@ -1,31 +0,0 @@
-
-export const galleryActions = {
- showAdd,
- hideAdd,
- showUpload,
- hideUpload,
- changeImageSize
-};
-
-function showAdd(){
- return{type: "SHOW_ADD_MODAL"}
-}
-
-function showUpload(){
- return{type: "SHOW_UPLOAD_MODAL"}
-}
-
-function hideAdd(){
- return{type: "HIDE_ADD_MODAL"}
-}
-
-function hideUpload(){
- return{type: "HIDE_UPLOAD_MODAL"}
-}
-
-function changeImageSize(size){
- const type = "CHANGE_IMAGE_SIZE"
- return{type: type, size: size}
-
-
-}
diff --git a/frontend/src/store/actions/index.js b/frontend/src/store/actions/index.js
deleted file mode 100644
index d0fe556..0000000
--- a/frontend/src/store/actions/index.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import { notification } from 'antd';
-
-export * from './user';
-export * from './photos';
-export * from './collections';
-export * from './gallery';
-export * from './settings';
-export * from './tasks';
-
-export function getOptions(){
- if(localStorage.getItem('token')){
- return{headers: {Authorization:localStorage.getItem('token')}}
- }
-}
-
-export function notify(type, description){
- let message = ""
- switch(type){
- case "warning": message = "Oh dear something went wong!"; break
- default: message = "Completed successfully" ; break
- }
-
- notification[type]({
- message: message,
- description: description,
- duration: 5,
- });
-}
\ No newline at end of file
diff --git a/frontend/src/store/actions/photos.js b/frontend/src/store/actions/photos.js
deleted file mode 100644
index a5325ea..0000000
--- a/frontend/src/store/actions/photos.js
+++ /dev/null
@@ -1,56 +0,0 @@
-
-import axios from 'axios';
-import {getOptions, notify} from './index';
-import {config} from '../index';
-import { logout } from './user';
-
-export const photoActions = {
- getAll,
- edit
-};
-
-function getAll(){
- return dispatch => {
- dispatch(photoUpdating());
- axios.get(config.baseUrl+"/photos", getOptions()).then((response)=>{
- console.log(response.data);
- dispatch(setPhotoDetails(response.data));
- }).catch((error)=>{
- console.log(error.response)
- if (error.response.status === 401 ){
- dispatch(logout());
- };
- notify("warning", "Error from server: "+error)
- })
- }
-
-}
-
-function edit(photo){
- return dispatch => {
- dispatch(photoUpdating());
- axios.post(config.baseUrl+"/photo/"+photo.id, photo, getOptions()).then((response)=>{
- notify("success", "Photo edited successfully")
- dispatch(getAll(response.data));
- }).catch((err)=>{
- if (err.response.status === 401 ){
- dispatch(logout());
- };
- notify("warning", "Error from server: "+err)
- })
- }
-}
-
-function photoUpdating(){
- return{
- type: "PHOTO_FETCHING"
- }
-}
-
-function setPhotoDetails(photos){
- return{
- type: "PHOTO_RECEIVED",
- photos: photos,
- }
-}
-
diff --git a/frontend/src/store/actions/settings.js b/frontend/src/store/actions/settings.js
deleted file mode 100644
index fc854bc..0000000
--- a/frontend/src/store/actions/settings.js
+++ /dev/null
@@ -1,115 +0,0 @@
-
-import axios from 'axios';
-import {config} from '../index';
-import {getOptions, notify} from './index';
-export const settingsActions = {
- stats,
- all,
- setProfile,
- setGallery,
- setDeploy
-};
-
-function all(){
- return dispatch => {
- dispatch(stats())
- dispatch(gallery())
- dispatch(profile())
- dispatch(deploy())
- }
-}
-function stats(){
- return dispatch => {
- axios.get(config.baseUrl+"/settings/stats",getOptions()).then((response)=>{
- dispatch(statsUpdated(response.data))
- }).catch((err)=>{
- notify("warning", "Error from server: "+err)
- })
- };
-}
-function profile(){
- return dispatch => {
- axios.get(config.baseUrl+"/settings/profile",getOptions()).then((response)=>{
- dispatch(profileUpdated(response.data))
- }).catch((err)=>{
- notify("warning", "Error from server: "+err)
- })
- };
-}
-function gallery(){
- return dispatch => {
- axios.get(config.baseUrl+"/settings/gallery",getOptions()).then((response)=>{
- dispatch(galleryUpdated(response.data))
- }).catch((err)=>{
- notify("warning", "Error from server: "+err)
- })
- };
-}
-function deploy(){
- return dispatch => {
- axios.get(config.baseUrl+"/settings/deploy",getOptions()).then((response)=>{
- dispatch(deployUpdated(response.data))
- }).catch((err)=>{
- notify("warning", "Error from server: "+err)
- })
- };
-}
-
-function setProfile(profile){
- return dispatch => {
- axios.post(config.baseUrl+"/settings/profile", profile ,getOptions()).then((response)=>{
- dispatch(profileUpdated(response.data))
- notify("success", "Profile edited successfully")
- }).catch((err)=>{
- notify("warning", "Error from server: "+err)
- })
- };
-}
-function setGallery(gallery){
- return dispatch => {
- axios.post(config.baseUrl+"/settings/gallery", gallery, getOptions()).then((response)=>{
- dispatch(galleryUpdated(response.data))
- notify("success", "Gallery edited successfully")
- }).catch((err)=>{
- notify("warning", "Error from server: "+err)
- })
- };
-}
-
-function setDeploy(deploy){
- return dispatch => {
- axios.post(config.baseUrl+"/settings/deploy", deploy, getOptions()).then((response)=>{
- dispatch(deployUpdated(response.data))
- notify("success", "Deploy edited successfully")
- }).catch((err)=>{
- notify("warning", "Error from server: "+err)
- })
- };
-}
-
-
-function statsUpdated(stats){
- return{
- type: "STATS_UPDATED",
- stats: stats
- }
-}
-function profileUpdated(profile){
- return{
- type: "PROFILE_UPDATED",
- profile: profile
- }
-}
-function galleryUpdated(gallery){
- return{
- type: "GALLERY_UPDATED",
- gallery: gallery
- }
-}
-function deployUpdated(deploy){
- return{
- type: "DEPLOY_UPDATED",
- deploy: deploy
- }
-}
-
diff --git a/frontend/src/store/actions/tasks.js b/frontend/src/store/actions/tasks.js
deleted file mode 100644
index 9e557b6..0000000
--- a/frontend/src/store/actions/tasks.js
+++ /dev/null
@@ -1,90 +0,0 @@
-
-import axios from 'axios';
-import {config} from '../index';
-import {getOptions, notify} from './index';
-
-function download(content, fileName, contentType) {
- var a = document.createElement("a");
- var file = new Blob([JSON.stringify(content,null,2)], {type: contentType});
- a.href = URL.createObjectURL(file);
- a.download = fileName;
- a.click();
-}
-
-function rescan(){
- return dispatch => {
- axios.get(config.baseUrl+"/tasks/rescan",getOptions()).then((response)=>{
- notify("success", "Rescan task started")
- }).catch((err)=>{
- notify("warning", "Error from server: "+err)
- })
- };
-}
-function purge(){
- return dispatch => {
- axios.get(config.baseUrl+"/tasks/purge",getOptions()).then((response)=>{
- notify("success", "Purge task started")
- }).catch((err)=>{
- notify("warning", "Error from server: "+err)
- })
- };
-}
-function clear(){
- return dispatch => {
- axios.get(config.baseUrl+"/tasks/clear",getOptions()).then((response)=>{
- notify("success", "Clear task started")
- }).catch((err)=>{
- notify("warning", "Error from server: "+err)
- })
- };
-}
-
-function backup(){
- return dispatch => {
- axios.get(config.baseUrl+"/tasks/backup",getOptions()).then((response)=>{
- download(response.data, 'galleryBackup.txt', 'text/plain');
- }).catch((err)=>{
- notify("warning", "Error from server: "+err)
- })
- };
-}
-
-function templateCacheClear(){
- return dispatch => {
- axios.get(config.baseUrl+"/tasks/clearTemplateCache",getOptions()).then(()=>{
- notify("success", "Template Cache Cleared")
- }).catch((err)=>{
- notify("warning", "Error from server: "+err)
- })
- }
-}
-
-function templateBuild(){
- return dispatch => {
- axios.post(config.baseUrl+"/tasks/build",getOptions()).then(()=>{
- notify("success", "Site Build Started")
- }).catch((err)=>{
- notify("warning", "Error from server: "+err)
- })
- }
-}
-
-function templateDeploy(){
- return dispatch => {
- axios.post(config.baseUrl+"/tasks/publish",getOptions()).then(()=>{
- notify("success", "Site Deploy Started")
- }).catch((err)=>{
- notify("warning", "Error from server: "+err)
- })
- }
-}
-
-export const taskActions = {
- purge,
- rescan,
- clear,
- backup,
- templateCacheClear,
- templateBuild,
- templateDeploy
-};
\ No newline at end of file
diff --git a/frontend/src/store/actions/user.js b/frontend/src/store/actions/user.js
deleted file mode 100644
index c1cbd9a..0000000
--- a/frontend/src/store/actions/user.js
+++ /dev/null
@@ -1,100 +0,0 @@
-
-import axios from 'axios';
-import { config} from '../index';
-import {getOptions, notify} from './index';
-export const userActions = {
- login,
- reauth,
- logout,
- update
-};
-
-function login(username, password, navigate){
- console.log('login: ', username);
- return dispatch => {
- let payload = {
- username: username,
- password: password
- }
- console.log('dispathc: ', username);
- axios.post(config.baseUrl+"/login", payload).then((response)=>{
- console.log(response.data);
- if (response.data.token) {
- localStorage.setItem('token', response.data.token);
- localStorage.setItem('email', response.data.email);
- localStorage.setItem('username', response.data.username);
- dispatch(setUserDetails(response.data));
- navigate('/');
- }else{
- dispatch(logout())
- }
- }).catch((err)=>{
- dispatch(logoutFailedUser())
- })
- };
-}
-function update(user){
- return dispatch => {
- axios.post(config.baseUrl+"/auth/update", user, getOptions()).then((response)=>{
- localStorage.setItem('email', response.data.email);
- localStorage.setItem('username', response.data.username);
- dispatch(setUserDetails(response.data));
- notify("success", "User details edited successfully")
-
- }).catch((err)=>{
- console.log("Error in response");
- console.log(err);
- })
- };
-}
-
-
-function reauth(){
- return dispatch => {
- axios.get(config.baseUrl+"/authorised",getOptions()).then((response)=>{
- if (response.data.token) {
- localStorage.setItem('token', response.data.token);
- localStorage.setItem('email', response.data.email);
- localStorage.setItem('username', response.data.username);
-
- }
- }).catch((err)=>{
- console.log("Error in response");
- console.log(err);
- dispatch(logout());
- })
- };
-}
-
-export function logout(){
- return dispatch => {
- localStorage.removeItem('email');
- localStorage.removeItem('token');
- localStorage.removeItem('username');
- dispatch(logoutUser());
- // history.push('/');
- }
-}
-
-export function setUserDetails(user){
- return{
- type: "LOGIN_SUCCESS",
- email: user.email,
- username: user.username,
- token: user.token
- }
-}
-
-export function logoutFailedUser(){
- return{
- type: "LOGOUT_FAILED",
- }
-}
-export function logoutUser(){
- return{
- type: "LOGOUT_SUCCESS",
- auth: false,
- email: '',
- token: ''
- }
-}
\ No newline at end of file
diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js
deleted file mode 100644
index d36d56b..0000000
--- a/frontend/src/store/index.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import { createStore, applyMiddleware } from 'redux'
-import rootReducer from './reducers'
-import thunkMiddleware from 'redux-thunk'
-
-const Constants = {
- prod : {
- baseUrl: "http://localhost:8800/api/admin",
- imageUrl: "http://localhost:8800/img/"
- },
- dev : {
- baseUrl: "http://localhost:8800/api/admin",
- imageUrl: "http://localhost:8800/img/"
- }
-}
-export const config = {
- baseUrl: "http://localhost:8800/api/admin",
- imageUrl: "http://localhost:8800/img/"
- }
-
-
-export default function configureStore(preloadedState) {
- return createStore(
- rootReducer,
- preloadedState,
- applyMiddleware(thunkMiddleware)
- )
- }
-
-
-export function IDFromTree(collections, key){
- key = key.split("-")
- key.shift()
- let el = {children:collections}
- key.forEach(k => {
- el = el.children[parseInt(k)]
- })
- return el
-}
\ No newline at end of file
diff --git a/frontend/src/store/reducers/collections.js b/frontend/src/store/reducers/collections.js
deleted file mode 100644
index b3b1fd1..0000000
--- a/frontend/src/store/reducers/collections.js
+++ /dev/null
@@ -1,43 +0,0 @@
-const initialState = {
- collections: [],
- dates: [],
- uploadDates: [],
- isUpdating: false
-};
-
-const convertToTree = (tree) => {
- const proceesNode = (node) => {
- node.key = node.id
- node.value = node.id
- node.title = node.name
- node.children = Object.values(node.children)
- if (node.children.length === 0 || node.children === undefined ) {
- node["key"] = node.id
- return node
- }
- node.children = node.children.map(n => proceesNode(n))
- return node
- }
- tree = Object.values(tree)
- return tree.map(node => proceesNode(node))
-}
-
-export function CollectionsReducer(state = initialState, action) {
- switch (action.type) {
- case 'COLLECTIONS_FETCHING':
- return {
- ...state,
- isUpdating: true
- };
- case 'COLLECTIONS_RECEIVED':
- return {
- ...state,
- isUpdating: false,
- collections: convertToTree(action.collections),
- dates: action.dates,
- uploadDates: action.uploadDates
- };
- default:
- return state
- }
- }
\ No newline at end of file
diff --git a/frontend/src/store/reducers/gallery.js b/frontend/src/store/reducers/gallery.js
deleted file mode 100644
index 502b9ce..0000000
--- a/frontend/src/store/reducers/gallery.js
+++ /dev/null
@@ -1,23 +0,0 @@
-
-const initialState = {
- addCollectionModalVisable: false,
- uploadModalVisable: false,
- imageSize: "small"
-};
-
-export function GalleryReducer(state = initialState, action) {
- switch (action.type) {
- case 'SHOW_ADD_MODAL':
- return {...state, addCollectionModalVisable: true };
- case 'HIDE_ADD_MODAL':
- return {...state, addCollectionModalVisable: false };
- case 'SHOW_UPLOAD_MODAL':
- return {...state, uploadModalVisable: true };
- case 'HIDE_UPLOAD_MODAL':
- return {...state, uploadModalVisable: false };
- case 'CHANGE_IMAGE_SIZE':
- return {...state, imageSize: action.size};
- default:
- return state
- }
- }
\ No newline at end of file
diff --git a/frontend/src/store/reducers/index.js b/frontend/src/store/reducers/index.js
deleted file mode 100644
index 28df0dc..0000000
--- a/frontend/src/store/reducers/index.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import { combineReducers } from 'redux'
-
-import {UserReducer} from './user'
-import {PhotoReducer} from './photos'
-import {CollectionsReducer} from './collections'
-import {GalleryReducer} from './gallery'
-import {SettingsReducer} from './settings'
-
-export default combineReducers({
- UserReducer,
- CollectionsReducer,
- PhotoReducer,
- GalleryReducer,
- SettingsReducer
- })
\ No newline at end of file
diff --git a/frontend/src/store/reducers/photos.js b/frontend/src/store/reducers/photos.js
deleted file mode 100644
index 033555e..0000000
--- a/frontend/src/store/reducers/photos.js
+++ /dev/null
@@ -1,23 +0,0 @@
-
-const initialState = {
- photos: [],
- isUpdating: false
-};
-
-export function PhotoReducer(state = initialState, action) {
- switch (action.type) {
- case 'PHOTO_FETCHING':
- return {
- ...state,
- isUpdating: true
- };
- case 'PHOTO_RECEIVED':
- return {
- ...state,
- isUpdating: false,
- photos: action.photos
- };
- default:
- return state
- }
- }
\ No newline at end of file
diff --git a/frontend/src/store/reducers/settings.js b/frontend/src/store/reducers/settings.js
deleted file mode 100644
index 3f29fd5..0000000
--- a/frontend/src/store/reducers/settings.js
+++ /dev/null
@@ -1,44 +0,0 @@
-
-const initialState = {
- stats: {},
- profile:{},
- gallery:{},
- deploy:{},
- isUpdating: false
-};
-
-export function SettingsReducer(state = initialState, action) {
- switch (action.type) {
- case 'STATS_FETCHING':
- return {
- ...state,
- isUpdating: true
- };
- case 'STATS_UPDATED':
- return {
- ...state,
- isUpdating: false,
- stats: action.stats
- };
- case 'PROFILE_UPDATED':
- return {
- ...state,
- isUpdating: false,
- profile: action.profile
- }
- case 'GALLERY_UPDATED':
- return {
- ...state,
- isUpdating: false,
- gallery: action.gallery
- };
- case 'DEPLOY_UPDATED':
- return {
- ...state,
- isUpdating: false,
- deploy: action.deploy
- };
- default:
- return state
- }
-}
\ No newline at end of file
diff --git a/frontend/src/store/reducers/user.js b/frontend/src/store/reducers/user.js
deleted file mode 100644
index 24276cb..0000000
--- a/frontend/src/store/reducers/user.js
+++ /dev/null
@@ -1,35 +0,0 @@
-
-const initialState = {
- loggedIn: true,
- loginFailed: false,
- token: localStorage.getItem('token') || "",
- email: localStorage.getItem('email') || "",
- username: localStorage.getItem('username') || ""
-};
-
-export function UserReducer(state = initialState, action) {
- switch (action.type) {
- case 'LOGIN_SUCCESS':
- console.log("UDPATING USER SETTINGS")
- return {
- loggingIn: true,
- token: action.token,
- username: action.username,
- email: action.email,
- loginFailed: false,
- auth: true
- };
- case 'LOGOUT_SUCCESS':
- return {
- auth: false,
- loginFailed: false
- };
- case 'LOGOUT_FAILED':
- return {
- auth: false,
- loginFailed: true
- };
- default:
- return state
- }
-}
\ No newline at end of file
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
deleted file mode 100644
index 4955065..0000000
--- a/frontend/vite.config.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import {defineConfig} from 'vite'
-import react from '@vitejs/plugin-react'
-
-// https://vitejs.dev/config/
-export default defineConfig({
- plugins: [react()]
-})
diff --git a/frontend/wailsjs/runtime/package.json b/frontend/wailsjs/runtime/package.json
deleted file mode 100644
index 1e7c8a5..0000000
--- a/frontend/wailsjs/runtime/package.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "name": "@wailsapp/runtime",
- "version": "2.0.0",
- "description": "Wails Javascript runtime library",
- "main": "runtime.js",
- "types": "runtime.d.ts",
- "scripts": {
- },
- "repository": {
- "type": "git",
- "url": "git+https://github.com/wailsapp/wails.git"
- },
- "keywords": [
- "Wails",
- "Javascript",
- "Go"
- ],
- "author": "Lea Anthony ",
- "license": "MIT",
- "bugs": {
- "url": "https://github.com/wailsapp/wails/issues"
- },
- "homepage": "https://github.com/wailsapp/wails#readme"
-}
diff --git a/frontend/wailsjs/runtime/runtime.d.ts b/frontend/wailsjs/runtime/runtime.d.ts
deleted file mode 100644
index 94778df..0000000
--- a/frontend/wailsjs/runtime/runtime.d.ts
+++ /dev/null
@@ -1,249 +0,0 @@
-/*
- _ __ _ __
-| | / /___ _(_) /____
-| | /| / / __ `/ / / ___/
-| |/ |/ / /_/ / / (__ )
-|__/|__/\__,_/_/_/____/
-The electron alternative for Go
-(c) Lea Anthony 2019-present
-*/
-
-export interface Position {
- x: number;
- y: number;
-}
-
-export interface Size {
- w: number;
- h: number;
-}
-
-export interface Screen {
- isCurrent: boolean;
- isPrimary: boolean;
- width : number
- height : number
-}
-
-// Environment information such as platform, buildtype, ...
-export interface EnvironmentInfo {
- buildType: string;
- platform: string;
- arch: string;
-}
-
-// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
-// emits the given event. Optional data may be passed with the event.
-// This will trigger any event listeners.
-export function EventsEmit(eventName: string, ...data: any): void;
-
-// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
-export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
-
-// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
-// sets up a listener for the given event name, but will only trigger a given number times.
-export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
-
-// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
-// sets up a listener for the given event name, but will only trigger once.
-export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
-
-// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
-// unregisters the listener for the given event name.
-export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
-
-// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
-// unregisters all listeners.
-export function EventsOffAll(): void;
-
-// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
-// logs the given message as a raw message
-export function LogPrint(message: string): void;
-
-// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
-// logs the given message at the `trace` log level.
-export function LogTrace(message: string): void;
-
-// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
-// logs the given message at the `debug` log level.
-export function LogDebug(message: string): void;
-
-// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
-// logs the given message at the `error` log level.
-export function LogError(message: string): void;
-
-// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
-// logs the given message at the `fatal` log level.
-// The application will quit after calling this method.
-export function LogFatal(message: string): void;
-
-// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
-// logs the given message at the `info` log level.
-export function LogInfo(message: string): void;
-
-// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
-// logs the given message at the `warning` log level.
-export function LogWarning(message: string): void;
-
-// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
-// Forces a reload by the main application as well as connected browsers.
-export function WindowReload(): void;
-
-// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
-// Reloads the application frontend.
-export function WindowReloadApp(): void;
-
-// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
-// Sets the window AlwaysOnTop or not on top.
-export function WindowSetAlwaysOnTop(b: boolean): void;
-
-// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
-// *Windows only*
-// Sets window theme to system default (dark/light).
-export function WindowSetSystemDefaultTheme(): void;
-
-// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
-// *Windows only*
-// Sets window to light theme.
-export function WindowSetLightTheme(): void;
-
-// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
-// *Windows only*
-// Sets window to dark theme.
-export function WindowSetDarkTheme(): void;
-
-// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
-// Centers the window on the monitor the window is currently on.
-export function WindowCenter(): void;
-
-// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
-// Sets the text in the window title bar.
-export function WindowSetTitle(title: string): void;
-
-// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
-// Makes the window full screen.
-export function WindowFullscreen(): void;
-
-// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
-// Restores the previous window dimensions and position prior to full screen.
-export function WindowUnfullscreen(): void;
-
-// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
-// Returns the state of the window, i.e. whether the window is in full screen mode or not.
-export function WindowIsFullscreen(): Promise;
-
-// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
-// Sets the width and height of the window.
-export function WindowSetSize(width: number, height: number): Promise;
-
-// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
-// Gets the width and height of the window.
-export function WindowGetSize(): Promise;
-
-// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
-// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
-// Setting a size of 0,0 will disable this constraint.
-export function WindowSetMaxSize(width: number, height: number): void;
-
-// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
-// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
-// Setting a size of 0,0 will disable this constraint.
-export function WindowSetMinSize(width: number, height: number): void;
-
-// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
-// Sets the window position relative to the monitor the window is currently on.
-export function WindowSetPosition(x: number, y: number): void;
-
-// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
-// Gets the window position relative to the monitor the window is currently on.
-export function WindowGetPosition(): Promise;
-
-// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
-// Hides the window.
-export function WindowHide(): void;
-
-// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
-// Shows the window, if it is currently hidden.
-export function WindowShow(): void;
-
-// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
-// Maximises the window to fill the screen.
-export function WindowMaximise(): void;
-
-// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
-// Toggles between Maximised and UnMaximised.
-export function WindowToggleMaximise(): void;
-
-// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
-// Restores the window to the dimensions and position prior to maximising.
-export function WindowUnmaximise(): void;
-
-// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
-// Returns the state of the window, i.e. whether the window is maximised or not.
-export function WindowIsMaximised(): Promise;
-
-// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
-// Minimises the window.
-export function WindowMinimise(): void;
-
-// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
-// Restores the window to the dimensions and position prior to minimising.
-export function WindowUnminimise(): void;
-
-// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
-// Returns the state of the window, i.e. whether the window is minimised or not.
-export function WindowIsMinimised(): Promise;
-
-// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
-// Returns the state of the window, i.e. whether the window is normal or not.
-export function WindowIsNormal(): Promise;
-
-// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
-// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
-export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
-
-// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
-// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
-export function ScreenGetAll(): Promise;
-
-// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
-// Opens the given URL in the system browser.
-export function BrowserOpenURL(url: string): void;
-
-// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
-// Returns information about the environment
-export function Environment(): Promise;
-
-// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
-// Quits the application.
-export function Quit(): void;
-
-// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
-// Hides the application.
-export function Hide(): void;
-
-// [Show](https://wails.io/docs/reference/runtime/intro#show)
-// Shows the application.
-export function Show(): void;
-
-// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
-// Returns the current text stored on clipboard
-export function ClipboardGetText(): Promise;
-
-// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
-// Sets a text on the clipboard
-export function ClipboardSetText(text: string): Promise;
-
-// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
-// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
-export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
-
-// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
-// OnFileDropOff removes the drag and drop listeners and handlers.
-export function OnFileDropOff() :void
-
-// Check if the file path resolver is available
-export function CanResolveFilePaths(): boolean;
-
-// Resolves file paths for an array of files
-export function ResolveFilePaths(files: File[]): void
\ No newline at end of file
diff --git a/frontend/wailsjs/runtime/runtime.js b/frontend/wailsjs/runtime/runtime.js
deleted file mode 100644
index 623397b..0000000
--- a/frontend/wailsjs/runtime/runtime.js
+++ /dev/null
@@ -1,238 +0,0 @@
-/*
- _ __ _ __
-| | / /___ _(_) /____
-| | /| / / __ `/ / / ___/
-| |/ |/ / /_/ / / (__ )
-|__/|__/\__,_/_/_/____/
-The electron alternative for Go
-(c) Lea Anthony 2019-present
-*/
-
-export function LogPrint(message) {
- window.runtime.LogPrint(message);
-}
-
-export function LogTrace(message) {
- window.runtime.LogTrace(message);
-}
-
-export function LogDebug(message) {
- window.runtime.LogDebug(message);
-}
-
-export function LogInfo(message) {
- window.runtime.LogInfo(message);
-}
-
-export function LogWarning(message) {
- window.runtime.LogWarning(message);
-}
-
-export function LogError(message) {
- window.runtime.LogError(message);
-}
-
-export function LogFatal(message) {
- window.runtime.LogFatal(message);
-}
-
-export function EventsOnMultiple(eventName, callback, maxCallbacks) {
- return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
-}
-
-export function EventsOn(eventName, callback) {
- return EventsOnMultiple(eventName, callback, -1);
-}
-
-export function EventsOff(eventName, ...additionalEventNames) {
- return window.runtime.EventsOff(eventName, ...additionalEventNames);
-}
-
-export function EventsOnce(eventName, callback) {
- return EventsOnMultiple(eventName, callback, 1);
-}
-
-export function EventsEmit(eventName) {
- let args = [eventName].slice.call(arguments);
- return window.runtime.EventsEmit.apply(null, args);
-}
-
-export function WindowReload() {
- window.runtime.WindowReload();
-}
-
-export function WindowReloadApp() {
- window.runtime.WindowReloadApp();
-}
-
-export function WindowSetAlwaysOnTop(b) {
- window.runtime.WindowSetAlwaysOnTop(b);
-}
-
-export function WindowSetSystemDefaultTheme() {
- window.runtime.WindowSetSystemDefaultTheme();
-}
-
-export function WindowSetLightTheme() {
- window.runtime.WindowSetLightTheme();
-}
-
-export function WindowSetDarkTheme() {
- window.runtime.WindowSetDarkTheme();
-}
-
-export function WindowCenter() {
- window.runtime.WindowCenter();
-}
-
-export function WindowSetTitle(title) {
- window.runtime.WindowSetTitle(title);
-}
-
-export function WindowFullscreen() {
- window.runtime.WindowFullscreen();
-}
-
-export function WindowUnfullscreen() {
- window.runtime.WindowUnfullscreen();
-}
-
-export function WindowIsFullscreen() {
- return window.runtime.WindowIsFullscreen();
-}
-
-export function WindowGetSize() {
- return window.runtime.WindowGetSize();
-}
-
-export function WindowSetSize(width, height) {
- window.runtime.WindowSetSize(width, height);
-}
-
-export function WindowSetMaxSize(width, height) {
- window.runtime.WindowSetMaxSize(width, height);
-}
-
-export function WindowSetMinSize(width, height) {
- window.runtime.WindowSetMinSize(width, height);
-}
-
-export function WindowSetPosition(x, y) {
- window.runtime.WindowSetPosition(x, y);
-}
-
-export function WindowGetPosition() {
- return window.runtime.WindowGetPosition();
-}
-
-export function WindowHide() {
- window.runtime.WindowHide();
-}
-
-export function WindowShow() {
- window.runtime.WindowShow();
-}
-
-export function WindowMaximise() {
- window.runtime.WindowMaximise();
-}
-
-export function WindowToggleMaximise() {
- window.runtime.WindowToggleMaximise();
-}
-
-export function WindowUnmaximise() {
- window.runtime.WindowUnmaximise();
-}
-
-export function WindowIsMaximised() {
- return window.runtime.WindowIsMaximised();
-}
-
-export function WindowMinimise() {
- window.runtime.WindowMinimise();
-}
-
-export function WindowUnminimise() {
- window.runtime.WindowUnminimise();
-}
-
-export function WindowSetBackgroundColour(R, G, B, A) {
- window.runtime.WindowSetBackgroundColour(R, G, B, A);
-}
-
-export function ScreenGetAll() {
- return window.runtime.ScreenGetAll();
-}
-
-export function WindowIsMinimised() {
- return window.runtime.WindowIsMinimised();
-}
-
-export function WindowIsNormal() {
- return window.runtime.WindowIsNormal();
-}
-
-export function BrowserOpenURL(url) {
- window.runtime.BrowserOpenURL(url);
-}
-
-export function Environment() {
- return window.runtime.Environment();
-}
-
-export function Quit() {
- window.runtime.Quit();
-}
-
-export function Hide() {
- window.runtime.Hide();
-}
-
-export function Show() {
- window.runtime.Show();
-}
-
-export function ClipboardGetText() {
- return window.runtime.ClipboardGetText();
-}
-
-export function ClipboardSetText(text) {
- return window.runtime.ClipboardSetText(text);
-}
-
-/**
- * Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
- *
- * @export
- * @callback OnFileDropCallback
- * @param {number} x - x coordinate of the drop
- * @param {number} y - y coordinate of the drop
- * @param {string[]} paths - A list of file paths.
- */
-
-/**
- * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
- *
- * @export
- * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
- * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
- */
-export function OnFileDrop(callback, useDropTarget) {
- return window.runtime.OnFileDrop(callback, useDropTarget);
-}
-
-/**
- * OnFileDropOff removes the drag and drop listeners and handlers.
- */
-export function OnFileDropOff() {
- return window.runtime.OnFileDropOff();
-}
-
-export function CanResolveFilePaths() {
- return window.runtime.CanResolveFilePaths();
-}
-
-export function ResolveFilePaths(files) {
- return window.runtime.ResolveFilePaths(files);
-}
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 9019525..bc2bdb3 100644
--- a/go.mod
+++ b/go.mod
@@ -1,123 +1,140 @@
-module github.com/robrotheram/gogallery
+module gogallery
-go 1.22.0
+go 1.24.2
-toolchain go1.23.2
+require (
+ fyne.io/fyne/v2 v2.6.1
+ github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
+ github.com/dsoprea/go-exif/v3 v3.0.0-20210428042052-dca55bf8ca15
+ gorm.io/gorm v1.30.0
+)
require (
- github.com/asdine/storm v2.1.2+incompatible
- github.com/bep/gowebp v0.4.0
- github.com/disintegration/imaging v1.6.2
- github.com/dsoprea/go-exif/v3 v3.0.1
- github.com/evanoberholster/imagemeta v0.3.1
- github.com/go-openapi/runtime v0.28.0
- github.com/go-openapi/strfmt v0.23.0
- github.com/gorilla/handlers v1.5.2
- github.com/gorilla/mux v1.8.1
- github.com/gosimple/slug v1.14.0
- github.com/gosuri/uiprogress v0.0.1
- github.com/k0kubun/pp/v3 v3.3.0
- github.com/manifoldco/promptui v0.9.0
- github.com/mitchellh/go-homedir v1.1.0
- github.com/netlify/open-api/v2 v2.34.0
- github.com/sirupsen/logrus v1.9.3
- github.com/spf13/cobra v1.8.1
- github.com/spf13/viper v1.19.0
- github.com/tdewolff/minify/v2 v2.21.2
- github.com/wailsapp/wails/v2 v2.9.2
- golang.org/x/image v0.22.0
- golang.org/x/net v0.31.0
+ cloud.google.com/go v0.116.0 // indirect
+ cloud.google.com/go/auth v0.13.0 // indirect
+ cloud.google.com/go/compute/metadata v0.6.0 // indirect
+ github.com/felixge/httpsnoop v1.0.4 // indirect
+ github.com/google/go-cmp v0.6.0 // indirect
+ github.com/google/s2a-go v0.1.8 // indirect
+ github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
+ github.com/googleapis/gax-go/v2 v2.14.1 // indirect
+ github.com/gorilla/websocket v1.5.3 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
+ golang.org/x/crypto v0.33.0 // indirect
+ google.golang.org/genai v1.14.0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
+ google.golang.org/grpc v1.67.3 // indirect
+ google.golang.org/protobuf v1.36.1 // indirect
)
require (
- github.com/Azure/go-autorest v14.2.0+incompatible // indirect
- github.com/Azure/go-autorest/autorest v0.11.29 // indirect
- github.com/Azure/go-autorest/autorest/adal v0.9.24 // indirect
- github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
- github.com/Azure/go-autorest/logger v0.2.1 // indirect
- github.com/Azure/go-autorest/tracing v0.6.0 // indirect
- github.com/DataDog/zstd v1.5.2 // indirect
- github.com/Sereal/Sereal v0.0.0-20220903133728-b4d312952c4c // indirect
+ github.com/Azure/go-autorest/autorest v0.10.1 // indirect
+ github.com/Azure/go-autorest/autorest/adal v0.8.2 // indirect
+ github.com/Azure/go-autorest/autorest/date v0.2.0 // indirect
+ github.com/Azure/go-autorest/logger v0.1.0 // indirect
+ github.com/Azure/go-autorest/tracing v0.5.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
- github.com/bep/debounce v1.2.1 // indirect
- github.com/cenkalti/backoff/v4 v4.3.0 // indirect
- github.com/chzyer/readline v1.5.1 // indirect
- github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
+ github.com/cenkalti/backoff/v4 v4.0.2 // indirect
+ github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
+ github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
+ github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb // indirect
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
- github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 // indirect
- github.com/felixge/httpsnoop v1.0.4 // indirect
- github.com/fsnotify/fsnotify v1.8.0 // indirect
- github.com/go-errors/errors v1.5.1 // indirect
+ github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c // indirect
+ github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e // indirect
+ github.com/go-errors/errors v1.1.1 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
- github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-openapi/analysis v0.23.0 // indirect
github.com/go-openapi/errors v0.22.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/loads v0.22.0 // indirect
+ github.com/go-openapi/runtime v0.28.0
github.com/go-openapi/spec v0.21.0 // indirect
+ github.com/go-openapi/strfmt v0.23.0
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-openapi/validate v0.24.0 // indirect
- github.com/godbus/dbus/v5 v5.1.0 // indirect
- github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
- github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
+ github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
+ github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b // indirect
+ github.com/golang/geo v0.0.0-20200319012246-673a6f80352d // indirect
github.com/google/uuid v1.6.0 // indirect
+ github.com/gorilla/mux v1.8.1
github.com/gosimple/unidecode v1.0.1 // indirect
github.com/gosuri/uilive v0.0.4 // indirect
- github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
- github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
+ github.com/jinzhu/inflection v1.0.0 // indirect
+ github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
- github.com/labstack/echo/v4 v4.12.0 // indirect
- github.com/labstack/gommon v0.4.2 // indirect
- github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
- github.com/leaanthony/gosod v1.0.4 // indirect
- github.com/leaanthony/slicer v1.6.0 // indirect
- github.com/leaanthony/u v1.1.1 // indirect
- github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
- github.com/mattn/go-colorable v0.1.13 // indirect
- github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-isatty v0.0.3 // indirect
+ github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
- github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
- github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
- github.com/rivo/uniseg v0.4.7 // indirect
- github.com/rs/zerolog v1.33.0 // indirect
github.com/rsc/goversion v1.2.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
- github.com/sagikazarmark/locafero v0.6.0 // indirect
- github.com/sagikazarmark/slog-shim v0.1.0 // indirect
- github.com/samber/lo v1.47.0 // indirect
+ github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
- github.com/spf13/afero v1.11.0 // indirect
- github.com/spf13/cast v1.7.0 // indirect
- github.com/spf13/pflag v1.0.5 // indirect
+ github.com/spf13/afero v1.12.0 // indirect
+ github.com/spf13/cast v1.7.1 // indirect
+ github.com/spf13/pflag v1.0.6 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
- github.com/tdewolff/parse/v2 v2.7.19 // indirect
- github.com/tinylib/msgp v1.2.4 // indirect
- github.com/tkrajina/go-reflector v0.5.8 // indirect
- github.com/valyala/bytebufferpool v1.0.0 // indirect
- github.com/valyala/fasttemplate v1.2.2 // indirect
- github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
- github.com/wailsapp/go-webview2 v1.0.17 // indirect
- github.com/wailsapp/mimetype v1.4.1 // indirect
- go.etcd.io/bbolt v1.3.11 // indirect
- go.mongodb.org/mongo-driver v1.17.1 // indirect
- go.opentelemetry.io/otel v1.32.0 // indirect
- go.opentelemetry.io/otel/metric v1.32.0 // indirect
- go.opentelemetry.io/otel/trace v1.32.0 // indirect
- go.uber.org/multierr v1.11.0 // indirect
- golang.org/x/crypto v0.29.0 // indirect
- golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect
- golang.org/x/sync v0.9.0 // indirect
- golang.org/x/sys v0.27.0 // indirect
- golang.org/x/text v0.20.0 // indirect
- gopkg.in/ini.v1 v1.67.0 // indirect
+ github.com/tdewolff/parse/v2 v2.8.1 // indirect
+ go.mongodb.org/mongo-driver v1.14.0 // indirect
+ go.opentelemetry.io/otel v1.29.0 // indirect
+ go.opentelemetry.io/otel/metric v1.29.0 // indirect
+ go.opentelemetry.io/otel/trace v1.29.0 // indirect
+ go.uber.org/atomic v1.9.0 // indirect
+ go.uber.org/multierr v1.9.0 // indirect
+ golang.org/x/sync v0.11.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
+)
+
+require (
+ fyne.io/systray v1.11.0 // indirect
+ github.com/BurntSushi/toml v1.4.0 // indirect
+ github.com/bep/gowebp v0.4.0
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/disintegration/imaging v1.6.2
+ github.com/dsoprea/go-jpeg-image-structure/v2 v2.0.0-20221012074422-4f3f7e934102
+ github.com/fredbi/uri v1.1.0 // indirect
+ github.com/fsnotify/fsnotify v1.8.0 // indirect
+ github.com/fyne-io/gl-js v0.1.0 // indirect
+ github.com/fyne-io/glfw-js v0.2.0 // indirect
+ github.com/fyne-io/image v0.1.1 // indirect
+ github.com/fyne-io/oksvg v0.1.0 // indirect
+ github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
+ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect
+ github.com/go-text/render v0.2.0 // indirect
+ github.com/go-text/typesetting v0.2.1 // indirect
+ github.com/godbus/dbus/v5 v5.1.0 // indirect
+ github.com/gosimple/slug v1.15.0
+ github.com/gosuri/uiprogress v0.0.1
+ github.com/hack-pad/go-indexeddb v0.3.2 // indirect
+ github.com/hack-pad/safejs v0.1.0 // indirect
+ github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 // indirect
+ github.com/joho/godotenv v1.5.1
+ github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
+ github.com/mitchellh/go-homedir v1.1.0
+ github.com/netlify/open-api/v2 v2.37.0
+ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
+ github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/rymdport/portal v0.4.1 // indirect
+ github.com/sirupsen/logrus v1.9.3
+ github.com/spf13/cobra v1.9.1
+ github.com/spf13/viper v1.20.1
+ github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
+ github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
+ github.com/stretchr/testify v1.10.0 // indirect
+ github.com/tdewolff/minify/v2 v2.23.8
+ github.com/yuin/goldmark v1.7.8 // indirect
+ golang.org/x/image v0.24.0 // indirect
+ golang.org/x/net v0.35.0 // indirect
+ golang.org/x/sys v0.30.0 // indirect
+ golang.org/x/text v0.22.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
+ gorm.io/driver/sqlite v1.6.0
)
diff --git a/go.sum b/go.sum
index 3a06ec1..2e2cf3d 100644
--- a/go.sum
+++ b/go.sum
@@ -5,53 +5,53 @@ cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6A
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
+cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE=
+cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U=
+cloud.google.com/go/auth v0.13.0 h1:8Fu8TZy167JkW8Tj3q7dIkr2v4cndv41ouecJx0PAHs=
+cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
+cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
-github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
-github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
+fyne.io/fyne/v2 v2.6.1 h1:kjPJD4/rBS9m2nHJp+npPSuaK79yj6ObMTuzR6VQ1Is=
+fyne.io/fyne/v2 v2.6.1/go.mod h1:YZt7SksjvrSNJCwbWFV32WON3mE1Sr7L41D29qMZ/lU=
+fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg=
+fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
+github.com/Azure/go-autorest/autorest v0.10.1 h1:uaB8A32IZU9YKs9v50+/LWIWTDHJk2vlGzbfd7FfESI=
github.com/Azure/go-autorest/autorest v0.10.1/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630=
-github.com/Azure/go-autorest/autorest v0.11.29 h1:I4+HL/JDvErx2LjyzaVxllw2lRDB5/BT2Bm4g20iqYw=
-github.com/Azure/go-autorest/autorest v0.11.29/go.mod h1:ZtEzC4Jy2JDrZLxvWs8LrBWEBycl1hbT1eknI8MtfAs=
github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
+github.com/Azure/go-autorest/autorest/adal v0.8.2 h1:O1X4oexUxnZCaEUGsvMnr8ZGj8HI37tNezwY4npRqA0=
github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
-github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk=
-github.com/Azure/go-autorest/autorest/adal v0.9.24 h1:BHZfgGsGwdkHDyZdtQRQk1WeUdW0m2WPAwuHZwUi5i4=
-github.com/Azure/go-autorest/autorest/adal v0.9.24/go.mod h1:7T1+g0PYFmACYW5LlG2fcoPiPlFHjClyRGL7dRlP5c8=
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
+github.com/Azure/go-autorest/autorest/date v0.2.0 h1:yW+Zlqf26583pE43KhfnhFcdmSWlm5Ew6bxipnr/tbM=
github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g=
-github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw=
-github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
+github.com/Azure/go-autorest/autorest/mocks v0.3.0 h1:qJumjCaCudz+OcqE9/XtEPfvtOjOmKaui4EOpFI6zZc=
github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM=
-github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
-github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw=
-github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU=
+github.com/Azure/go-autorest/logger v0.1.0 h1:ruG4BSDXONFRrZZJ2GUXDiUyVpayPmb1GnWeHDdaNKY=
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
-github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg=
-github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
+github.com/Azure/go-autorest/tracing v0.5.0 h1:TRn4WjSnkcSy5AEG3pnbtFSwNtwzjr4VYyQflFE619k=
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
-github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
-github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
+github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
-github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8=
-github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
-github.com/Sereal/Sereal v0.0.0-20220903133728-b4d312952c4c h1:fZYZayNeQmCugRjmTWQFoCpon0iFbESYOpNdMCsf5sQ=
-github.com/Sereal/Sereal v0.0.0-20220903133728-b4d312952c4c/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
+github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
+github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
@@ -62,31 +62,17 @@ github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:o
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
-github.com/asdine/storm v2.1.2+incompatible h1:dczuIkyqwY2LrtXPz8ixMrU/OFgZp71kbKTHGrXYt/Q=
-github.com/asdine/storm v2.1.2+incompatible/go.mod h1:RarYDc9hq1UPLImuiXK3BIWPJLdIygvV3PsInK0FbVQ=
github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
-github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
-github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/bep/gowebp v0.4.0 h1:QihuVnvIKbRoeBNQkN0JPMM8ClLmD6V2jMftTFwSK3Q=
github.com/bep/gowebp v0.4.0/go.mod h1:95gtYkAA8iIn1t3HkAPurRCVGV/6NhgaHJ1urz0iIwc=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
+github.com/cenkalti/backoff/v4 v4.0.2 h1:JIufpQLbh4DkbQoii76ItQIUFzevQSqOLZca4eamEDs=
github.com/cenkalti/backoff/v4 v4.0.2/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg=
-github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
-github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
-github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
-github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
-github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
-github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
-github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
-github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
-github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
-github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
-github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/corbym/gocrest v1.0.3/go.mod h1:maVFL5lbdS2PgfOQgGRWDYTeunSWQeiEgoNdTABShCs=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
@@ -94,16 +80,14 @@ github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc
github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
-github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
-github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
-github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
-github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
@@ -112,44 +96,56 @@ github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E=
github.com/dsoprea/go-exif/v3 v3.0.0-20200717053412-08f1b6708903/go.mod h1:0nsO1ce0mh5czxGeLo4+OCZ/C6Eo6ZlMWsz7rH/Gxv8=
-github.com/dsoprea/go-exif/v3 v3.0.0-20210625224831-a6301f85c82b/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk=
-github.com/dsoprea/go-exif/v3 v3.0.0-20221003160559-cf5cd88aa559/go.mod h1:rW6DMEv25U9zCtE5ukC7ttBRllXj7g7TAHl7tQrT5No=
-github.com/dsoprea/go-exif/v3 v3.0.0-20221003171958-de6cb6e380a8/go.mod h1:akyZEJZ/k5bmbC9gA612ZLQkcED8enS9vuTiuAkENr0=
-github.com/dsoprea/go-exif/v3 v3.0.1 h1:/IE4iW7gvY7BablV1XY0unqhMv26EYpOquVMwoBo/wc=
-github.com/dsoprea/go-exif/v3 v3.0.1/go.mod h1:10HkA1Wz3h398cDP66L+Is9kKDmlqlIJGPv8pk4EWvc=
+github.com/dsoprea/go-exif/v3 v3.0.0-20210428042052-dca55bf8ca15 h1:QQjMErNKRqrPUfRmdBpICftkac6holciY+B95S002fY=
+github.com/dsoprea/go-exif/v3 v3.0.0-20210428042052-dca55bf8ca15/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk=
+github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb h1:gwjJjUr6FY7zAWVEueFPrcRHhd9+IK81TcItbqw2du4=
+github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM=
+github.com/dsoprea/go-jpeg-image-structure/v2 v2.0.0-20221012074422-4f3f7e934102 h1:gmTXQdSuuuORRFPTS2uaYpAXU5oUNkXdeYSlZe5NvsE=
+github.com/dsoprea/go-jpeg-image-structure/v2 v2.0.0-20221012074422-4f3f7e934102/go.mod h1:WaARaUjQuSuDCDFAiU/GwzfxMTJBulfEhqEA2Tx6B4Y=
github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696/go.mod h1:Nm/x2ZUNRW6Fe5C3LxdY1PyZY5wmDv/s5dkPJ/VB3iA=
github.com/dsoprea/go-logging v0.0.0-20200517223158-a10564966e9d/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8=
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd h1:l+vLbuxptsC6VQyQsfD7NnEC8BZuFpz45PgY+pH8YTg=
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8=
+github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c h1:7j5aWACOzROpr+dvMtu8GnI97g9ShLWD72XIELMgn+c=
+github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c/go.mod h1:pqKB+ijp27cEcrHxhXVgUUMlSDRuGJJp1E+20Lj5H0E=
github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf/go.mod h1:95+K3z2L0mqsVYd6yveIv1lmtT3tcQQ3dVakPySffW8=
+github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e h1:IxIbA7VbCNrwumIYjDoMOdf4KOSkMC6NJE4s8oRbE7E=
github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e/go.mod h1:uAzdkPTub5Y9yQwXe8W4m2XuP0tK4a9Q/dantD0+uaU=
-github.com/dsoprea/go-utility/v2 v2.0.0-20221003142440-7a1927d49d9d/go.mod h1:LVjRU0RNUuMDqkPTxcALio0LWPFPXxxFCvVGVAwEpFc=
-github.com/dsoprea/go-utility/v2 v2.0.0-20221003160719-7bc88537c05e/go.mod h1:VZ7cB0pTjm1ADBWhJUOHESu4ZYy9JN+ZPqjfiW09EPU=
-github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 h1:DilThiXje0z+3UQ5YjYiSRRzVdtamFpvBQXKwMglWqw=
-github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349/go.mod h1:4GC5sXji84i/p+irqghpPFZBF8tRN/Q7+700G0/DLe8=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/evanoberholster/imagemeta v0.3.1 h1:E4GUjXcvlVMjP9joN25+bBNf3Al3MTTfMqCrDOCW+LE=
-github.com/evanoberholster/imagemeta v0.3.1/go.mod h1:V0vtDJmjTqvwAYO8r+u33NRVIMXQb0qSqEfImoKEiXM=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
+github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/fredbi/uri v1.1.0 h1:OqLpTXtyRg9ABReqvDGdJPqZUxs8cyBDOMXBbskCaB8=
+github.com/fredbi/uri v1.1.0/go.mod h1:aYTUoAXBOq7BLfVJ8GnKmfcuURosB1xyHDIfWeC/iW4=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/fyne-io/gl-js v0.1.0 h1:8luJzNs0ntEAJo+8x8kfUOXujUlP8gB3QMOxO2mUdpM=
+github.com/fyne-io/gl-js v0.1.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
+github.com/fyne-io/glfw-js v0.2.0 h1:8GUZtN2aCoTPNqgRDxK5+kn9OURINhBEBc7M4O1KrmM=
+github.com/fyne-io/glfw-js v0.2.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk=
+github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
+github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
+github.com/fyne-io/oksvg v0.1.0 h1:7EUKk3HV3Y2E+qypp3nWqMXD7mum0hCw2KEGhI1fnBw=
+github.com/fyne-io/oksvg v0.1.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
+github.com/go-errors/errors v1.1.1 h1:ljK/pL5ltg3qoN+OtN6yCv9HWSfMwxSx90GJCZQxYNg=
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
-github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
-github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
-github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
+github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
+github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
@@ -158,8 +154,6 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
-github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
-github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
@@ -256,6 +250,16 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-swagger/go-swagger v0.24.0/go.mod h1:c9aQFJez+LeV1heNtxEclIL2XIbeS+CTKt+Bm1kuz4E=
github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013/go.mod h1:b65mBPzqzZWxOZGxSWrqs4GInLIn+u99Q9q7p+GKni0=
+github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc=
+github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU=
+github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8=
+github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M=
+github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
+github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
+github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
+github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b h1:khEcpUM4yFcxg4/FHQWkvVRmgijNXRfzkIDHh23ggEo=
+github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=
github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=
github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg=
@@ -280,20 +284,13 @@ github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWe
github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ=
github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=
github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
-github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
-github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
-github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
-github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
-github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
+github.com/golang/geo v0.0.0-20200319012246-673a6f80352d h1:C/hKUcHT483btRbeGkrRjJz+Zbcj8audldIi9tRJDCc=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
-github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
-github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I=
-github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@@ -309,11 +306,7 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
-github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
-github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -326,22 +319,30 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
+github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
+github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
+github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
+github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
-github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
-github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/gosimple/slug v1.14.0 h1:RtTL/71mJNDfpUbCOmnf/XFkzKRtD6wL6Uy+3akm4Es=
-github.com/gosimple/slug v1.14.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
+github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
+github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo=
+github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
github.com/gosuri/uilive v0.0.4 h1:hUEBpQDj8D8jXgtCdBu7sWsy5sbW/5GhuO8KBwJ2jyY=
@@ -351,6 +352,10 @@ github.com/gosuri/uiprogress v0.0.1/go.mod h1:C1RTYn4Sc7iEyf6j8ft5dyoZ4212h8G1ol
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
+github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
+github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8=
+github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -366,7 +371,6 @@ github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
@@ -375,22 +379,27 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
-github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
-github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
+github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 h1:wMeVzrPO3mfHIWLZtDcSaGAe2I4PW9B/P5nMkRSwCAc=
+github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
-github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
+github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
-github.com/k0kubun/pp/v3 v3.3.0 h1:/Unrck5tDGUSjsUJsmx9GUL64pNKOY5UEdoP1F7FBq8=
-github.com/k0kubun/pp/v3 v3.3.0/go.mod h1:wJadGBvcY6JKaiUkB89VzUACKDmTX1r4aQTPERpZc6w=
github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
@@ -410,23 +419,7 @@ github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
-github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
-github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
-github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
-github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
-github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
-github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
-github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
-github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
-github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
-github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
-github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
-github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
-github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
-github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
-github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@@ -435,21 +428,14 @@ github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
-github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
-github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
-github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
-github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
-github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
-github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
-github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
-github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
-github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
@@ -470,8 +456,12 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/netlify/open-api/v2 v2.34.0 h1:bOjMLlcQQBdhb8mUHvTNkbkVkYTHCDCMeLKw5Hjjmig=
-github.com/netlify/open-api/v2 v2.34.0/go.mod h1:g4zTkS20wcAzQCJuBfaIw4pUeTPpaKTDMIT/9yiNoNs=
+github.com/netlify/open-api/v2 v2.37.0 h1:ev+D1hcwjl72UUehUvZRevOGqayzJK+cOE1MqmtqR4Y=
+github.com/netlify/open-api/v2 v2.37.0/go.mod h1:g4zTkS20wcAzQCJuBfaIw4pUeTPpaKTDMIT/9yiNoNs=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
+github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
+github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
@@ -485,17 +475,14 @@ github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAv
github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
-github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
-github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
-github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
-github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
+github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
-github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
@@ -508,29 +495,23 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
-github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
-github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
-github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
-github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
-github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
-github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/rsc/goversion v1.2.0 h1:zVF4y5ciA/rw779S62bEAq4Yif1cBc/UwRkXJ2xZyT4=
github.com/rsc/goversion v1.2.0/go.mod h1:Tf/O0TQyfRvp7NelXAyfXYRKUO+LX3KNgXc8ALRUv4k=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
-github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=
-github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
-github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
-github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
-github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
-github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
+github.com/rymdport/portal v0.4.1 h1:2dnZhjf5uEaeDjeF/yBIeeRo6pNI2QAKm7kq1w/kbnA=
+github.com/rymdport/portal v0.4.1/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
+github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
+github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
+github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
@@ -548,23 +529,28 @@ github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIK
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
-github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
-github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
+github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
+github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
-github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
+github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
-github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
-github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
+github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
+github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
-github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
+github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
-github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
-github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
+github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
+github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
+github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
+github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
+github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
+github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
@@ -578,68 +564,55 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
-github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
-github.com/tdewolff/minify/v2 v2.21.2 h1:VfTvmGVtBYhMTlUAeHtXM7XOsW0JT/6uMwUPPqgUs9k=
-github.com/tdewolff/minify/v2 v2.21.2/go.mod h1:Olje3eHdBnrMjINKffDsil/3NV98Iv7MhWf7556WQVg=
-github.com/tdewolff/parse/v2 v2.7.19 h1:7Ljh26yj+gdLFEq/7q9LT4SYyKtwQX4ocNrj45UCePg=
-github.com/tdewolff/parse/v2 v2.7.19/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
-github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
-github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo=
-github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
+github.com/tdewolff/minify/v2 v2.23.8 h1:tvjHzRer46kwOfpdCBCWsDblCw3QtnLJRd61pTVkyZ8=
+github.com/tdewolff/minify/v2 v2.23.8/go.mod h1:VW3ISUd3gDOZuQ/jwZr4sCzsuX+Qvsx87FDMjk6Rvno=
+github.com/tdewolff/parse/v2 v2.8.1 h1:J5GSHru6o3jF1uLlEKVXkDxxcVx6yzOlIVIotK4w2po=
+github.com/tdewolff/parse/v2 v2.8.1/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo=
+github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE=
+github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
-github.com/tinylib/msgp v1.2.4 h1:yLFeUGostXXSGW5vxfT5dXG/qzkn4schv2I7at5+hVU=
-github.com/tinylib/msgp v1.2.4/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
-github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
-github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM=
-github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
-github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
-github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
-github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
-github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
-github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
-github.com/wailsapp/go-webview2 v1.0.17 h1:DkLnUKqW7J///OBXkInMq1fzC88G6ZjHwKuHXThuaco=
-github.com/wailsapp/go-webview2 v1.0.17/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo=
-github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
-github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
-github.com/wailsapp/wails/v2 v2.9.2 h1:Xb5YRTos1w5N7DTMyYegWaGukCP2fIaX9WF21kPPF2k=
-github.com/wailsapp/wails/v2 v2.9.2/go.mod h1:uehvlCwJSFcBq7rMCGfk4rxca67QQGsbg5Nm4m9UnBs=
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
+github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
-go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
-go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
go.mongodb.org/mongo-driver v1.3.0/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE=
go.mongodb.org/mongo-driver v1.3.4/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE=
go.mongodb.org/mongo-driver v1.4.3/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc=
go.mongodb.org/mongo-driver v1.4.4/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc=
-go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM=
-go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4=
+go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
+go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
-go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
-go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
-go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
-go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
-go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
-go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
-go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
-go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
+go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
+go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
+go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
+go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
+go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo=
+go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok=
+go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
+go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
+go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
-go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
-go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
+go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -656,23 +629,18 @@ golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
-golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
-golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
-golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
+golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
+golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
-golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
-golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
-golang.org/x/image v0.22.0 h1:UtK5yLUzilVrkjMAZAZ34DXGpASN8i8pj8g+O+yd10g=
-golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4=
+golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
+golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -687,7 +655,6 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -713,18 +680,14 @@ golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
-golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
-golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
+golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
+golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -737,16 +700,14 @@ golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
-golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
+golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -763,44 +724,27 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200610111108-226ff32320da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
-golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
+golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
-golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
-golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
+golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
+golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -832,7 +776,6 @@ golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200612220849-54c614fe050c/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -846,8 +789,8 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
-google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
+google.golang.org/genai v1.14.0 h1:oggc+F4l0MsRMQ1H/O2v8fXGD5B04rvd1q0GvHNsgEo=
+google.golang.org/genai v1.14.0/go.mod h1:QPj5NGJw+3wEOHg+PrsWwJKvG6UC84ex5FR7qAYsN/M=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@@ -858,11 +801,16 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.67.3 h1:OgPcDAFKHnH8X3O4WcO4XUc8GRDeKsKReqbQtiCj7N8=
+google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -872,8 +820,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
-google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
-google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
+google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -883,8 +831,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
-gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
@@ -901,6 +847,10 @@ gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
+gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
+gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
+gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/main.go b/main.go
index 485c131..15a0f97 100644
--- a/main.go
+++ b/main.go
@@ -1,21 +1,18 @@
+// Responsive grid layout for Fyne
+
package main
import (
"embed"
-
- "github.com/robrotheram/gogallery/backend/cmd"
- "github.com/robrotheram/gogallery/backend/embeds"
+ "gogallery/cmd"
+ "gogallery/pkg/embeds"
)
-//go:embed frontend/dist
-var assets embed.FS
-
-//go:embed themes/eastnor
+//go:embed themes
var ThemeFS embed.FS
func init() {
embeds.ThemeFS = ThemeFS
- embeds.DashboardFS = assets
}
func main() {
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..605db0c
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,521 @@
+{
+ "name": "gogallery",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "node_modules/@ampproject/remapping": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
+ "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@isaacs/fs-minipass": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
+ "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^7.0.4"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.12",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
+ "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
+ "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.29",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz",
+ "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@parcel/watcher": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
+ "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "detect-libc": "^1.0.3",
+ "is-glob": "^4.0.3",
+ "micromatch": "^4.0.5",
+ "node-addon-api": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "@parcel/watcher-android-arm64": "2.5.1",
+ "@parcel/watcher-darwin-arm64": "2.5.1",
+ "@parcel/watcher-darwin-x64": "2.5.1",
+ "@parcel/watcher-freebsd-x64": "2.5.1",
+ "@parcel/watcher-linux-arm-glibc": "2.5.1",
+ "@parcel/watcher-linux-arm-musl": "2.5.1",
+ "@parcel/watcher-linux-arm64-glibc": "2.5.1",
+ "@parcel/watcher-linux-arm64-musl": "2.5.1",
+ "@parcel/watcher-linux-x64-glibc": "2.5.1",
+ "@parcel/watcher-linux-x64-musl": "2.5.1",
+ "@parcel/watcher-win32-arm64": "2.5.1",
+ "@parcel/watcher-win32-ia32": "2.5.1",
+ "@parcel/watcher-win32-x64": "2.5.1"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-x64-glibc": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
+ "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/cli": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.11.tgz",
+ "integrity": "sha512-7RAFOrVaXCFz5ooEG36Kbh+sMJiI2j4+Ozp71smgjnLfBRu7DTfoq8DsTvzse2/6nDeo2M3vS/FGaxfDgr3rtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@parcel/watcher": "^2.5.1",
+ "@tailwindcss/node": "4.1.11",
+ "@tailwindcss/oxide": "4.1.11",
+ "enhanced-resolve": "^5.18.1",
+ "mri": "^1.2.0",
+ "picocolors": "^1.1.1",
+ "tailwindcss": "4.1.11"
+ },
+ "bin": {
+ "tailwindcss": "dist/index.mjs"
+ }
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz",
+ "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.3.0",
+ "enhanced-resolve": "^5.18.1",
+ "jiti": "^2.4.2",
+ "lightningcss": "1.30.1",
+ "magic-string": "^0.30.17",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.1.11"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz",
+ "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "detect-libc": "^2.0.4",
+ "tar": "^7.4.3"
+ },
+ "engines": {
+ "node": ">= 10"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.1.11",
+ "@tailwindcss/oxide-darwin-arm64": "4.1.11",
+ "@tailwindcss/oxide-darwin-x64": "4.1.11",
+ "@tailwindcss/oxide-freebsd-x64": "4.1.11",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.1.11",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.1.11",
+ "@tailwindcss/oxide-linux-x64-musl": "4.1.11",
+ "@tailwindcss/oxide-wasm32-wasi": "4.1.11",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.1.11"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz",
+ "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide/node_modules/detect-libc": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
+ "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chownr": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
+ "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
+ "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
+ "license": "Apache-2.0",
+ "bin": {
+ "detect-libc": "bin/detect-libc.js"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.18.2",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz",
+ "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "license": "ISC"
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
+ "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
+ "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-darwin-arm64": "1.30.1",
+ "lightningcss-darwin-x64": "1.30.1",
+ "lightningcss-freebsd-x64": "1.30.1",
+ "lightningcss-linux-arm-gnueabihf": "1.30.1",
+ "lightningcss-linux-arm64-gnu": "1.30.1",
+ "lightningcss-linux-arm64-musl": "1.30.1",
+ "lightningcss-linux-x64-gnu": "1.30.1",
+ "lightningcss-linux-x64-musl": "1.30.1",
+ "lightningcss-win32-arm64-msvc": "1.30.1",
+ "lightningcss-win32-x64-msvc": "1.30.1"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz",
+ "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss/node_modules/detect-libc": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
+ "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.17",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
+ "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/minizlib": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz",
+ "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
+ "license": "MIT",
+ "dependencies": {
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
+ "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
+ "license": "MIT",
+ "bin": {
+ "mkdirp": "dist/cjs/src/bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/mri": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
+ "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/node-addon-api": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
+ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
+ "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
+ "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tar": {
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
+ "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
+ "license": "ISC",
+ "dependencies": {
+ "@isaacs/fs-minipass": "^4.0.0",
+ "chownr": "^3.0.0",
+ "minipass": "^7.1.2",
+ "minizlib": "^3.0.1",
+ "mkdirp": "^3.0.1",
+ "yallist": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
+ "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ }
+ }
+}
diff --git a/pkg/ai/capition.go b/pkg/ai/capition.go
new file mode 100644
index 0000000..935c09e
--- /dev/null
+++ b/pkg/ai/capition.go
@@ -0,0 +1,65 @@
+package ai
+
+import (
+ "fmt"
+ "gogallery/pkg/datastore"
+ "os"
+)
+
+type ImageCaption struct {
+ Title string `json:"title"`
+ Caption string `json:"caption"`
+}
+
+const basePrompt = `
+ You are a helpful assistant.
+ You will be given an image and I need a title and a caption for it.
+ Please provide a short title and a detailed caption for the image.
+ The Captions should be descriptive and engaging, providing context and details about the image.
+ Make sure to include any relevant information that would help someone understand the image better.
+`
+
+type AIClient interface {
+ GenerateCaption(image []byte) (*ImageCaption, error)
+}
+
+var clients = map[string]AIClient{}
+
+func IsAi() bool {
+ return len(clients) > 0
+}
+
+func GenerateCaption(db *datastore.DataStore, id string) (*ImageCaption, error) {
+ stat := db.NewTask("Ai Caption for "+id, 1)
+ defer stat.Complete()
+ stat.Start()
+ c, ok := clients["gemini"]
+ if !ok {
+ stat.Fail("AI client not found")
+ return nil, fmt.Errorf("AI client not found")
+ }
+ pic, err := db.Pictures.FindById(id)
+ if err != nil {
+ stat.Fail("Failed to find picture: " + err.Error())
+ return nil, fmt.Errorf("failed to find picture: %w", err)
+ }
+ bytes, err := os.ReadFile(pic.Path)
+ if err != nil {
+ stat.Fail("Failed to read image file: " + err.Error())
+ return nil, fmt.Errorf("failed to read image file: %w", err)
+ }
+ caption, err := c.GenerateCaption(bytes)
+ if err != nil {
+ stat.Fail("Failed to generate caption: " + err.Error())
+ return nil, fmt.Errorf("failed to generate caption: %w", err)
+ }
+ fmt.Println("Generated Caption:", caption.Caption)
+ pic.Caption = caption.Caption
+ pic.Name = caption.Title
+ if err := db.Pictures.Update(pic.Id, pic); err != nil {
+ stat.Fail("Failed to update picture: " + err.Error())
+ return nil, fmt.Errorf("failed to update picture: %w", err)
+ }
+ fmt.Println("Picture updated successfully")
+ return caption, nil
+}
diff --git a/pkg/ai/gemini.go b/pkg/ai/gemini.go
new file mode 100644
index 0000000..3a13ead
--- /dev/null
+++ b/pkg/ai/gemini.go
@@ -0,0 +1,69 @@
+package ai
+
+import (
+ "context"
+ "encoding/json"
+ "gogallery/pkg/config"
+ "log"
+
+ "google.golang.org/genai"
+)
+
+type GeminiClient struct {
+ *genai.Client
+}
+
+func RegisterGeminiClient() (*GeminiClient, error) {
+ ctx := context.Background()
+ cc := &genai.ClientConfig{
+ APIKey: config.Config.UI.GeminiApiKey,
+ }
+ client, err := genai.NewClient(ctx, cc)
+ if err != nil {
+ log.Printf("Error creating Gemini client: %v", err)
+ return nil, err
+ }
+ gm := &GeminiClient{Client: client}
+ clients["gemini"] = gm
+ return gm, nil
+}
+
+func (g *GeminiClient) GenerateCaption(bytes []byte) (*ImageCaption, error) {
+ var caption ImageCaption
+ ctx := context.Background()
+
+ config := &genai.GenerateContentConfig{
+ ResponseMIMEType: "application/json",
+ ResponseSchema: &genai.Schema{
+ Type: genai.TypeObject,
+ Properties: map[string]*genai.Schema{
+ "title": {Type: genai.TypeString},
+ "caption": {Type: genai.TypeString},
+ },
+ },
+ }
+ parts := []*genai.Part{
+ genai.NewPartFromText(basePrompt),
+ genai.NewPartFromBytes(bytes, "image/jpeg"),
+ }
+
+ contents := []*genai.Content{
+ genai.NewContentFromParts(parts, genai.RoleUser),
+ }
+
+ result, err := g.Client.Models.GenerateContent(
+ ctx,
+ "gemini-2.5-flash",
+ contents,
+ config,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ err = json.Unmarshal([]byte(result.Text()), &caption)
+ if err != nil {
+ return nil, err
+ }
+ return &caption, nil
+}
diff --git a/backend/config/configuration.go b/pkg/config/configuration.go
similarity index 57%
rename from backend/config/configuration.go
rename to pkg/config/configuration.go
index d3604b5..b96beff 100644
--- a/backend/config/configuration.go
+++ b/pkg/config/configuration.go
@@ -1,26 +1,26 @@
package config
import (
- "fmt"
"log"
"os"
"strings"
- "github.com/manifoldco/promptui"
"github.com/spf13/viper"
)
type Configuration struct {
- Server ServerConfiguration
+ UI UIConfiguration
About AboutConfiguration
Gallery GalleryConfiguration
Deploy DeployConfig
}
-type ServerConfiguration struct {
- Host string
- Port string
- Debug bool
+type UIConfiguration struct {
+ Public bool
+ Notification bool
+ Theme string
+ ImagesPerPage int
+ GeminiApiKey string
}
type GalleryConfiguration struct {
@@ -47,6 +47,7 @@ type AboutConfiguration struct {
BackgroundPhoto string
Blog string
Website string
+ Github string
}
type DeployConfig struct {
@@ -72,24 +73,6 @@ func LoadConfig() *Configuration {
}
return Config
}
-func (c *ServerConfiguration) GetAddr() string {
- host := c.Host
- if len(host) == 0 {
- host = "localhost"
- }
- c.Port = strings.Replace(c.Port, ":", "", 1)
- return fmt.Sprintf("%s:%s", host, c.Port)
-}
-
-func (c *ServerConfiguration) GetLocalAddr() string {
- host := c.Host
- if len(host) == 0 || host == "0.0.0.0" {
- host = "localhost"
- }
- c.Port = strings.Replace(c.Port, ":", "", 1)
- return fmt.Sprintf("%s:%s", host, c.Port)
-}
-
func (c *AboutConfiguration) Save() {
log.Println("Saving About Config")
viper.Set("about", c)
@@ -111,44 +94,11 @@ func (c *DeployConfig) Save() {
func (c *Configuration) Save() {
viper.Set("about", c.About)
viper.Set("gallery", c.Gallery)
- viper.Set("server", c.Server)
+ viper.Set("ui", c.UI)
viper.Set("deploy", c.Deploy)
viper.WriteConfig()
}
-func (c *Configuration) PromptSiteName() {
- prompt := promptui.Prompt{Label: "Site Name", Default: c.Gallery.Name}
- result, _ := prompt.Run()
- c.Gallery.Name = result
-}
-
-func (c *Configuration) PromptGalleryBasePath() {
- prompt := promptui.Prompt{
- Label: "Path to your images",
- Default: c.Gallery.Basepath,
- Validate: func(s string) error {
- if !c.FileExists(s) {
- return fmt.Errorf("path %s, does not exits", s)
- }
- return nil
- },
- }
- result, _ := prompt.Run()
- c.Gallery.Basepath = result
-}
-
-func (c *Configuration) PromptGalleryDest() {
- prompt := promptui.Prompt{Label: "Path to destination", Default: c.Gallery.Destpath}
- result, _ := prompt.Run()
- c.Gallery.Destpath = result
-}
-
-func (c *Configuration) PromptGalleryTheme() {
- prompt := promptui.Prompt{Label: "Theme to use", Default: "./theme/estnor"}
- result, _ := prompt.Run()
- c.Gallery.Theme = result
-}
-
func (c *Configuration) FileExists(path string) bool {
if _, err := os.Stat(path); os.IsNotExist(err) {
return false
@@ -161,18 +111,21 @@ func (c *Configuration) Validate() {
log.Panic("path to images does not exist")
os.Exit(1)
}
- if !c.FileExists(c.Gallery.Theme) && c.Gallery.Theme != "default" {
- log.Panic("path to theme does not exist")
- }
}
func DefaultConfig() {
Config = &Configuration{
- Server: ServerConfiguration{},
- About: AboutConfiguration{},
+ UI: UIConfiguration{},
+ About: AboutConfiguration{},
Gallery: GalleryConfiguration{
Theme: "default",
},
}
+ // Ensure config file is created if it doesn't exist
+ if !Config.FileExists(viper.ConfigFileUsed()) {
+ viper.SetConfigName(".gogallery")
+ viper.SetConfigType("yaml")
+ _ = viper.SafeWriteConfig() // ignore error if file exists
+ }
Config.Save()
}
diff --git a/pkg/config/imageConfig.go b/pkg/config/imageConfig.go
new file mode 100644
index 0000000..61b8887
--- /dev/null
+++ b/pkg/config/imageConfig.go
@@ -0,0 +1,21 @@
+package config
+
+type ImgSize struct {
+ MinWidth int // Minimum screen width in pixels for this image source
+ ImgWidth int // Recommended image width to generate for this breakpoint
+}
+
+var ImageSizes = map[string]ImgSize{
+ "xsmall": {MinWidth: 0, ImgWidth: 200}, // Phones (default)
+ "small": {MinWidth: 480, ImgWidth: 400}, // Small tablets / landscape phones
+ "medium": {MinWidth: 768, ImgWidth: 960}, // Tablets
+ "large": {MinWidth: 1024, ImgWidth: 1280}, // Laptops / small desktops
+ "xlarge": {MinWidth: 1440, ImgWidth: 0}, // Large desktops (0 means use original size)
+}
+
+type ImageType int
+
+const (
+ JPEG ImageType = iota
+ WebP
+)
diff --git a/backend/config/md5Hash.go b/pkg/config/md5Hash.go
similarity index 100%
rename from backend/config/md5Hash.go
rename to pkg/config/md5Hash.go
diff --git a/pkg/config/utils.go b/pkg/config/utils.go
new file mode 100644
index 0000000..df8f2fa
--- /dev/null
+++ b/pkg/config/utils.go
@@ -0,0 +1,78 @@
+package config
+
+import (
+ "fmt"
+ "io"
+ "io/fs"
+ "os"
+ "path"
+)
+
+func FileExists(filename string) bool {
+ info, err := os.Stat(filename)
+ if os.IsNotExist(err) {
+ return false
+ }
+ if info.Size() == 0 {
+ return false
+ }
+ return !info.IsDir()
+}
+
+func Dir(src string, dst string) error {
+ var err error
+ var fds []fs.DirEntry
+ var srcinfo os.FileInfo
+
+ if srcinfo, err = os.Stat(src); err != nil {
+ return err
+ }
+
+ if err = os.MkdirAll(dst, srcinfo.Mode()); err != nil {
+ return err
+ }
+
+ if fds, err = os.ReadDir(src); err != nil {
+ return err
+ }
+ for _, fd := range fds {
+ srcfp := path.Join(src, fd.Name())
+ dstfp := path.Join(dst, fd.Name())
+
+ if fd.IsDir() {
+ if err = Dir(srcfp, dstfp); err != nil {
+ fmt.Println(err)
+ }
+ } else {
+ if err = Copy(srcfp, dstfp); err != nil {
+ fmt.Println(err)
+ }
+ }
+ }
+ return nil
+}
+
+func Copy(src, dst string) error {
+ var err error
+ var srcfd *os.File
+ var dstfd *os.File
+ var srcinfo os.FileInfo
+
+ if srcfd, err = os.Open(src); err != nil {
+ return err
+ }
+ defer srcfd.Close()
+
+ if dstfd, err = os.Create(dst); err != nil {
+ return err
+ }
+ defer dstfd.Close()
+
+ if _, err = io.Copy(dstfd, srcfd); err != nil {
+ return err
+ }
+ if srcinfo, err = os.Stat(src); err != nil {
+ return err
+ }
+ return os.Chmod(dst, srcinfo.Mode())
+}
diff --git a/pkg/datastore/Exif.go b/pkg/datastore/Exif.go
new file mode 100644
index 0000000..11a12a2
--- /dev/null
+++ b/pkg/datastore/Exif.go
@@ -0,0 +1,401 @@
+package datastore
+
+import (
+ "bytes"
+ "fmt"
+ "image"
+ "log"
+ "math"
+ "os"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/araddon/dateparse"
+ "github.com/dsoprea/go-exif/v3"
+ exifcommon "github.com/dsoprea/go-exif/v3/common"
+ jpeg "github.com/dsoprea/go-jpeg-image-structure/v2"
+)
+
+var exifIfdMapping *exifcommon.IfdMapping
+var exifTagIndex = exif.NewTagIndex()
+var exifDateTimeTags = []string{"DateTimeOriginal", "DateTimeCreated", "CreateDate", "DateTime", "DateTimeDigitized"}
+
+func parser(fileName string) (rawExif []byte, err error) {
+ jpegMp := jpeg.NewJpegMediaParser()
+ sl, err := jpegMp.ParseFile(fileName)
+
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse jpeg file %s: %w", fileName, err)
+ }
+ _, rawExif, err = sl.Exif()
+ if err == nil {
+ return rawExif, nil
+ }
+ rawExif, err = exif.SearchFileAndExtractExif(fileName)
+ if err != nil {
+ return rawExif, fmt.Errorf("found no exif data")
+ }
+ return rawExif, nil
+}
+
+func (u *Picture) parseGPS(rawExif []byte) error {
+ var ifdIndex exif.IfdIndex
+ _, ifdIndex, err := exif.Collect(exifIfdMapping, exifTagIndex, rawExif)
+
+ if err != nil {
+ return err
+ }
+
+ return u.extractGPSData(ifdIndex)
+}
+
+func (u *Picture) extractGPSData(ifdIndex exif.IfdIndex) error {
+ ifd, err := ifdIndex.RootIfd.ChildWithIfdPath(exifcommon.IfdGpsInfoStandardIfdIdentity)
+ if err != nil {
+ return nil // No GPS data found, not an error.
+ }
+
+ gi, err := ifd.GpsInfo()
+ if err != nil {
+ return err
+ }
+
+ if !math.IsNaN(gi.Latitude.Decimal()) && !math.IsNaN(gi.Longitude.Decimal()) {
+ u.GPSLat, u.GPSLng = NormalizeGPS(gi.Latitude.Decimal(), gi.Longitude.Decimal())
+ }
+
+ if gi.Altitude != 0 {
+ u.GPSAltitude = float64(gi.Altitude)
+ }
+
+ if !gi.Timestamp.IsZero() {
+ u.GPSTimestamp = gi.Timestamp
+ }
+
+ return nil
+}
+
+func (u *Picture) CreateExif() error {
+
+ rawExif, err := parser(u.Path)
+ if err != nil {
+ return err
+ }
+
+ opt := exif.ScanOptions{}
+ entries, _, err := exif.GetFlatExifData(rawExif, &opt)
+ if err != nil {
+ log.Printf("Error getting flat EXIF data for %s: %v", u.Path, err)
+ return err
+ }
+
+ tags := make(map[string]string, len(entries))
+
+ // Ignore IFD1 tags with existing IFD0 values.
+ for _, tag := range entries {
+ s := strings.Split(tag.FormattedFirst, "\x00")
+ if tag.TagName == "" || len(s) == 0 {
+ // Do nothing.
+ } else if s[0] != "" && (tags[tag.TagName] == "" || tag.IfdPath != exif.ThumbnailFqIfdPath) {
+ tags[tag.TagName] = s[0]
+ }
+ }
+
+ // Abort if no values were found.
+ if len(tags) == 0 {
+ return fmt.Errorf("metadata: no exif data in %s", u.Path)
+ }
+
+ u.parseGPS(rawExif)
+
+ u.DateTaken = parseExifDateTime(tags)
+ u.Camera = cameraModelToString(tags)
+ u.FStop = apatureToString(tags)
+ u.FocalLength = focalLengthToString(tags)
+
+ if value, ok := tags["FocalLengthIn35mmFilm"]; ok {
+ u.FocalLength = value
+ } else {
+ u.FocalLength = tags["FocalLength"]
+ }
+
+ u.Name = tags["DocumentName"]
+ u.Caption = tags["ImageDescription"]
+
+ u.ISO = tags["ISOSpeedRatings"]
+ u.ShutterSpeed = tags["ExposureTime"]
+ u.LensModel = tags["LensModel"]
+ u.FileFormat = tags["FileType"]
+ u.Software = tags["Software"]
+
+ if w, h, err := GetImageDention(u); err == nil {
+ u.Dimension = fmt.Sprintf("%dx%d", w, h)
+ u.AspectRatio = float32(w) / float32(h)
+ }
+
+ u.ColorSpace = formatColorSpace(tags["ColorSpace"])
+ u.MeteringMode = formatMeteringMode(tags["MeteringMode"])
+ u.Saturation = formatSaturation(tags["Saturation"])
+ u.Contrast = formatContrast(tags["Contrast"])
+ u.Sharpness = formatSharpness(tags["Sharpness"])
+ u.Temperature = formatTemperature(tags["Temperature"])
+ u.WhiteBalance = formatWhiteBalance(tags["WhiteBalance"])
+
+ return nil
+}
+func GetImageDention(u *Picture) (width, height int, err error) {
+ f, err := os.Open(u.Path)
+ if err != nil {
+ return 0, 0, fmt.Errorf("failed to open image file %s: %v", u.Path, err)
+ }
+ defer f.Close()
+
+ img, format, err := image.DecodeConfig(f)
+ if err != nil {
+ return 0, 0, fmt.Errorf("image decode config failed: %v", err)
+ }
+ if format != "jpeg" && format != "png" && format != "gif" && format != "webp" {
+ return 0, 0, fmt.Errorf("unsupported image format: %s", format)
+ }
+ return img.Width, img.Height, nil
+}
+
+func parseExifDateTime(tags map[string]string) time.Time {
+ takenAt := time.Time{}
+ for _, name := range exifDateTimeTags {
+ if dateTime, _ := dateparse.ParseAny(tags[name]); !dateTime.IsZero() {
+ takenAt = dateTime
+ break
+ }
+ }
+ if !takenAt.IsZero() {
+ if takenAtLocal, err := time.ParseInLocation("2006-01-02T15:04:05", takenAt.Format("2006-01-02T15:04:05"), time.UTC); err == nil {
+ return takenAtLocal
+ } else {
+ return takenAt
+ }
+ }
+ return takenAt
+}
+
+func cameraModelToString(tags map[string]string) string {
+ // Extract camera make and model from EXIF tags.
+ make := tags["Make"]
+ model := tags["Model"]
+ if make != "" && model != "" {
+ return fmt.Sprintf("%s %s", make, model)
+ } else if make != "" {
+ return make
+ } else if model != "" {
+ return model
+ } else {
+ return "Unknown Camera"
+ }
+}
+
+func apatureToString(tags map[string]string) string {
+ if value, ok := tags["FNumber"]; ok {
+ values := strings.Split(value, "/")
+ if len(values) == 2 && values[1] != "0" && values[1] != "" {
+ number, _ := strconv.ParseFloat(values[0], 64)
+ denom, _ := strconv.ParseFloat(values[1], 64)
+ return fmt.Sprintf("%.1f", math.Round((number/denom)*1000)/1000)
+ }
+ }
+ return "0.0"
+}
+
+func focalLengthToString(tags map[string]string) string {
+ if value, ok := tags["FocalLengthIn35mmFilm"]; ok {
+ return value
+ }
+ return tags["FocalLength"]
+}
+
+func formatSaturation(sat string) string {
+ switch sat {
+ case "0", "":
+ return "Normal"
+ case "-1":
+ return "Low"
+ case "1":
+ return "High"
+ default:
+ return "Unknown"
+ }
+}
+
+func formatContrast(contrast string) string {
+ switch contrast {
+ case "0", "":
+ return "Normal"
+ case "-1":
+ return "Low"
+ case "1":
+ return "High"
+ default:
+ return "Unknown"
+ }
+}
+
+func formatSharpness(sharpness string) string {
+ switch sharpness {
+ case "0", "":
+ return "Normal"
+ case "-1":
+ return "Low"
+ case "1":
+ return "High"
+ default:
+ return "Unknown"
+ }
+}
+
+func formatTemperature(temp string) string {
+ switch temp {
+ case "0", "":
+ return "Auto"
+ case "1":
+ return "Manual"
+ case "2":
+ return "Daylight"
+ case "3":
+ return "Cloudy"
+ case "4":
+ return "Tungsten"
+ case "5":
+ return "Fluorescent"
+ case "6":
+ return "Flash"
+ default:
+ return "Unknown"
+ }
+}
+
+func formatWhiteBalance(wb string) string {
+ switch wb {
+ case "0", "":
+ return "Auto"
+ case "1":
+ return "Manual"
+ case "2":
+ return "Daylight"
+ case "3":
+ return "Cloudy"
+ case "4":
+ return "Tungsten"
+ case "5":
+ return "Fluorescent"
+ case "6":
+ return "Flash"
+ default:
+ return "Unknown"
+ }
+}
+
+func formatColorSpace(val string) string {
+ cs, _ := strconv.ParseUint(val, 16, 16)
+ switch cs {
+ case 0x1:
+ return "sRGB"
+ case 0x2:
+ return "Adobe RGB"
+ case 0xfffd:
+ return "Wide Gamut RGB"
+ case 0xfffe:
+ return "ICC Profile"
+ case 0xffff:
+ return "Uncalibrated"
+ default:
+ return "Unknown"
+ }
+}
+
+func formatMeteringMode(val string) string {
+ mm, _ := strconv.ParseUint(val, 16, 8)
+ switch mm {
+ case 0x1:
+ return "Average"
+ case 0x2:
+ return "Center Weighted Average"
+ case 0x3:
+ return "Spot"
+ case 0x4:
+ return "Multi Spot"
+ case 0x5:
+ return "Pattern"
+ case 0x6:
+ return "Partial"
+ case 0x7:
+ return "Other"
+ default:
+ return "Unknown"
+ }
+}
+
+func setExifTag(rootIB *exif.IfdBuilder, ifdPath, tagName, tagValue string) error {
+ ifdIb, err := exif.GetOrCreateIbFromRootIb(rootIB, ifdPath)
+ if err != nil {
+ return fmt.Errorf("failed to get or create IB: %v", err)
+ }
+
+ if err := ifdIb.SetStandardWithName(tagName, tagValue); err != nil {
+ return fmt.Errorf("failed to set tag", err)
+ }
+ return nil
+}
+
+func constructExifBuilder() (*exif.IfdBuilder, error) {
+ im, err := exifcommon.NewIfdMappingWithStandard()
+ if err != nil {
+ return nil, fmt.Errorf("failed to create new IFD mapping with standard tags: %v", err)
+ }
+ ti := exif.NewTagIndex()
+ if err := exif.LoadStandardTags(ti); err != nil {
+ return nil, fmt.Errorf("failed to load standard tags: %v", err)
+ }
+
+ rootIb := exif.NewIfdBuilder(im, ti, exifcommon.IfdStandardIfdIdentity,
+ exifcommon.EncodeDefaultByteOrder)
+ rootIb.AddStandardWithName("ProcessingSoftware", "photos-uploader")
+ return rootIb, nil
+}
+
+func (pic *Picture) UpdateExifTags() error {
+ parser := jpeg.NewJpegMediaParser()
+ intfc, err := parser.ParseFile(pic.Path)
+ if err != nil {
+ return fmt.Errorf("failed to parse JPEG file: %v", err)
+ }
+
+ sl := intfc.(*jpeg.SegmentList)
+
+ rootIb, err := sl.ConstructExifBuilder()
+ if err != nil {
+ rootIb, err = constructExifBuilder()
+ if err != nil {
+ return fmt.Errorf("failed to construct EXIF builder: %v", err)
+ }
+ }
+
+ if err := setExifTag(rootIb, "IFD0", "ImageDescription", pic.Caption); err != nil {
+ return fmt.Errorf("failed to set tag: %v", err)
+ }
+
+ if err := setExifTag(rootIb, "IFD0", "DocumentName", pic.Name); err != nil {
+ return fmt.Errorf("failed to set tag: %v", err)
+ }
+
+ if err := sl.SetExif(rootIb); err != nil {
+ return fmt.Errorf("failed to set EXIF to jpeg: %v", err)
+ }
+ b := new(bytes.Buffer)
+ if err := sl.Write(b); err != nil {
+ return fmt.Errorf("failed to create JPEG data: %v", err)
+ }
+ if err := os.WriteFile(pic.Path, b.Bytes(), 0644); err != nil {
+ return fmt.Errorf("failed to write JPEG file: %v", err)
+ }
+ return nil
+}
diff --git a/pkg/datastore/album.go b/pkg/datastore/album.go
new file mode 100644
index 0000000..7782df6
--- /dev/null
+++ b/pkg/datastore/album.go
@@ -0,0 +1,161 @@
+package datastore
+
+import (
+ "fmt"
+ "gogallery/pkg/config"
+ "os"
+ "path/filepath"
+ "sort"
+ "sync"
+ "time"
+
+ "gorm.io/gorm"
+ "gorm.io/gorm/clause"
+)
+
+type Album struct {
+ Id string `gorm:"primaryKey;size:64" json:"id"`
+ Name string `gorm:"column:name" json:"name"`
+ ModTime time.Time `gorm:"column:mod_time" json:"mod_time"`
+ Parent string `gorm:"column:parent" json:"parent"`
+ ParentPath string `gorm:"column:parent_path" json:"parentPath,omitempty"`
+ Path string `gorm:"column:path" json:"path,omitempty"`
+ ProfileId string `gorm:"column:profile_id" json:"profile_image"`
+}
+
+type AlbumCollection struct {
+ DB *gorm.DB
+ sync.Mutex
+}
+
+func NewAlbumCollection(db *gorm.DB) *AlbumCollection {
+ db.AutoMigrate(&Album{}) // Use the correct struct for migration
+ albumCollection := &AlbumCollection{DB: db}
+ return albumCollection
+}
+
+// Save or update an album (upsert by primary key)
+func (c *AlbumCollection) Save(album Album) error {
+ c.Lock()
+ defer c.Unlock()
+ return c.DB.Save(&album).Error
+}
+
+func (c *AlbumCollection) BatchInsert(albums []Album) error {
+ err := c.DB.Clauses(clause.OnConflict{
+ Columns: []clause.Column{{Name: "id"}}, // primary key
+ UpdateAll: true, // update all fields on conflict
+ }).CreateInBatches(albums, 100).Error
+ return err
+}
+
+// Update fields of an album by ID
+func (c *AlbumCollection) Update(id string, updates Album) error {
+ c.Lock()
+ defer c.Unlock()
+ return c.DB.Model(&Album{}).Where("id = ?", id).Updates(updates).Error
+}
+
+// Get all albums
+func (c *AlbumCollection) GetAll() ([]Album, error) {
+ var albums []Album
+ if err := c.DB.Find(&albums).Error; err != nil {
+ return nil, err
+ }
+ return albums, nil
+}
+
+// Find album by ID
+func (c *AlbumCollection) FindById(id string) (Album, error) {
+ var album Album
+ if err := c.DB.First(&album, "id = ?", id).Error; err != nil {
+ return album, err
+ }
+ return album, nil
+}
+
+// Find albums by field (simple string fields)
+func (c *AlbumCollection) FindByField(field, value string) ([]Album, error) {
+ var albums []Album
+ if err := c.DB.Where(field+" = ?", value).Find(&albums).Error; err != nil {
+ return nil, err
+ }
+ return albums, nil
+}
+func (c *AlbumCollection) Reset() error {
+ c.Lock()
+ defer c.Unlock()
+ if err := c.DB.Exec("DELETE FROM albums").Error; err != nil {
+ return fmt.Errorf("failed to reset albums: %w", err)
+ }
+ // Optionally, you can also reset the auto-increment ID
+ if err := c.DB.Exec("DELETE FROM sqlite_sequence WHERE name='albums'").Error; err != nil {
+ return fmt.Errorf("failed to reset auto-increment ID: %w", err)
+ }
+ return nil
+}
+
+func (c *AlbumCollection) GetAlbumStructure(config config.GalleryConfiguration) AlbumStrcure {
+ albums, _ := c.GetAll()
+ for _, alb := range albums {
+ if IsAlbumInBlacklist(alb.Name) {
+ albums = RemoveAlbumFromSlice(albums, alb)
+ }
+ }
+ newalbms := SliceToTree(albums, config.Basepath)
+ return newalbms
+}
+
+func (c *AlbumCollection) FindLatestInAlbum(id string) (Picture, error) {
+ c.Lock()
+ defer c.Unlock()
+ var pic Picture
+ err := c.DB.Model(&Picture{}).
+ Where("album = ?", id).
+ Order("date_taken DESC").
+ First(&pic).Error
+ if err != nil {
+ return pic, err
+ }
+ return pic, nil
+}
+
+func (c *AlbumCollection) GetLatestAlbums() ([]Album, error) {
+ albums, err := c.GetAll()
+ if err != nil {
+ return nil, err
+ }
+ for i, album := range albums {
+ if pic, err := c.FindLatestInAlbum(album.Id); err == nil {
+ albums[i].ModTime = pic.DateTaken
+ } else {
+ albums[i].ModTime = time.Time{} // Set to zero time if no picture found
+ }
+ }
+ sort.Slice(albums, func(i, j int) bool {
+ return albums[i].ModTime.After(albums[j].ModTime)
+ })
+ return albums, err
+}
+
+func (a *AlbumCollection) MovePictureToAlbum(picture Picture, newAlbum string) error {
+ album, _ := a.FindById(newAlbum)
+ newName := fmt.Sprintf("%s/%s/%s%s", album.ParentPath, album.Name, picture.Name, filepath.Ext(picture.Path))
+ err := os.Rename(picture.Path, newName)
+ if err != nil {
+ return err
+ }
+ picture.Path = newName
+ picture.Album = newAlbum
+ return nil
+}
+
+// RemoveAlbumFromSlice removes the first occurrence of the album with the same ID from the slice
+func RemoveAlbumFromSlice(albums []Album, target Album) []Album {
+ for i, alb := range albums {
+ if alb.Id == target.Id {
+ return append(albums[:i], albums[i+1:]...)
+ }
+ }
+ return albums
+}
diff --git a/pkg/datastore/album_structure.go b/pkg/datastore/album_structure.go
new file mode 100644
index 0000000..d5e781a
--- /dev/null
+++ b/pkg/datastore/album_structure.go
@@ -0,0 +1,141 @@
+package datastore
+
+import (
+ "gogallery/pkg/config"
+ "path"
+ "sort"
+ "strings"
+)
+
+type AlbumNode struct {
+ Album
+ Children AlbumStrcure `json:"children"`
+}
+
+func (a Album) ToAlbumNode() AlbumNode {
+ return AlbumNode{
+ Album: a,
+ Children: make(AlbumStrcure),
+ }
+}
+
+type AlbumStrcure = map[string]AlbumNode
+
+func SliceToTree(albms []Album, basepath string) AlbumStrcure {
+ newalbms := initializeAlbumNodes(albms, basepath)
+ processChildAlbums(albms, basepath, newalbms)
+ setParentProfileImages(newalbms)
+ return newalbms
+}
+
+func initializeAlbumNodes(albms []Album, basepath string) map[string]AlbumNode {
+ newalbms := make(map[string]AlbumNode)
+ sort.Slice(albms, func(i, j int) bool {
+ return albms[i].ParentPath < albms[j].ParentPath
+ })
+ for _, ab := range albms {
+ if ab.ParentPath == basepath {
+ ab.ParentPath = ""
+ newalbms[ab.Name] = ab.ToAlbumNode()
+ }
+ }
+ return newalbms
+}
+
+func processChildAlbums(albms []Album, basepath string, newalbms map[string]AlbumNode) {
+ for _, ab := range albms {
+ if (ab.ParentPath != basepath) && (ab.Id != config.GetMD5Hash(basepath)) {
+ updateAlbumHierarchy(ab, basepath, newalbms)
+ }
+ }
+}
+
+func updateAlbumHierarchy(ab Album, basepath string, newalbms map[string]AlbumNode) {
+ s := strings.Split(strings.Replace(ab.ParentPath, basepath, "", 1), "/")
+ copy(s, s[1:])
+ s = s[:len(s)-1]
+ pth := basepath
+ var alb AlbumNode
+ for i, p := range s {
+ if i == 0 {
+ alb = newalbms[p]
+ } else {
+ alb = alb.Children[p]
+ }
+ pth = path.Join(pth, p)
+ if i == len(s)-1 {
+ if alb.Children != nil {
+ ab.ParentPath = ""
+ alb.Children[ab.Name] = ab.ToAlbumNode()
+ }
+ }
+ }
+}
+
+func FindInAlbumStrcureById(ab AlbumNode, Id string) AlbumNode {
+ if ab.Id == Id {
+ return ab
+ }
+ for _, v := range ab.Children {
+ a := FindInAlbumStrcureById(v, Id)
+ if a.Id == Id {
+ return a
+ }
+ }
+ return AlbumNode{}
+}
+
+func GetAlbmusFromTree(as AlbumStrcure) []AlbumNode {
+ albumList := make([]AlbumNode, 0)
+ for _, v := range as {
+ albumList = append(albumList, v)
+ }
+ sort.Slice(albumList, func(i, j int) bool {
+ return strings.ToLower(albumList[i].Name) < strings.ToLower(albumList[j].Name)
+ })
+ return albumList
+}
+
+func GetAlbumFromStructure(as AlbumStrcure, Id string) AlbumNode {
+ album := AlbumNode{}
+ for _, v := range as {
+ album = FindInAlbumStrcureById(v, Id)
+ if album.Id != "" {
+ return album
+ }
+ }
+ return album
+}
+
+func SortByTime(albs []Album) []Album {
+ sort.Slice(albs, func(i, j int) bool {
+ return albs[i].ModTime.After(albs[j].ModTime)
+ })
+ return albs
+}
+
+// Recursively set profile image for parent albums if not set, using a child album's profile image
+func setParentProfileImages(tree AlbumStrcure) {
+ for key, node := range tree {
+ if node.ProfileId == "" && len(node.Children) > 0 {
+ node.ProfileId = setProfileImageRecursive(&node)
+ tree[key] = node // Update the node in the map
+ }
+ }
+}
+
+func setProfileImageRecursive(node *AlbumNode) string {
+ // If already has a profile image, return it
+ if node.ProfileId != "" {
+ return node.ProfileId
+ }
+ // Try to get from children
+ for _, child := range node.Children {
+ childProfile := setProfileImageRecursive(&child)
+ if childProfile != "" {
+ node.ProfileId = childProfile
+ return childProfile
+ }
+ }
+ return ""
+}
diff --git a/pkg/datastore/database.go b/pkg/datastore/database.go
new file mode 100644
index 0000000..00f38f9
--- /dev/null
+++ b/pkg/datastore/database.go
@@ -0,0 +1,61 @@
+package datastore
+
+import (
+ "gogallery/pkg/monitor"
+
+ "gorm.io/driver/sqlite"
+ "gorm.io/gorm"
+ "gorm.io/gorm/logger"
+)
+
+type DataStore struct {
+ DB *gorm.DB
+ Pictures *PictureCollection
+ Albums *AlbumCollection
+ ImageCache *ImageCache
+ Monitor monitor.Monitor
+}
+
+func Open(path string, monitor monitor.Monitor) (*DataStore, error) {
+ db, err := gorm.Open(sqlite.Open("gogallery.sql.db"), &gorm.Config{
+ Logger: logger.Default.LogMode(logger.Silent),
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return &DataStore{
+ DB: db,
+ Pictures: NewPictureCollection(db),
+ Albums: NewAlbumCollection(db),
+ ImageCache: NewImageCache(),
+ Monitor: monitor,
+ }, nil
+}
+
+func (d *DataStore) GetLatestAlbum() string {
+ pics := d.Pictures.GetFilteredPictures(false)
+ latests := pics[0].DateTaken
+ album := pics[0].Album
+ for _, p := range pics {
+ if p.DateTaken.After(latests) {
+ latests = p.DateTaken
+ album = p.Album
+ }
+ }
+ return album
+}
+
+func (d *DataStore) Reset() {
+ d.Pictures.Reset()
+ d.Albums.Reset()
+ d.ImageCache.Reset()
+}
+
+func (d *DataStore) GetTasks() []monitor.MonitorStat {
+ return d.Monitor.GetTasks()
+}
+
+func (d *DataStore) NewTask(name string, total int) monitor.MonitorStat {
+ return d.Monitor.NewTask(name, total)
+}
diff --git a/backend/datastore/fileUtils.go b/pkg/datastore/fileUtils.go
similarity index 91%
rename from backend/datastore/fileUtils.go
rename to pkg/datastore/fileUtils.go
index cdce68a..9326e05 100644
--- a/backend/datastore/fileUtils.go
+++ b/pkg/datastore/fileUtils.go
@@ -2,18 +2,15 @@ package datastore
import (
"fmt"
+ "gogallery/pkg/config"
"io"
"os"
"path/filepath"
"strings"
"time"
-
- "github.com/robrotheram/gogallery/backend/config"
)
var validExtension = []string{"jpg", "png", "gif"}
-var IsScanning bool
-var gConfig = config.Config.Gallery
type FileInfo struct {
Name string `json:"name"`
@@ -24,7 +21,7 @@ type FileInfo struct {
}
// Helper function to create a local FileInfo struct from os.FileInfo interface.
-func fileInfoFromInterface(v os.FileInfo) FileInfo {
+func FileInfoFromInterface(v os.FileInfo) FileInfo {
return FileInfo{v.Name(), v.Size(), v.Mode(), v.ModTime(), v.IsDir()}
}
@@ -36,7 +33,7 @@ type Node struct {
Parent *Node `json:"-"`
}
-func checkEXT(path string) bool {
+func CheckEXT(path string) bool {
chk := false
for _, ext := range validExtension {
if strings.ToLower(filepath.Ext(path)) == "."+ext {
@@ -65,7 +62,7 @@ func RemoveContents(dir string) error {
return nil
}
-func contains(s []string, e string) bool {
+func Contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
diff --git a/pkg/datastore/file_scanner.go b/pkg/datastore/file_scanner.go
new file mode 100644
index 0000000..922c94e
--- /dev/null
+++ b/pkg/datastore/file_scanner.go
@@ -0,0 +1,159 @@
+package datastore
+
+import (
+ "gogallery/pkg/config"
+ "log"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "sync"
+ "time"
+
+ "gorm.io/gorm"
+)
+
+func createPicture(name string, path string, album string) Picture {
+ p := Picture{
+ Id: config.GetMD5Hash(path),
+ Name: name,
+ Path: path,
+ Ext: filepath.Ext(path),
+ Album: config.GetMD5Hash(filepath.Dir(path)),
+ AlbumName: album,
+ RootPath: config.Config.Gallery.Basepath,
+ PostedToIG: false,
+ Visibility: "PUBLIC",
+ DateAdded: time.Now(),
+ DateModified: time.Now(),
+ }
+ // p.CreateExif()
+ return p
+}
+
+func createAlbum(fInfo os.FileInfo, path string) Album {
+ info := FileInfoFromInterface(fInfo)
+ albumId := config.GetMD5Hash(path)
+ return Album{
+ Id: albumId,
+ Name: info.Name,
+ ModTime: info.ModTime,
+ Parent: filepath.Base(filepath.Dir(path)),
+ ParentPath: (filepath.Dir(path)),
+ }
+}
+
+type albumUpdate struct {
+ AlbumId string
+ ProfileId string
+}
+
+func (db *DataStore) updateExif(pics []Picture) {
+ stat := db.Monitor.NewTask("Update Exif Data", len(pics))
+ defer stat.Complete()
+ workerCount := runtime.NumCPU()
+ jobs := make(chan *Picture, len(pics))
+ var wg sync.WaitGroup
+
+ // Start worker pool
+ for i := 0; i < workerCount; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for pic := range jobs {
+ pic.CreateExif()
+ stat.Update()
+ }
+ }()
+ }
+
+ // Send jobs to workers
+ for i := range pics {
+ jobs <- &pics[i]
+ }
+ close(jobs)
+ wg.Wait()
+}
+
+func (db *DataStore) ScanPath(path string) error {
+ cfg := config.Config.Gallery
+
+ log.Println("Scanning Folders at:" + path)
+ absRoot, err := filepath.Abs(path)
+ if err != nil {
+ return err
+ }
+
+ pictures, albums, albumUpdates, err := db.walkPath(absRoot, cfg)
+ if err != nil {
+ log.Printf("Error walking path %s: %v", absRoot, err)
+ return err
+ }
+
+ log.Println("Start processing Exif data for pictures")
+ db.updateExif(pictures)
+ // log.Println("Exif data processing complete")
+
+ db.Pictures.BatchInsert(pictures)
+ db.Albums.BatchInsert(albums)
+ db.updateAlbumProfiles(albumUpdates)
+
+ // log.Println("Scanning Complete")
+ return nil
+}
+
+func (db *DataStore) walkPath(absRoot string, cfg config.GalleryConfiguration) ([]Picture, []Album, []albumUpdate, error) {
+ pictures := []Picture{}
+ albums := []Album{}
+ albumUpdates := []albumUpdate{}
+
+ walkFunc := func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ if CheckEXT(path) && !info.IsDir() {
+ db.processFile(path, info, &pictures, &albumUpdates)
+ }
+ if info.IsDir() {
+ db.processDirectory(path, info, cfg, &albums)
+ }
+ return nil
+ }
+
+ err := filepath.Walk(absRoot, walkFunc)
+ return pictures, albums, albumUpdates, err
+}
+
+func (db *DataStore) processFile(path string, info os.FileInfo, pictures *[]Picture, albumUpdates *[]albumUpdate) {
+ albumName := filepath.Base(filepath.Dir(path))
+ picName := strings.TrimSuffix(info.Name(), filepath.Ext(info.Name()))
+ if !IsAlbumInBlacklist(albumName) && !IsPictureInBlacklist(picName) {
+ p := createPicture(picName, path, albumName)
+ *pictures = append(*pictures, p)
+ *albumUpdates = append(*albumUpdates, albumUpdate{
+ AlbumId: p.Album,
+ ProfileId: p.Id,
+ })
+ }
+}
+
+func (db *DataStore) processDirectory(path string, info os.FileInfo, cfg config.GalleryConfiguration, albums *[]Album) {
+ if !IsAlbumInBlacklist(info.Name()) {
+ if filepath.Base(filepath.Dir(path)) != cfg.Basepath {
+ *albums = append(*albums, createAlbum(info, path))
+ }
+ }
+}
+
+func (db *DataStore) updateAlbumProfiles(albumUpdates []albumUpdate) {
+ if len(albumUpdates) > 0 {
+ db.Albums.DB.Transaction(func(tx *gorm.DB) error {
+ for _, au := range albumUpdates {
+ if err := tx.Model(&Album{}).Where("id = ?", au.AlbumId).Update("profile_id", au.ProfileId).Error; err != nil {
+ log.Printf("Album profile_id update failed for %s: %v", au.AlbumId, err)
+ }
+ }
+ return nil
+ })
+ }
+}
diff --git a/pkg/datastore/gps.go b/pkg/datastore/gps.go
new file mode 100644
index 0000000..56c60f1
--- /dev/null
+++ b/pkg/datastore/gps.go
@@ -0,0 +1,47 @@
+package datastore
+
+import "math"
+
+const (
+ LatMax = 90
+ LngMax = 180
+)
+
+func NormalizeGPS(lat, lng float64) (float64, float64) {
+
+ if lat < LatMax || lat > LatMax || lng < LngMax || lng > LngMax {
+ // Clip the latitude. Normalise the longitude.
+ lat, lng = clipLat(lat), normalizeLng(lng)
+ }
+
+ return lat, lng
+}
+func clipLat(lat float64) float64 {
+ if lat > LatMax*2 {
+ return math.Mod(lat, LatMax)
+ } else if lat > LatMax {
+ return lat - LatMax
+ }
+
+ if lat < -LatMax*2 {
+ return math.Mod(lat, LatMax)
+ } else if lat < -LatMax {
+ return lat + LatMax
+ }
+
+ return lat
+}
+
+func normalizeLng(value float64) float64 {
+ return normalizeCoord(value, LngMax)
+}
+
+func normalizeCoord(value, max float64) float64 {
+ for value < -max {
+ value += 2 * max
+ }
+ for value >= max {
+ value -= 2 * max
+ }
+ return value
+}
diff --git a/pkg/datastore/image_cache.go b/pkg/datastore/image_cache.go
new file mode 100644
index 0000000..622655d
--- /dev/null
+++ b/pkg/datastore/image_cache.go
@@ -0,0 +1,64 @@
+package datastore
+
+import (
+ "fmt"
+ "gogallery/pkg/config"
+ "os"
+ "path"
+)
+
+type ImageCache struct {
+ base string
+}
+
+func NewImageCache() *ImageCache {
+ path := path.Join(os.TempDir(), "gogallery")
+ os.MkdirAll(path, 0755)
+ return &ImageCache{
+ base: path,
+ }
+}
+
+func extension(encodeType config.ImageType) string {
+ switch encodeType {
+ case config.JPEG:
+ return "jpg"
+ case config.WebP:
+ return "webp"
+ default:
+ return "jpg" // Default to JPEG if type is unknown
+ }
+}
+
+func (ic *ImageCache) Get(name string, encodeType config.ImageType, size string) (*os.File, error) {
+
+ file, err := os.Open(path.Join(ic.base, fmt.Sprintf("%s-%s.%s", name, size, extension(encodeType))))
+ if err != nil {
+ return nil, fmt.Errorf("file not found: %s-%s.%s", name, size, extension(encodeType))
+ }
+
+ stat, err := file.Stat()
+ if err != nil {
+ file.Close()
+ return nil, fmt.Errorf("could not retrieve file info: %v", err)
+ }
+
+ if stat.Size() == 0 {
+ file.Close()
+ return nil, fmt.Errorf("file is empty: %s-%s.%s", name, size, extension(encodeType))
+ }
+
+ return file, nil
+}
+func (ic *ImageCache) Writer(name string, encodeType config.ImageType, size string) (*os.File, error) {
+ return os.Create(path.Join(ic.base, fmt.Sprintf("%s-%s.%s", name, size, extension(encodeType))))
+}
+
+func (ic *ImageCache) Reset() {
+ if err := os.RemoveAll(ic.base); err != nil {
+ fmt.Printf("Failed to reset image cache: %v\n", err)
+ }
+ if err := os.MkdirAll(ic.base, 0755); err != nil {
+ fmt.Printf("Failed to recreate image cache directory: %v\n", err)
+ }
+}
diff --git a/pkg/datastore/pictures.go b/pkg/datastore/pictures.go
new file mode 100644
index 0000000..8b93450
--- /dev/null
+++ b/pkg/datastore/pictures.go
@@ -0,0 +1,170 @@
+package datastore
+
+import (
+ "fmt"
+ "image"
+ "os"
+ "sync"
+ "time"
+
+ "gorm.io/gorm"
+ "gorm.io/gorm/clause"
+)
+
+type PictureCollection struct {
+ DB *gorm.DB
+ sync.Mutex
+}
+
+type Picture struct {
+ Id string `gorm:"primaryKey;size:64" json:"id"`
+ Name string `gorm:"size:255" json:"name"`
+ Caption string `gorm:"size:255" json:"caption"`
+ Path string `gorm:"size:1024" json:"path,omitempty"`
+ Ext string `gorm:"size:32" json:"extention,omitempty"`
+ FormatTime string `gorm:"size:64" json:"format_time"`
+ Album string `gorm:"size:64" json:"album"`
+ AlbumName string `gorm:"size:255" json:"album_name"`
+ RootPath string `gorm:"size:1024" json:"root_path,omitempty"`
+ // Flattened Exif fields
+ FStop string `gorm:"size:32" json:"f_stop"`
+ FocalLength string `gorm:"size:32" json:"focal_length"`
+ ShutterSpeed string `gorm:"size:32" json:"shutter_speed"`
+ ISO string `gorm:"size:32" json:"iso"`
+ Dimension string `gorm:"size:32" json:"dimension"`
+ AspectRatio float32 `json:"aspect_ratio"`
+ Camera string `gorm:"size:255" json:"camera"`
+ LensModel string `gorm:"size:255" json:"lens_model"`
+ DateTaken time.Time `json:"date_taken"`
+ // GPS coordinates
+ GPSLat float64 `json:"gps_latitude"`
+ GPSLng float64 `json:"gps_longitude"`
+ GPSAltitude float64 `json:"gps_altitude,omitempty"`
+ GPSTimestamp time.Time `json:"gps_timestamp,omitempty"`
+ // Additional metadata
+ FileFormat string `gorm:"size:32" json:"file_format"`
+ Software string `gorm:"size:255" json:"software"`
+ ColorSpace string `gorm:"size:32" json:"color_space"`
+
+ MeteringMode string `gorm:"size:32" json:"metering_mode"`
+ WhiteBalance string `gorm:"size:32" json:"white_balance,omitempty"`
+ Saturation string `gorm:"size:32" json:"saturation,omitempty"`
+ Contrast string `gorm:"size:32" json:"contrast,omitempty"`
+ Sharpness string `gorm:"size:32" json:"sharpness,omitempty"`
+ Temperature string `gorm:"size:32" json:"temperature,omitempty"`
+
+ // Flattened Meta fields
+ PostedToIG bool `json:"posted_to_IG,omitempty"`
+ Visibility string `gorm:"size:32" json:"visibility,omitempty"`
+ DateAdded time.Time `json:"date_added,omitempty"`
+ DateModified time.Time `json:"date_modified,omitempty"`
+}
+
+func NewPictureCollection(db *gorm.DB) *PictureCollection {
+ db.AutoMigrate(&Picture{}) // Use the correct struct for migration
+ pictureCollection := &PictureCollection{DB: db}
+ return pictureCollection
+}
+
+func (p *PictureCollection) Save(pic Picture) error {
+ p.Lock()
+ defer p.Unlock()
+ return p.DB.Create(pic).Error
+}
+
+func (p *PictureCollection) Reset() error {
+ p.Lock()
+ defer p.Unlock()
+ // Delete all pictures from the database
+ if err := p.DB.Exec("DELETE FROM pictures").Error; err != nil {
+ return fmt.Errorf("failed to reset pictures: %w", err)
+ }
+ // Optionally, you can also reset the auto-increment ID
+ if err := p.DB.Exec("DELETE FROM sqlite_sequence WHERE name='pictures'").Error; err != nil {
+ return fmt.Errorf("failed to reset auto-increment ID: %w", err)
+ }
+ return nil
+}
+
+// Update fields of an album by ID
+func (p *PictureCollection) Update(id string, updates Picture) error {
+ p.Lock()
+ defer p.Unlock()
+ updates.UpdateExifTags()
+ return p.DB.Model(&Picture{}).Where("id = ?", id).Updates(updates).Error
+}
+
+func (p *PictureCollection) BatchInsert(pics []Picture) error {
+ err := p.DB.Clauses(clause.OnConflict{
+ Columns: []clause.Column{{Name: "id"}}, // primary key
+ UpdateAll: false, // update all fields on conflict
+ }).CreateInBatches(pics, 100).Error
+ return err
+}
+
+// GetAll returns all pictures as domain models
+func (p *PictureCollection) GetAll() ([]Picture, error) {
+ var dbModels []Picture
+ if err := p.DB.Order("date_taken desc").Find(&dbModels).Error; err != nil {
+ return nil, err
+ }
+ return dbModels, nil
+}
+
+// FindByID returns a picture by its ID as a domain model
+func (p *PictureCollection) FindById(id string) (Picture, error) {
+ var dbModel Picture
+ if err := p.DB.First(&dbModel, "id = ?", id).Error; err != nil {
+ return dbModel, err
+ }
+ return dbModel, nil
+}
+
+func (p *PictureCollection) FindLatestInAlbum(album string) (Picture, error) {
+ var pic Picture
+ err := p.DB.Where("Album = ?", album).Order("date_taken desc").First(&pic).Error
+ return pic, err
+}
+
+// FindByField returns all pictures where a field matches a value (simple string fields)
+func (p *PictureCollection) FindByField(field, value string) ([]Picture, error) {
+ var dbModels []Picture
+ if err := p.DB.Where(field+" = ?", value).Find(&dbModels).Error; err != nil {
+ return nil, err
+ }
+ return dbModels, nil
+}
+
+func (p *PictureCollection) GetFilteredPictures(admin bool) []Picture {
+ var filterPics []Picture
+ pictures, _ := p.GetAll()
+ for _, pic := range pictures {
+ if admin {
+ filterPics = append(filterPics, pic)
+ } else if !IsAlbumInBlacklist(pic.Album) && pic.Visibility == "PUBLIC" {
+ filterPics = append(filterPics, pic)
+ }
+ }
+ return (filterPics)
+}
+
+func (p *PictureCollection) Delete(picture Picture) error {
+ p.Lock()
+ defer p.Unlock()
+ os.Remove(picture.Path)
+ p.DB.Delete(picture)
+ return nil
+}
+
+func (p *Picture) Load() (image.Image, error) {
+ f, err := os.Open(p.Path)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+ img, _, err := image.Decode(f)
+ if err != nil {
+ return nil, fmt.Errorf("image %s, decode failed: %v", p.Path, err)
+ }
+ return img, nil
+}
diff --git a/backend/deploy/netifly.go b/pkg/deploy/netifly.go
similarity index 89%
rename from backend/deploy/netifly.go
rename to pkg/deploy/netifly.go
index 4ffd08c..49695b4 100644
--- a/backend/deploy/netifly.go
+++ b/pkg/deploy/netifly.go
@@ -3,6 +3,8 @@ package deploy
import (
"context"
"fmt"
+ "gogallery/pkg/config"
+ "gogallery/pkg/monitor"
"time"
oapiclient "github.com/go-openapi/runtime/client"
@@ -10,16 +12,15 @@ import (
models "github.com/netlify/open-api/v2/go/models"
porcelain "github.com/netlify/open-api/v2/go/porcelain"
ooapicontext "github.com/netlify/open-api/v2/go/porcelain/context"
- "github.com/robrotheram/gogallery/backend/config"
- "github.com/robrotheram/gogallery/backend/monitor"
"github.com/sirupsen/logrus"
)
-func DeploySite(c config.Configuration, stats *monitor.ProgressStats) error {
+func DeploySite(c config.Configuration, stats monitor.MonitorStat) error {
if len(c.Deploy.SiteId) == 0 || len(c.Deploy.AuthToken) == 0 {
return fmt.Errorf("no deployment config found")
}
stats.Start()
+ defer stats.Complete()
logger := logrus.New()
logger.SetLevel(logrus.FatalLevel)
client := porcelain.NewRetryableHTTPClient(strfmt.NewFormats(), 10)
@@ -49,15 +50,15 @@ func DeploySite(c config.Configuration, stats *monitor.ProgressStats) error {
} else if resp.DeployURL != "" {
fmt.Println("Site avalible: " + resp.DeployURL)
}
- stats.End()
+
return nil
}
type DeployObserver struct {
- stats *monitor.ProgressStats
+ stats monitor.MonitorStat
}
-func NewDeployObserver(stats *monitor.ProgressStats) *DeployObserver {
+func NewDeployObserver(stats monitor.MonitorStat) *DeployObserver {
return &DeployObserver{
stats: stats,
}
diff --git a/pkg/embeds/embeds.go b/pkg/embeds/embeds.go
new file mode 100644
index 0000000..0388227
--- /dev/null
+++ b/pkg/embeds/embeds.go
@@ -0,0 +1,65 @@
+package embeds
+
+import (
+ "embed"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+var ThemeFS embed.FS
+
+func CopyTheme(templatePath string) {
+ os.MkdirAll(templatePath, os.ModePerm)
+ fs.WalkDir(ThemeFS, ".", func(path string, d fs.DirEntry, err error) error {
+ newPath := filepath.Join(templatePath, path)
+ if d.IsDir() {
+ os.MkdirAll(newPath, os.ModePerm)
+ } else {
+ file, _ := ThemeFS.ReadFile(path)
+ os.WriteFile(newPath, file, os.ModePerm)
+ }
+ return nil
+ })
+
+}
+
+func ListThemes() []string {
+ items, err := ThemeFS.ReadDir("themes")
+ if err != nil {
+ return nil
+ }
+ var pages []string
+ for _, item := range items {
+ name := strings.TrimSuffix(item.Name(), filepath.Ext(item.Name()))
+ pages = append(pages, name)
+ }
+ return pages
+}
+
+func DoesThmeExist(theme string) bool {
+ themes := ListThemes()
+ for _, t := range themes {
+ if t == theme {
+ return true
+ }
+ }
+ return false
+}
+
+func CopyThemeAssets(theme string, templatePath string) {
+ os.MkdirAll(templatePath, os.ModePerm)
+ root := "themes/" + theme + "/assets"
+ fs.WalkDir(ThemeFS, root, func(path string, d fs.DirEntry, err error) error {
+ newPath := filepath.Join(templatePath, strings.Replace(path, root, "", -1))
+ if d.IsDir() {
+ os.MkdirAll(newPath, os.ModePerm)
+ } else {
+ file, _ := ThemeFS.ReadFile(path)
+ os.WriteFile(newPath, file, os.ModePerm)
+ }
+ return nil
+ })
+
+}
diff --git a/pkg/monitor/cmd_monitor .go b/pkg/monitor/cmd_monitor .go
new file mode 100644
index 0000000..33d408b
--- /dev/null
+++ b/pkg/monitor/cmd_monitor .go
@@ -0,0 +1,96 @@
+package monitor
+
+import (
+ "fmt"
+ "sort"
+
+ "github.com/gosuri/uiprogress"
+ "github.com/gosuri/uiprogress/util/strutil"
+)
+
+type CmdMonitor struct {
+ Tasks map[string]*CmdTask
+ done chan bool
+}
+
+type CmdTask struct {
+ *ProgressStats
+ *uiprogress.Bar
+}
+
+func NewCmdTask(name string, total int) *CmdTask {
+ stat := NewProgressStats(name, total)
+ bar := uiprogress.AddBar(total).AppendCompleted()
+ bar.PrependFunc(func(b *uiprogress.Bar) string {
+ return strutil.Resize(name, 20)
+ })
+ bar.AppendFunc(func(b *uiprogress.Bar) string {
+ return fmt.Sprintf("%d/%d", uint(b.Current()), uint(b.Total))
+ })
+ return &CmdTask{
+ ProgressStats: stat,
+ Bar: bar,
+ }
+}
+func (t *CmdTask) Start() {
+ t.ProgressStats.Start()
+ if t.Bar != nil {
+ t.Bar.Set(t.GetProcessed())
+ }
+}
+func (t *CmdTask) Update() {
+ t.ProgressStats.Update()
+ if t.Bar != nil {
+ t.Bar.Set(t.GetProcessed())
+ }
+}
+
+func (t *CmdTask) Fail(string) {
+ if t.Bar != nil {
+ t.Bar.Set(t.GetProcessed())
+ }
+}
+
+func (t *CmdTask) Complete() {
+ t.ProgressStats.Complete()
+ if t.Bar != nil {
+ t.Bar.Set(t.GetProcessed())
+ }
+}
+
+func NewCMDMonitor() *CmdMonitor {
+ return &CmdMonitor{
+ Tasks: make(map[string]*CmdTask),
+ done: make(chan bool),
+ }
+}
+
+func (t *CmdMonitor) NewTask(name string, total int) MonitorStat {
+ if stat, exists := t.Tasks[name]; exists {
+ return stat
+ }
+ stat := NewCmdTask(name, total)
+ t.Tasks[name] = stat
+ return stat
+}
+
+func (t *CmdMonitor) GetTasks() []MonitorStat {
+ keys := make([]string, 0, len(t.Tasks))
+ values := make([]MonitorStat, 0, len(t.Tasks))
+ for k := range t.Tasks {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+
+ for _, k := range keys {
+ values = append(values, t.Tasks[k])
+ }
+ return values
+}
+
+func (t *CmdMonitor) StartUpdater() {
+ uiprogress.Start() // Start the renderer in a separate goroutine
+}
+func (t *CmdMonitor) StopUpdater() {
+ t.done <- true
+}
diff --git a/pkg/monitor/monitor.go b/pkg/monitor/monitor.go
new file mode 100644
index 0000000..8ced0fc
--- /dev/null
+++ b/pkg/monitor/monitor.go
@@ -0,0 +1,14 @@
+package monitor
+
+type Monitor interface {
+ NewTask(name string, total int) MonitorStat
+ GetTasks() []MonitorStat
+ // Update(name string)
+}
+
+type MonitorStat interface {
+ Start()
+ Update()
+ Complete()
+ Fail(string)
+}
diff --git a/backend/monitor/progress_stats.go b/pkg/monitor/progress_stats.go
similarity index 63%
rename from backend/monitor/progress_stats.go
rename to pkg/monitor/progress_stats.go
index 75020a5..eed3c49 100644
--- a/backend/monitor/progress_stats.go
+++ b/pkg/monitor/progress_stats.go
@@ -1,6 +1,9 @@
package monitor
-import "time"
+import (
+ "sync"
+ "time"
+)
type ProssesState string
@@ -20,6 +23,8 @@ type ProgressStats struct {
Total int `json:"total"`
Proceesed int `json:"processed"`
State ProssesState `json:"state"`
+ Message string `json:"message,omitempty"`
+ mu sync.Mutex `json:"-"`
}
func NewProgressStats(name string, total int) *ProgressStats {
@@ -36,15 +41,33 @@ func (p *ProgressStats) Start() {
p.State = RUNNING
}
-func (p *ProgressStats) Update() {
- p.Proceesed = p.Proceesed + 1
- p.Duration = time.Since(p.StartTime)
+func (p *ProgressStats) Fail(msg string) {
+ p.State = ERROR
+ p.EndTime = time.Now()
+ p.Duration = p.EndTime.Sub(p.StartTime)
+ p.Message = msg
+}
+
+func (s *ProgressStats) Update() {
+ s.mu.Lock()
+ if s.State != RUNNING {
+ s.Start()
+ }
+ s.Proceesed++
+ s.mu.Unlock()
+}
+
+func (s *ProgressStats) GetProcessed() int {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ return s.Proceesed
}
-func (p *ProgressStats) End() {
+func (p *ProgressStats) Complete() {
p.EndTime = time.Now()
p.Duration = p.EndTime.Sub(p.StartTime)
p.State = COMPLETE
+ p.Proceesed = p.Total // Ensure processed is set to total on completion
}
func (p *ProgressStats) Percent() float64 {
diff --git a/backend/monitor/task_monitor.go b/pkg/monitor/task_monitor.go
similarity index 68%
rename from backend/monitor/task_monitor.go
rename to pkg/monitor/task_monitor.go
index 2839fce..f240af9 100644
--- a/backend/monitor/task_monitor.go
+++ b/pkg/monitor/task_monitor.go
@@ -16,24 +16,22 @@ func NewMonitor() *TasksMonitor {
}
}
-func (t *TasksMonitor) NewTask(name string, total int) *ProgressStats {
+func (t *TasksMonitor) NewTask(name string, total int) MonitorStat {
stat := NewProgressStats(name, total)
t.Tasks[name] = stat
return stat
}
-func (t *TasksMonitor) GetTasks() []ProgressStats {
+func (t *TasksMonitor) GetTasks() []MonitorStat {
keys := make([]string, 0, len(t.Tasks))
- values := make([]ProgressStats, 0, len(t.Tasks))
-
+ values := make([]MonitorStat, 0, len(t.Tasks))
for k := range t.Tasks {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
- values = append(values, *t.Tasks[k])
+ values = append(values, t.Tasks[k])
}
-
return values
}
diff --git a/pkg/pipeline/AlbumPagePipeline.go b/pkg/pipeline/AlbumPagePipeline.go
new file mode 100644
index 0000000..75e9ea6
--- /dev/null
+++ b/pkg/pipeline/AlbumPagePipeline.go
@@ -0,0 +1,45 @@
+package pipeline
+
+import (
+ "bufio"
+ "fmt"
+ "gogallery/pkg/datastore"
+ templateengine "gogallery/pkg/templateEngine"
+ "io"
+ "os"
+ "path/filepath"
+
+ "github.com/gosimple/slug"
+)
+
+func (r *RenderPipeline) BuildAlbum(albId string, w io.Writer) {
+ page := templateengine.NewPage(nil)
+
+ albums := r.Albums.GetAlbumStructure(page.Settings)
+ album := datastore.GetAlbumFromStructure(albums, albId)
+
+ page.Album = album
+ page.Images, _ = r.Pictures.FindByField("Album", album.Id)
+
+ profile, _ := r.Pictures.FindById(album.ProfileId)
+
+ page.Picture = templateengine.NewPagePicture(profile)
+ page.SEO.Description = fmt.Sprintf("Album: %s", album.Name)
+ page.SEO.Title = fmt.Sprintf("Album: %s", album.Name)
+ page.SEO.SetImage(profile)
+
+ templateengine.Templates.RenderPage(w, templateengine.CollectionTemplate, page)
+
+}
+
+func (r *RenderPipeline) renderAlbumTemplate() func(alb datastore.Album) error {
+ return func(alb datastore.Album) error {
+ albPath := filepath.Join(albumDir, slug.Make(alb.Id))
+ os.MkdirAll(albPath, os.ModePerm)
+ f, _ := os.Create(filepath.Join(albPath, "index.html"))
+ w := bufio.NewWriter(f)
+ r.BuildAlbum(alb.Id, w)
+ f.Close()
+ return nil
+ }
+}
diff --git a/pkg/pipeline/ImagePipeline.go b/pkg/pipeline/ImagePipeline.go
new file mode 100644
index 0000000..544c61a
--- /dev/null
+++ b/pkg/pipeline/ImagePipeline.go
@@ -0,0 +1,83 @@
+package pipeline
+
+import (
+ "gogallery/pkg/config"
+ "gogallery/pkg/datastore"
+ "image"
+ "image/jpeg"
+ "io"
+ "os"
+ "path/filepath"
+
+ "github.com/bep/gowebp/libwebp"
+ "github.com/bep/gowebp/libwebp/webpoptions"
+ "github.com/disintegration/imaging"
+)
+
+func ProcessImage(src image.Image, size int, encodeType config.ImageType, w io.Writer) {
+ // Use a faster filter for small images, Lanczos for larger ones
+ var filter imaging.ResampleFilter
+ switch {
+ case size <= 350:
+ filter = imaging.Linear // fast, good for thumbnails
+ case size <= 640:
+ filter = imaging.CatmullRom // good quality, faster than Lanczos
+ default:
+ filter = imaging.Lanczos // best for large images
+ }
+ if size > 0 {
+ src = imaging.Resize(src, size, 0, filter)
+ }
+
+ switch encodeType {
+ case config.JPEG:
+ jpeg.Encode(w, src, &jpeg.Options{
+ Quality: 85,
+ })
+ case config.WebP:
+ libwebp.Encode(w, src, webpoptions.EncodingOptions{
+ Quality: 85,
+ EncodingPreset: webpoptions.EncodingPresetPhoto,
+ UseSharpYuv: size == 0, // better color and performance
+ })
+ }
+}
+func ImageGenV2(pic datastore.Picture) error {
+ destPath := filepath.Join(imgDir, pic.Id)
+ os.MkdirAll(destPath, os.ModePerm)
+
+ toRender := map[string]int{}
+ for key, size := range config.ImageSizes {
+ cachePath := filepath.Join(destPath, key+".webp")
+ if !config.FileExists(cachePath) {
+ toRender[key] = size.ImgWidth
+ }
+ }
+
+ if config.Config.Gallery.UseOriginal {
+ orginalPath := filepath.Join(destPath, "original"+pic.Ext)
+ if !config.FileExists(orginalPath) {
+ config.Copy(pic.Path, orginalPath)
+ }
+ }
+
+ if len(toRender) == 0 {
+ return nil
+ }
+
+ src, err := pic.Load()
+ if err != nil {
+ return err
+ }
+
+ for key, size := range toRender {
+ cachePath := filepath.Join(destPath, key+".webp")
+ fo, err := os.Create(cachePath)
+ if err != nil {
+ continue
+ }
+ defer fo.Close()
+ ProcessImage(src, size, config.WebP, fo)
+ }
+ return nil
+}
diff --git a/pkg/pipeline/IndexPagePipeluine.go b/pkg/pipeline/IndexPagePipeluine.go
new file mode 100644
index 0000000..7fb0b5b
--- /dev/null
+++ b/pkg/pipeline/IndexPagePipeluine.go
@@ -0,0 +1,130 @@
+package pipeline
+
+import (
+ "bufio"
+ "fmt"
+ "gogallery/pkg/config"
+ "gogallery/pkg/datastore"
+ "gogallery/pkg/embeds"
+ templateengine "gogallery/pkg/templateEngine"
+ "io"
+ "os"
+ "path/filepath"
+)
+
+func (r *RenderPipeline) BuildIndex(w io.Writer) {
+ imagesPerPage := 24
+ latestAlbumID := r.GetLatestAlbum()
+
+ if r.config.ImagesPerPage == 0 {
+ alb, _ := r.Pictures.FindByField("album", latestAlbumID)
+ imagesPerPage = len(alb) - 2 // reserve 2 images for the featured image and the picture of the day
+ }
+
+ indexPage := templateengine.NewPage(nil)
+ images := r.Pictures.GetFilteredPictures(false)
+
+ featuredImage := images[0]
+ images = images[1:]
+
+ pages := paginateImages(images, imagesPerPage)
+ indexPage.Images = pages[0]
+
+ albums, _ := r.Albums.GetLatestAlbums()
+ indexPage.Albums = make([]datastore.AlbumNode, 3)
+ for i, alb := range albums {
+ if i >= 3 {
+ break
+ }
+ indexPage.Albums[i] = alb.ToAlbumNode()
+ }
+
+ if len(images) > 0 {
+ indexPage.SEO.SetImage(featuredImage)
+ indexPage.Picture = templateengine.PagePicture{
+ Picture: featuredImage,
+ }
+ }
+ featuedAlbum, _ := r.Albums.FindById(latestAlbumID)
+ indexPage.FeaturedAlbum = featuedAlbum.ToAlbumNode()
+
+ templateengine.Templates.RenderPage(w, templateengine.HomeTemplate, indexPage)
+}
+
+func (r *RenderPipeline) BuildAlbums(w io.Writer) {
+ page := templateengine.NewPage(nil)
+ tree := r.Albums.GetAlbumStructure(page.Settings)
+ page.Albums = datastore.GetAlbmusFromTree(tree)
+ templateengine.Templates.RenderPage(w, templateengine.AlbumTemplate, page)
+}
+
+func (r *RenderPipeline) renderIndex() {
+
+ f, _ := os.Create(filepath.Join(root, "index.html"))
+ w := bufio.NewWriter(f)
+ r.BuildIndex(w)
+ // renderPages(pages, latestAlbumID, albums)
+ w.Flush()
+ f.Close()
+
+ f, _ = os.Create(filepath.Join(root, "manifest.json"))
+ w = bufio.NewWriter(f)
+ templateengine.ManifestWriter(w, r.config)
+ w.Flush()
+ f.Close()
+
+ f, _ = os.Create(filepath.Join(root, "service-worker.js"))
+ w = bufio.NewWriter(f)
+ templateengine.ServiceWorkerWriter(w)
+ w.Flush()
+ f.Close()
+}
+
+func paginateImages(slice []datastore.Picture, chunkSize int) [][]datastore.Picture {
+ var chunks [][]datastore.Picture
+ for i := 0; i < len(slice); i += chunkSize {
+ end := i + chunkSize
+ if end > len(slice) {
+ end = len(slice)
+ }
+ chunks = append(chunks, slice[i:end])
+ }
+ return chunks
+}
+
+func (r *RenderPipeline) renderPages(pages [][]datastore.Picture, albumID string, albums datastore.AlbumStrcure) {
+ pagesPath := filepath.Join(root, "page")
+ os.MkdirAll(pagesPath, os.ModePerm)
+ for page, pageImages := range pages {
+ pagePath := filepath.Join(pagesPath, fmt.Sprint(page))
+ os.MkdirAll(pagePath, os.ModePerm)
+ page := templateengine.NewPage(nil)
+ page.Images = pageImages
+ page.Albums = datastore.GetAlbmusFromTree(albums)
+ if len(pageImages) > 0 {
+ page.SEO.SetImage(pageImages[0])
+ }
+ f, _ := os.Create(filepath.Join(pagePath, "index.html"))
+ w := bufio.NewWriter(f)
+ templateengine.Templates.RenderPage(w, templateengine.PaginationTemplate, page)
+ w.Flush()
+ f.Close()
+ }
+}
+
+func (r *RenderPipeline) renderAlbums() {
+ os.MkdirAll(albumsDir, os.ModePerm)
+ f, _ := os.Create(filepath.Join(albumsDir, "index.html"))
+ w := bufio.NewWriter(f)
+ r.BuildAlbums(w)
+ w.Flush()
+ f.Close()
+}
+
+func Assets() {
+ theme := config.Config.Gallery.Theme
+ if embeds.DoesThmeExist(theme) {
+ embeds.CopyThemeAssets(theme, filepath.Join(root, "assets"))
+ }
+ templateengine.Dir(filepath.Join(config.Config.Gallery.Theme, "assets"), filepath.Join(root, "assets"))
+}
diff --git a/pkg/pipeline/PhotoPagePipeline.go b/pkg/pipeline/PhotoPagePipeline.go
new file mode 100644
index 0000000..ec4b1f9
--- /dev/null
+++ b/pkg/pipeline/PhotoPagePipeline.go
@@ -0,0 +1,31 @@
+package pipeline
+
+import (
+ "gogallery/pkg/datastore"
+ templateengine "gogallery/pkg/templateEngine"
+ "io"
+ "os"
+ "path/filepath"
+
+ "github.com/gosimple/slug"
+)
+
+func (r *RenderPipeline) BuildPhoto(pic datastore.Picture, w io.Writer) {
+
+ album, _ := r.Pictures.FindByField("Album", pic.Album)
+ templateengine.RenderPhoto(w, pic, album, templateengine.NewPage(nil))
+}
+
+func (r *RenderPipeline) renderPhotoTemplate() func(alb datastore.Picture) error {
+ return func(pic datastore.Picture) error {
+ picPath := filepath.Join(photoDir, slug.Make(pic.Id))
+ os.MkdirAll(picPath, os.ModePerm)
+ f, err := os.Create(filepath.Join(picPath, "index.html"))
+ if err != nil {
+ return err
+ }
+ r.BuildPhoto(pic, f)
+ f.Close()
+ return nil
+ }
+}
diff --git a/pkg/pipeline/Render.go b/pkg/pipeline/Render.go
new file mode 100644
index 0000000..3fcc45f
--- /dev/null
+++ b/pkg/pipeline/Render.go
@@ -0,0 +1,78 @@
+package pipeline
+
+import (
+ "fmt"
+ "gogallery/pkg/config"
+ "gogallery/pkg/datastore"
+ templateengine "gogallery/pkg/templateEngine"
+ "os"
+ "path/filepath"
+)
+
+var root = ""
+var imgDir string
+var photoDir string
+var albumsDir string
+var albumDir string
+
+type RenderPipeline struct {
+ AlbumRender *BatchProcessing[datastore.Album]
+ PageRender *BatchProcessing[datastore.Picture]
+ ImageRender *BatchProcessing[datastore.Picture]
+ Thumbnails *BatchProcessing[datastore.Picture]
+ config *config.GalleryConfiguration
+ *datastore.DataStore
+}
+
+func NewRenderPipeline(config *config.GalleryConfiguration, db *datastore.DataStore) *RenderPipeline {
+ root = config.Destpath
+ imgDir = filepath.Join(root, "img")
+ photoDir = filepath.Join(root, "photo")
+ albumsDir = filepath.Join(root, "albums")
+ albumDir = filepath.Join(root, "album")
+
+ render := RenderPipeline{
+ DataStore: db,
+ config: config,
+ }
+ return &render
+}
+
+func (r *RenderPipeline) CreateDir() {
+ os.MkdirAll(root, os.ModePerm)
+ os.MkdirAll(imgDir, os.ModePerm)
+ os.MkdirAll(photoDir, os.ModePerm)
+ os.MkdirAll(albumDir, os.ModePerm)
+}
+
+func (r *RenderPipeline) DeleteSite() {
+ os.RemoveAll(root)
+}
+
+func (r *RenderPipeline) GenTumbnails() {
+ images, _ := r.Pictures.GetAll()
+ thumbnails := NewBatchProcessing(r.generateThumbnails(), images, r.NewTask("Optomizing thumbnails", len(images)))
+ thumbnails.Run()
+}
+
+func (r *RenderPipeline) BuildSite() {
+ r.CreateDir()
+ err := templateengine.Templates.Load(config.Config.Gallery.Theme)
+ if err != nil {
+ fmt.Println(err)
+ }
+ Assets()
+ r.renderIndex()
+ r.renderAlbums()
+
+ albums, _ := r.Albums.GetAll()
+ images, _ := r.Pictures.GetAll()
+
+ AlbumRender := NewBatchProcessing(r.renderAlbumTemplate(), albums, r.NewTask("Building albums", len(albums)))
+ PageRender := NewBatchProcessing(r.renderPhotoTemplate(), images, r.NewTask("Building pages", len(images)))
+ ImageRender := NewBatchProcessing(ImageGenV2, images, r.NewTask("Building images", len(images)))
+
+ AlbumRender.Run()
+ PageRender.Run()
+ ImageRender.Run()
+}
diff --git a/pkg/pipeline/Tumbnail.go b/pkg/pipeline/Tumbnail.go
new file mode 100644
index 0000000..bb8be2c
--- /dev/null
+++ b/pkg/pipeline/Tumbnail.go
@@ -0,0 +1,29 @@
+package pipeline
+
+import (
+ "fmt"
+ "gogallery/pkg/config"
+ "gogallery/pkg/datastore"
+)
+
+func (r *RenderPipeline) generateThumbnails() func(pic datastore.Picture) error {
+ return func(pic datastore.Picture) error {
+ size := "small" // Default size
+ if _, err := r.ImageCache.Get(pic.Id, config.JPEG, size); err == nil {
+ return nil // Skip if thumbnail already exists
+ }
+ src, err := pic.Load()
+ if err != nil {
+ fmt.Printf("Failed to load image %s: %v\n", pic.Id, err)
+ return fmt.Errorf("failed to load image %s: %w", pic.Id, err)
+ }
+ cache, err := r.ImageCache.Writer(pic.Id, config.JPEG, size)
+ if err != nil {
+ fmt.Printf("Failed to get cache writer for %s: %v\n", pic.Id, err)
+ return fmt.Errorf("failed to get cache writer for %s: %w", pic.Id, err)
+
+ }
+ ProcessImage(src, config.ImageSizes[size].ImgWidth, config.JPEG, cache)
+ return nil
+ }
+}
diff --git a/pkg/pipeline/batch.go b/pkg/pipeline/batch.go
new file mode 100644
index 0000000..85fa903
--- /dev/null
+++ b/pkg/pipeline/batch.go
@@ -0,0 +1,52 @@
+package pipeline
+
+import (
+ "gogallery/pkg/monitor"
+ "runtime"
+ "sync"
+)
+
+type BatchProcessing[T any] struct {
+ items []T
+ stat monitor.MonitorStat
+ work func(T) error
+ workers int
+}
+
+func (batch *BatchProcessing[T]) Run() {
+ var wg sync.WaitGroup
+ itemCh := make(chan T)
+ defer batch.stat.Complete()
+ // Start workers
+ for i := 0; i < batch.workers; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for item := range itemCh {
+ batch.work(item)
+ batch.stat.Update()
+ }
+ }()
+ }
+
+ // Feed items to workers
+ for _, item := range batch.items {
+ itemCh <- item
+ }
+ close(itemCh)
+ wg.Wait()
+}
+
+func NewBatchProcessing[T any](processing func(T) error, items []T, stat monitor.MonitorStat) *BatchProcessing[T] {
+ // stat.Total = len(items)
+ workers := runtime.NumCPU() - 2
+ if workers < 1 {
+ workers = 1
+ }
+ return &BatchProcessing[T]{
+ work: processing,
+ workers: workers,
+ items: items,
+ stat: stat,
+ }
+}
diff --git a/pkg/preview/preview.go b/pkg/preview/preview.go
new file mode 100644
index 0000000..cb2834a
--- /dev/null
+++ b/pkg/preview/preview.go
@@ -0,0 +1,144 @@
+package preview
+
+import (
+ "compress/gzip"
+ "gogallery/pkg/config"
+ "gogallery/pkg/embeds"
+ "gogallery/pkg/pipeline"
+ templateengine "gogallery/pkg/templateEngine"
+ "io"
+ "net/http"
+ "strings"
+
+ "github.com/gorilla/mux"
+)
+
+const assetPrefix = "/assets/"
+
+/**
+dynmically gnerate page for previewing site
+**/
+
+// cacheMiddleware sets cache headers for static assets and API responses.
+func cacheMiddleware(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // Cache for 1 hour for assets, otherwise no-cache
+ if strings.HasPrefix(r.URL.Path, assetPrefix) || strings.HasPrefix(r.URL.Path, "/img/") {
+ w.Header().Set("Cache-Control", "public, max-age=31536000")
+ }
+ next.ServeHTTP(w, r)
+ })
+}
+
+// compressionMiddleware compresses HTTP responses using gzip if the client supports it.
+func compressionMiddleware(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
+ next.ServeHTTP(w, r)
+ return
+ }
+ w.Header().Set("Content-Encoding", "gzip")
+ gz := gzip.NewWriter(w)
+ defer gz.Close()
+ gzw := gzipResponseWriter{Writer: gz, ResponseWriter: w}
+ next.ServeHTTP(gzw, r)
+ })
+}
+
+type gzipResponseWriter struct {
+ http.ResponseWriter
+ Writer *gzip.Writer
+}
+
+func (w gzipResponseWriter) Write(b []byte) (int, error) {
+ return w.Writer.Write(b)
+}
+
+var cfg = config.Config
+
+func (api *Server) Setup() {
+
+ api.Use(mux.CORSMethodMiddleware(api.Router))
+ api.Use(cacheMiddleware) // Add cache middleware
+ api.Use(compressionMiddleware) // Add compression middleware
+
+ api.HandleFunc("/", api.PreviewPageHandler).Methods("GET")
+ api.HandleFunc("/img/{id}", api.ImgHandler)
+ api.HandleFunc("/img/{id}/{size}.{ext}", api.ImgHandler)
+ api.HandleFunc("/manifest.json", api.PreviewManifest).Methods("GET")
+ api.HandleFunc("/albums", api.PreviewAlbumsHandler).Methods("GET")
+ api.HandleFunc("/photo/{id}", api.PreviewPictureHandler).Methods("GET")
+ api.HandleFunc("/album/{id}", api.PreviewCollectionHandler).Methods("GET")
+
+ api.PathPrefix(assetPrefix).Handler(api.assestHandler())
+}
+func (api *Server) assestHandler() http.Handler {
+ if embeds.DoesThmeExist(cfg.Gallery.Theme) {
+ return templateengine.Templates.AsseetServer(cfg.Gallery.Theme, assetPrefix)
+ }
+ asestPath := config.Config.Gallery.Theme + assetPrefix
+ return http.StripPrefix(assetPrefix, http.FileServer(http.Dir(asestPath)))
+}
+
+func (api *Server) ImgHandler(w http.ResponseWriter, r *http.Request) {
+ size := r.URL.Query().Get("size")
+ vars := mux.Vars(r)
+ id := vars["id"]
+ if len(size) == 0 {
+ size = vars["size"]
+ }
+ pic, _ := api.Pictures.FindById(id)
+ //Is image in cache
+ if file, err := api.ImageCache.Get(pic.Id, config.WebP, size); err == nil {
+ io.Copy(w, file)
+ return
+ }
+
+ src, err := pic.Load()
+ if err != nil {
+ return
+ }
+ cache, _ := api.ImageCache.Writer(pic.Id, config.WebP, size)
+ writer := io.MultiWriter(w, cache)
+ if size, ok := templateengine.ImageSizes[size]; ok {
+ pipeline.ProcessImage(src, size.ImgWidth, config.WebP, writer)
+ return
+ }
+}
+
+func (api *Server) PreviewPageHandler(w http.ResponseWriter, r *http.Request) {
+ templateengine.Templates.Load(cfg.Gallery.Theme)
+ builder := pipeline.NewRenderPipeline(&cfg.Gallery, api.DataStore)
+
+ w.WriteHeader(200)
+ builder.BuildIndex(w)
+}
+
+func (api *Server) PreviewAlbumsHandler(w http.ResponseWriter, r *http.Request) {
+ templateengine.Templates.Load(cfg.Gallery.Theme)
+ builder := pipeline.NewRenderPipeline(&cfg.Gallery, api.DataStore)
+ w.WriteHeader(200)
+ builder.BuildAlbums(w)
+}
+
+func (api *Server) PreviewPictureHandler(w http.ResponseWriter, r *http.Request) {
+ templateengine.Templates.Load(cfg.Gallery.Theme)
+ builder := pipeline.NewRenderPipeline(&cfg.Gallery, api.DataStore)
+ photoID := mux.Vars(r)["id"]
+ pic, _ := api.Pictures.FindById(photoID)
+ w.WriteHeader(200)
+ builder.BuildPhoto(pic, w)
+}
+
+func (api *Server) PreviewCollectionHandler(w http.ResponseWriter, r *http.Request) {
+ templateengine.Templates.Load(cfg.Gallery.Theme)
+ builder := pipeline.NewRenderPipeline(&cfg.Gallery, api.DataStore)
+ photoID := mux.Vars(r)["id"]
+ w.WriteHeader(200)
+ builder.BuildAlbum(photoID, w)
+}
+
+func (api *Server) PreviewManifest(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ templateengine.ManifestWriter(w, &cfg.Gallery)
+}
diff --git a/pkg/preview/server.go b/pkg/preview/server.go
new file mode 100644
index 0000000..c3cc962
--- /dev/null
+++ b/pkg/preview/server.go
@@ -0,0 +1,96 @@
+package preview
+
+import (
+ "context"
+ "fmt"
+ "gogallery/pkg/config"
+ "gogallery/pkg/datastore"
+ "net"
+ "net/http"
+ "sync"
+
+ "github.com/gorilla/mux"
+)
+
+type Server struct {
+ *mux.Router
+ *datastore.DataStore
+ server *http.Server
+ listener net.Listener
+ mu sync.Mutex
+ running bool
+ addr string // actual address (host:port)
+}
+
+// generateAddr generates a random address for the server to listen on.
+func generateAddr() string {
+ host := "127.0.0.1"
+ if config.Config.UI.Public {
+ host = "0.0.0.0"
+ }
+ // Always return host:0 so OS picks a free port
+ return fmt.Sprintf("%s:0", host)
+}
+
+func NewServer(db *datastore.DataStore) *Server {
+ router := mux.NewRouter()
+ addr := generateAddr()
+ server := &Server{
+ Router: router,
+ DataStore: db,
+ addr: addr,
+ mu: sync.Mutex{},
+ }
+ server.Setup()
+ return server
+}
+
+// Start starts the server on a random port. If public is true, binds to 0.0.0.0, else localhost.
+func (s *Server) Start() error {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ if s.running {
+ return nil // already running
+ }
+ // Listen on the requested address to get a free port, then use that for http.Server
+ ln, err := net.Listen("tcp", s.addr)
+ if err != nil {
+ return err
+ }
+ s.listener = ln
+ s.addr = ln.Addr().String()
+ s.server = &http.Server{Handler: s.Router}
+ s.running = true
+ go func() {
+ if err := s.server.Serve(ln); err != nil && err != http.ErrServerClosed {
+ fmt.Printf("Server error: %v\n", err)
+ }
+ }()
+ return nil
+}
+
+// Stop stops the server if running.
+func (s *Server) Stop() error {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ if !s.running {
+ return nil
+ }
+ err := s.server.Shutdown(context.Background())
+ s.listener.Close()
+ s.running = false
+ return err
+}
+
+// Addr returns the actual address the server is listening on.
+func (s *Server) Addr() string {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ return s.addr
+}
+
+func (s *Server) Status() (running bool, addr string) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ return s.running, s.addr
+}
diff --git a/backend/templateEngine/page.go b/pkg/templateEngine/page.go
similarity index 51%
rename from backend/templateEngine/page.go
rename to pkg/templateEngine/page.go
index 42d6661..538d400 100644
--- a/backend/templateEngine/page.go
+++ b/pkg/templateEngine/page.go
@@ -2,14 +2,13 @@ package templateengine
import (
"fmt"
+ "gogallery/pkg/config"
+ "gogallery/pkg/datastore"
"net/http"
-
- "github.com/robrotheram/gogallery/backend/config"
- "github.com/robrotheram/gogallery/backend/datastore/models"
)
type PagePicture struct {
- models.Picture
+ datastore.Picture
OrginalImgPath string
}
@@ -17,16 +16,16 @@ type Page struct {
Settings config.GalleryConfiguration
SEO SocailSEO
Author config.AboutConfiguration
- Images []models.Picture
- Albums models.AlbumStrcure
- Album models.Album
- LatestAlbum string
+ Images []datastore.Picture
+ Albums []datastore.AlbumNode
+ Album datastore.AlbumNode
+ FeaturedAlbum datastore.AlbumNode
Picture PagePicture
NextImagePath string
PreImagePath string
Body string
PagePath string
- ImgSizes map[string]int
+ ImgSizes map[string]ImgSize
}
type SocailSEO struct {
@@ -37,22 +36,26 @@ type SocailSEO struct {
ImageWidth int
ImageHeight int
}
+type ImgSize struct {
+ MinWidth int // Minimum screen width in pixels for this image source
+ ImgWidth int // Recommended image width to generate for this breakpoint
+}
-var ImageSizes = map[string]int{
- "xsmall": 350,
- "small": 640,
- "medium": 1024,
- "large": 1600,
- "xlarge": 1920,
+var ImageSizes = map[string]ImgSize{
+ "xsmall": {MinWidth: 0, ImgWidth: 360}, // Phones (default)
+ "small": {MinWidth: 480, ImgWidth: 640}, // Small tablets / landscape phones
+ "medium": {MinWidth: 768, ImgWidth: 960}, // Tablets
+ "large": {MinWidth: 1024, ImgWidth: 1280}, // Laptops / small desktops
+ "xlarge": {MinWidth: 1440, ImgWidth: 0}, // Large desktops (0 means use original size)
}
-func (s *SocailSEO) SetImage(picture models.Picture) {
- s.ImageUrl = fmt.Sprintf("%s/img/%s", config.Config.Gallery.Url, picture.Id)
+func (s *SocailSEO) SetImage(picture datastore.Picture) {
+ s.ImageUrl = fmt.Sprintf("%s/img/%s/xlarge.webp", config.Config.Gallery.Url, picture.Id)
s.ImageWidth = 1024
s.ImageHeight = 683
}
-func (s *SocailSEO) SetNameFromPhoto(picture models.Picture) {
+func (s *SocailSEO) SetNameFromPhoto(picture datastore.Picture) {
s.Title = picture.Name
if picture.Caption != "" {
s.Description = picture.Caption
@@ -68,12 +71,11 @@ func NewSocailSEO(path string) SocailSEO {
}
}
-func NewPage(r *http.Request, albumID string) Page {
+func NewPage(r *http.Request) Page {
page := Page{
- Settings: config.Config.Gallery,
- Author: config.Config.About,
- LatestAlbum: albumID,
- ImgSizes: ImageSizes,
+ Settings: config.Config.Gallery,
+ Author: config.Config.About,
+ ImgSizes: ImageSizes,
}
if r != nil {
page.SEO = NewSocailSEO(r.URL.EscapedPath())
@@ -82,7 +84,7 @@ func NewPage(r *http.Request, albumID string) Page {
return page
}
-func NewPagePicture(pic models.Picture) PagePicture {
+func NewPagePicture(pic datastore.Picture) PagePicture {
originalPath := fmt.Sprintf("/img/%s/xlarge.webp", pic.Id)
if config.Config.Gallery.UseOriginal {
originalPath = fmt.Sprintf("/img/%s/original%s", pic.Id, pic.Ext)
diff --git a/backend/templateEngine/photoPage.go b/pkg/templateEngine/photoPage.go
similarity index 65%
rename from backend/templateEngine/photoPage.go
rename to pkg/templateEngine/photoPage.go
index 1937ee3..d979544 100644
--- a/backend/templateEngine/photoPage.go
+++ b/pkg/templateEngine/photoPage.go
@@ -1,13 +1,11 @@
package templateengine
import (
+ "gogallery/pkg/datastore"
"io"
-
- "github.com/robrotheram/gogallery/backend/datastore/models"
)
-func RenderPhoto(w io.Writer, pic models.Picture, images []models.Picture, page Page) {
- images = models.SortByTime(images)
+func RenderPhoto(w io.Writer, pic datastore.Picture, images []datastore.Picture, page Page) {
for i, p := range images {
if p.Id == pic.Id {
if i-1 >= 0 {
diff --git a/backend/templateEngine/pwaManifest.go b/pkg/templateEngine/pwaManifest.go
similarity index 97%
rename from backend/templateEngine/pwaManifest.go
rename to pkg/templateEngine/pwaManifest.go
index 055de75..9a1c92b 100644
--- a/backend/templateEngine/pwaManifest.go
+++ b/pkg/templateEngine/pwaManifest.go
@@ -2,9 +2,8 @@ package templateengine
import (
"encoding/json"
+ "gogallery/pkg/config"
"io"
-
- "github.com/robrotheram/gogallery/backend/config"
)
type Manifest struct {
diff --git a/backend/templateEngine/templateCache.go b/pkg/templateEngine/templateCache.go
similarity index 100%
rename from backend/templateEngine/templateCache.go
rename to pkg/templateEngine/templateCache.go
diff --git a/backend/templateEngine/templateEngine.go b/pkg/templateEngine/templateEngine.go
similarity index 62%
rename from backend/templateEngine/templateEngine.go
rename to pkg/templateEngine/templateEngine.go
index 90e4735..a29bb69 100644
--- a/backend/templateEngine/templateEngine.go
+++ b/pkg/templateEngine/templateEngine.go
@@ -3,14 +3,16 @@ package templateengine
import (
"bytes"
"fmt"
+ "gogallery/pkg/embeds"
"html/template"
"io"
+ "io/fs"
+ "net/http"
"os"
"path/filepath"
"regexp"
"strings"
- "github.com/robrotheram/gogallery/backend/embeds"
"github.com/tdewolff/minify/v2"
"github.com/tdewolff/minify/v2/css"
"github.com/tdewolff/minify/v2/html"
@@ -24,33 +26,52 @@ const CollectionTemplate = "collections"
const PhotoTemplate = "photo"
const PaginationTemplate = "pagination"
-func (te *TemplateEngine) LoadFromEmbed() error {
+func (te *TemplateEngine) LoadFromEmbed(theme string) error {
te.Cache = newTeamplateCache()
-
- base, err := template.ParseFS(embeds.ThemeFS, "themes/eastnor/default.hbs")
+ path := "themes/" + theme
+ base, err := template.ParseFS(embeds.ThemeFS, path+"/default.tmpl.html")
if err != nil {
return err
}
- base.Funcs(template.FuncMap{"ImgSizes": func() map[string]int { return ImageSizes }})
- base, _ = base.ParseFS(embeds.ThemeFS, "themes/eastnor/partials/*.hbs")
+ base.Funcs(template.FuncMap{"ImgSizes": func() map[string]ImgSize { return ImageSizes }})
+ base, _ = base.ParseFS(embeds.ThemeFS, path+"/partials/*.html")
- items, err := embeds.ThemeFS.ReadDir("themes/eastnor/pages")
+ items, err := embeds.ThemeFS.ReadDir(path + "/pages")
if err != nil {
return err
}
for _, item := range items {
- name := strings.TrimSuffix(item.Name(), filepath.Ext(item.Name()))
+ name := strings.TrimSuffix(item.Name(), ".tmpl.html")
pageTemplate := template.Must(base.Clone())
- pageTemplate = template.Must(pageTemplate.ParseFS(embeds.ThemeFS, "themes/eastnor/pages/"+item.Name()))
+ pageTemplate = template.Must(pageTemplate.ParseFS(embeds.ThemeFS, path+"/pages/"+item.Name()))
te.Cache.Add(name, pageTemplate)
}
return nil
}
+func (te *TemplateEngine) AsseetServer(theme string, assestPath string) http.Handler {
+ assetPrefix := "/assets/"
+ embedPath := "themes/" + theme + "/" + assestPath
+ fs := http.FS(embeds.ThemeFS)
+ return http.StripPrefix(assetPrefix, http.FileServer(http.FS(&embedSubFS{fs, embedPath})))
+}
+
+// embedSubFS restricts access to a subdirectory of an http.FS.
+type embedSubFS struct {
+ fs http.FileSystem
+ subDir string
+}
+
+func (e *embedSubFS) Open(name string) (fs.File, error) {
+ clean := filepath.Clean("/" + name)
+ full := filepath.Join(e.subDir, clean)
+ return e.fs.Open(full)
+}
+
func (te *TemplateEngine) Load(basePath string) error {
- if basePath == "default" || basePath == "" {
- return te.LoadFromEmbed()
+ if embeds.DoesThmeExist(basePath) {
+ return te.LoadFromEmbed(basePath)
}
return te.LoadFromPath(basePath)
}
@@ -58,12 +79,12 @@ func (te *TemplateEngine) Load(basePath string) error {
func (te *TemplateEngine) LoadFromPath(basePath string) error {
te.Cache = newTeamplateCache()
pagePath := "pages"
- base, err := template.ParseFiles(filepath.Join(basePath, "default.hbs"))
+ base, err := template.ParseFiles(filepath.Join(basePath, "default.tmpl.html"))
if err != nil {
return err
}
- base.Funcs(template.FuncMap{"ImgSizes": func() map[string]int { return ImageSizes }})
- base, err = base.ParseGlob(filepath.Join(basePath, "partials/*.hbs"))
+ base.Funcs(template.FuncMap{"ImgSizes": func() map[string]ImgSize { return ImageSizes }})
+ base, err = base.ParseGlob(filepath.Join(basePath, "partials/*.tmpl.html"))
if err != nil {
return err
}
@@ -72,7 +93,7 @@ func (te *TemplateEngine) LoadFromPath(basePath string) error {
return err
}
for _, item := range items {
- name := strings.TrimSuffix(item.Name(), filepath.Ext(item.Name()))
+ name := strings.TrimSuffix(item.Name(), ".tmpl.html")
pageTemplate := template.Must(base.Clone())
pageTemplate = template.Must(pageTemplate.ParseGlob(filepath.Join(basePath, pagePath, item.Name())))
te.Cache.Add(name, pageTemplate)
diff --git a/backend/templateEngine/utils.go b/pkg/templateEngine/utils.go
similarity index 100%
rename from backend/templateEngine/utils.go
rename to pkg/templateEngine/utils.go
diff --git a/pkg/ui/app.go b/pkg/ui/app.go
new file mode 100644
index 0000000..060a8f4
--- /dev/null
+++ b/pkg/ui/app.go
@@ -0,0 +1,76 @@
+package ui
+
+import (
+ "fmt"
+ "gogallery/pkg/ai"
+ "gogallery/pkg/config"
+ "gogallery/pkg/datastore"
+ "gogallery/pkg/pipeline"
+ "gogallery/pkg/preview"
+ "gogallery/pkg/ui/components"
+ "gogallery/pkg/ui/pages"
+
+ uiMonitor "gogallery/pkg/ui/monitors"
+
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/app"
+ "fyne.io/fyne/v2/container"
+)
+
+func App() error {
+ cfg := config.LoadConfig()
+
+ myApp := app.New()
+ myApp.Settings().SetTheme(NewComfortableTheme(cfg.UI.Theme))
+ myWindow := myApp.NewWindow("GoGallery")
+
+ monitor := uiMonitor.NewUIMonitor()
+ db, err := datastore.Open("gogallery.sql.db", monitor)
+ if err != nil {
+ fmt.Println("Error opening database:", err)
+ return err
+ }
+ ai.RegisterGeminiClient()
+ server := preview.NewServer(db)
+ // Start background task to scan path and generate thumbnails
+ go backgroundTask(db, cfg)
+
+ galleryPage := pages.NewGalleryPage(db)
+ settingsPage := pages.NewSettingsPage(db)
+ tasksPage := pages.NewTasksPage(db, server)
+
+ pages := map[string]pages.Page{
+ "Gallery": galleryPage,
+ "Settings": settingsPage,
+ "Tasks": tasksPage,
+ }
+
+ var navBar *fyne.Container
+
+ setPage := func(page string) {
+ currentPage, ok := pages[page]
+ if !ok {
+ fmt.Println("Page not found:", page)
+ return
+ }
+ content := container.NewBorder(
+ navBar, nil, nil, nil,
+ container.NewStack(currentPage.Layout()),
+ )
+ myWindow.SetContent(content)
+ }
+ navBar = (components.NewHeader(config.Config.Gallery.Name, db, server, setPage)).Layout()
+ setPage("Gallery")
+
+ myWindow.Resize(fyne.NewSize(1200, 800))
+ myWindow.ShowAndRun()
+ return nil
+}
+
+func backgroundTask(db *datastore.DataStore, cfg *config.Configuration) {
+ fmt.Println("Starting background task to scan path:", cfg.Gallery.Basepath)
+ db.ScanPath(cfg.Gallery.Basepath)
+ pipeline := pipeline.NewRenderPipeline(&cfg.Gallery, db)
+ pipeline.GenTumbnails()
+ fmt.Println("Background task completed")
+}
diff --git a/pkg/ui/components/ClickableTitle.go b/pkg/ui/components/ClickableTitle.go
new file mode 100644
index 0000000..485cf7b
--- /dev/null
+++ b/pkg/ui/components/ClickableTitle.go
@@ -0,0 +1,66 @@
+package components
+
+import (
+ "image/color"
+
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/canvas"
+ "fyne.io/fyne/v2/driver/desktop"
+ "fyne.io/fyne/v2/widget"
+)
+
+// ClickableTitle is a canvas.Text that acts like a clickable title
+type ClickableTitle struct {
+ widget.BaseWidget
+ Text string
+ OnTapped func()
+}
+
+func NewClickableTitle(text string, onTapped func()) *ClickableTitle {
+ c := &ClickableTitle{Text: text, OnTapped: onTapped}
+ c.ExtendBaseWidget(c)
+ return c
+}
+
+func (c *ClickableTitle) CreateRenderer() fyne.WidgetRenderer {
+ title := canvas.NewText(c.Text, nil)
+ title.TextStyle = fyne.TextStyle{Bold: true}
+ title.TextSize = 28
+ objs := []fyne.CanvasObject{title}
+ return &clickableTitleRenderer{title: title, objects: objs}
+}
+
+func (c *ClickableTitle) Tapped(_ *fyne.PointEvent) {
+ if c.OnTapped != nil {
+ c.OnTapped()
+ }
+}
+
+type clickableTitleRenderer struct {
+ title *canvas.Text
+ objects []fyne.CanvasObject
+}
+
+func (r *clickableTitleRenderer) Layout(size fyne.Size) {
+ r.title.Resize(size)
+}
+func (r *clickableTitleRenderer) MinSize() fyne.Size {
+ return r.title.MinSize()
+}
+func (r *clickableTitleRenderer) Refresh() {
+ canvas.Refresh(r.title)
+}
+func (r *clickableTitleRenderer) BackgroundColor() color.Color {
+ return color.Transparent
+}
+func (r *clickableTitleRenderer) Objects() []fyne.CanvasObject {
+ return r.objects
+}
+func (r *clickableTitleRenderer) Destroy() {
+ // The Destroy method is intentionally left empty because
+ // there are no resources to clean up for this renderer.
+}
+
+func (r *ClickableTitle) Cursor() desktop.Cursor {
+ return desktop.PointerCursor
+}
diff --git a/pkg/ui/components/ImageCell.go b/pkg/ui/components/ImageCell.go
new file mode 100644
index 0000000..ff7a37d
--- /dev/null
+++ b/pkg/ui/components/ImageCell.go
@@ -0,0 +1,62 @@
+package components
+
+import (
+ "image/color"
+
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/canvas"
+ "fyne.io/fyne/v2/container"
+ "fyne.io/fyne/v2/driver/desktop"
+ "fyne.io/fyne/v2/widget"
+)
+
+// ImageCell is a custom widget for a clickable image with a border on hover
+type ImageCell struct {
+ widget.BaseWidget
+ img *canvas.Image
+ border *canvas.Rectangle
+ onClick func()
+}
+
+func NewImageCell(img *canvas.Image, onClick func()) *ImageCell {
+ border := canvas.NewRectangle(color.NRGBA{R: 0, G: 120, B: 255, A: 100})
+ border.StrokeWidth = 4
+ border.StrokeColor = color.NRGBA{R: 0, G: 120, B: 255, A: 255}
+ border.Hide()
+ cell := &ImageCell{
+ img: img,
+ border: border,
+ onClick: onClick,
+ }
+ cell.ExtendBaseWidget(cell)
+ return cell
+}
+
+func (c *ImageCell) CreateRenderer() fyne.WidgetRenderer {
+ return widget.NewSimpleRenderer(container.NewStack(c.img, c.border))
+}
+
+func (c *ImageCell) Tapped(_ *fyne.PointEvent) {
+ if c.onClick != nil {
+ c.onClick()
+ }
+}
+func (c *ImageCell) MouseIn(_ *desktop.MouseEvent) {
+ c.border.Show()
+ c.Refresh()
+}
+
+func (c *ImageCell) MouseOut() {
+ c.border.Hide()
+ c.Refresh()
+}
+func (c *ImageCell) MouseMoved(_ *desktop.MouseEvent) {
+ // This function is intentionally left empty because the ImageCell does not need
+ // to handle mouse movement events. It only reacts to mouse hover (MouseIn/MouseOut)
+ // and click (Tapped) events.
+}
+
+// Set the cursor to a pointer (hand) when hovering over the image cell
+func (c *ImageCell) Cursor() desktop.Cursor {
+ return desktop.PointerCursor
+}
diff --git a/pkg/ui/components/ResponsiveGrid.go b/pkg/ui/components/ResponsiveGrid.go
new file mode 100644
index 0000000..4cab7c9
--- /dev/null
+++ b/pkg/ui/components/ResponsiveGrid.go
@@ -0,0 +1,57 @@
+package components
+
+import "fyne.io/fyne/v2"
+
+// --- Responsive grid layout ---
+type ResponsiveGridLayout struct {
+ minCellWidth int
+ aspectRatio float64
+ gap int
+ lastWidth float32 // <-- add this
+}
+
+func (l *ResponsiveGridLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {
+ l.lastWidth = size.Width
+ w := float64(size.Width)
+ minCellWidth := float64(l.minCellWidth)
+ gap := float64(l.gap)
+ cols := max(int(w/(minCellWidth+gap)), 1)
+ cellW := (w - float64(cols+1)*gap) / float64(cols)
+ cellH := cellW / l.aspectRatio
+
+ for i, obj := range objects {
+ row := i / cols
+ col := i % cols
+ x := float32(gap + float64(col)*(cellW+gap))
+ y := float32(gap + float64(row)*(cellH+gap))
+ obj.Resize(fyne.NewSize(float32(cellW), float32(cellH)))
+ obj.Move(fyne.NewPos(x, y))
+ }
+}
+func (l *ResponsiveGridLayout) MinSize(objects []fyne.CanvasObject) fyne.Size {
+ minCellWidth := float32(l.minCellWidth)
+ gap := float32(l.gap)
+ n := len(objects)
+ if n == 0 {
+ cellH := minCellWidth / float32(l.aspectRatio)
+ return fyne.NewSize(minCellWidth+2*gap, cellH+2*gap)
+ }
+ w := minCellWidth + 2*float32(l.gap)
+ // Use lastWidth for dynamic calculation, fallback to 1 column if not set
+ width := l.lastWidth
+ if width < minCellWidth+2*gap {
+ width = minCellWidth + 2*gap
+ }
+ cols := int(width / (minCellWidth + gap))
+ if cols < 1 {
+ cols = 1
+ }
+ // Calculate dynamic cell width and height
+ cellW := (float64(width) - float64(cols+1)*float64(gap)) / float64(cols)
+ cellH := float32(cellW / l.aspectRatio)
+
+ rows := (n + cols - 1) / cols
+ h := float32(rows)*(cellH+gap) + gap
+
+ return fyne.NewSize(w, h)
+}
diff --git a/pkg/ui/components/header.go b/pkg/ui/components/header.go
new file mode 100644
index 0000000..d9d7afd
--- /dev/null
+++ b/pkg/ui/components/header.go
@@ -0,0 +1,94 @@
+package components
+
+import (
+ "fmt"
+ "gogallery/pkg/datastore"
+ "gogallery/pkg/preview"
+
+ "net/url"
+
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/canvas"
+ "fyne.io/fyne/v2/container"
+ "fyne.io/fyne/v2/layout"
+ "fyne.io/fyne/v2/theme"
+ "fyne.io/fyne/v2/widget"
+)
+
+type Header struct {
+ Title string
+ onNavChange func(page string)
+ server *preview.Server
+}
+
+func NewHeader(title string, db *datastore.DataStore, server *preview.Server, onNavChange func(page string)) *Header {
+ if title == "" {
+ title = "GoGallery"
+ }
+ return &Header{
+ Title: title,
+ onNavChange: onNavChange,
+ server: server,
+ }
+}
+
+func (h *Header) nav() *fyne.Container {
+ preview := widget.NewButtonWithIcon("Preview", theme.VisibilityIcon(), func() {
+ h.Preview()
+ })
+ tasks := widget.NewButtonWithIcon("Tasks", theme.ContentPasteIcon(), func() {
+ h.onNavChange("Tasks")
+ })
+ settings := widget.NewButtonWithIcon("Settings", theme.SettingsIcon(), func() {
+ h.onNavChange("Settings")
+ })
+ navButtons := []fyne.CanvasObject{preview, tasks, settings}
+ return container.NewHBox(container.NewHBox(navButtons...))
+}
+
+func (h *Header) Layout() *fyne.Container {
+ // Clickable title that looks like a real title
+ clickableTitle := NewClickableTitle(h.Title, func() {
+ h.onNavChange("Gallery")
+ })
+ leftPad := canvas.NewRectangle(nil)
+ leftPad.SetMinSize(fyne.NewSize(12, 0))
+ headerBox := container.NewHBox(
+ leftPad,
+ clickableTitle,
+ leftPad,
+ layout.NewSpacer(),
+ h.nav(),
+ leftPad,
+ )
+ headerBox.Resize(fyne.NewSize(0, 64))
+
+ return container.NewVBox(
+ headerBox,
+ widget.NewSeparator(),
+ )
+}
+
+func (h *Header) Preview() {
+ status, _ := h.server.Status()
+ if !status {
+ h.server.Start()
+ }
+ u, _ := url.Parse(fmt.Sprintf("http://%s", h.server.Addr()))
+ fyne.CurrentApp().OpenURL(u)
+}
+
+/*
+ if alb, err := h.Albums.GetAll(); err == nil {
+ options := make([]string, len(alb))
+ for i, a := range alb {
+ options[i] = a.Name
+ }
+ h.FilterList.Options = options
+ } else {
+ h.FilterList.Options = []string{"No Albums Found"}
+ }
+ h.FilterList.PlaceHolder = "Select Album"
+ h.FilterList.Selected = h.Title // Set the initial selected album to the title
+
+*/
diff --git a/pkg/ui/components/imageGrid.go b/pkg/ui/components/imageGrid.go
new file mode 100644
index 0000000..461a47f
--- /dev/null
+++ b/pkg/ui/components/imageGrid.go
@@ -0,0 +1,319 @@
+package components
+
+import (
+ "bytes"
+ "fmt"
+ "gogallery/pkg/config"
+ "gogallery/pkg/datastore"
+ "gogallery/pkg/pipeline"
+ "image"
+ "image/color"
+ "image/draw"
+ "image/jpeg"
+ "io"
+ "log"
+ "net/http"
+
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/canvas"
+ "fyne.io/fyne/v2/container"
+ "fyne.io/fyne/v2/layout"
+ "fyne.io/fyne/v2/theme"
+ "fyne.io/fyne/v2/widget"
+)
+
+type ImageGrid struct {
+ *datastore.DataStore
+ currentPage int
+ images []datastore.Picture
+ itemsPerPage int
+ totalPages int
+ paginationBar *fyne.Container
+ grid *fyne.Container
+ pageLabel *canvas.Text
+ gridItems []fyne.CanvasObject
+ title *canvas.Text
+ selectedAlbum string // Track currently selected album
+ OnImageSelected func(pic datastore.Picture) // Callback for image click
+}
+
+func NewImageGrid(db *datastore.DataStore) *ImageGrid {
+ itemsPerPage := 20 // Default value
+ if config.Config.UI.ImagesPerPage > 0 {
+ itemsPerPage = config.Config.UI.ImagesPerPage
+ }
+
+ ig := &ImageGrid{
+ DataStore: db,
+ currentPage: 0,
+ itemsPerPage: itemsPerPage,
+ totalPages: 0,
+ title: NewTextEntry("All Images", 20),
+ }
+ ig.pagination()
+ ig.imageGrid()
+
+ return ig
+}
+
+func (g *ImageGrid) filterByAlbum(alb string) {
+ if pics, err := g.DataStore.Pictures.FindByField("album_name", alb); err == nil {
+ g.SetImages(pics)
+ g.selectedAlbum = alb // Update selected album
+ g.title.Text = g.selectedAlbum
+ } else {
+ log.Println("Error filtering by album:", err)
+ }
+ g.currentPage = 0 // Reset to first page when filtering
+ g.Refresh()
+}
+
+func (g *ImageGrid) SetImages(images []datastore.Picture) {
+ if len(images) == 0 {
+ log.Println("No images to display")
+ g.placeholder() // Show placeholder if no images
+ g.totalPages = 0
+ g.currentPage = 0
+ g.Refresh()
+ return
+ }
+ g.totalPages = (len(images) + g.itemsPerPage - 1) / g.itemsPerPage
+ g.images = images
+ g.currentPage = 0 // Reset to first page when setting new images
+ //
+ g.Refresh()
+}
+
+func (g *ImageGrid) pagination() {
+ // Pagination controls
+ g.pageLabel = canvas.NewText(fmt.Sprintf("Page %d / %d", g.currentPage+1, g.totalPages), color.White)
+ g.pageLabel.Alignment = fyne.TextAlignCenter
+ prevBtn := widget.NewButtonWithIcon("Previous", theme.NavigateBackIcon(), func() {
+ if g.currentPage > 0 {
+ g.currentPage--
+ g.Refresh()
+ }
+ })
+ nextBtn := widget.NewButtonWithIcon("Next", theme.NavigateNextIcon(), func() {
+ if g.currentPage < g.totalPages-1 {
+ g.currentPage++
+ g.Refresh()
+ }
+ })
+ nextBtn.IconPlacement = widget.ButtonIconTrailingText
+ nextBtn.Alignment = widget.ButtonAlignCenter
+
+ g.paginationBar = container.NewHBox(
+ prevBtn,
+ layout.NewSpacer(),
+ container.NewStack(g.pageLabel),
+ layout.NewSpacer(),
+ nextBtn,
+ )
+}
+
+func (g *ImageGrid) imageGrid() {
+ // Only create the grid container if it doesn't exist
+ if g.grid == nil {
+ g.grid = container.New(&ResponsiveGridLayout{
+ minCellWidth: 400,
+ aspectRatio: 1.5,
+ gap: 20,
+ }, g.gridItems...)
+ } else {
+ // Just update the layout, don't reset gridItems
+ g.grid.Layout = &ResponsiveGridLayout{
+ minCellWidth: 400,
+ aspectRatio: 1.5,
+ gap: 20,
+ }
+ g.grid.Refresh()
+ }
+}
+
+func (g *ImageGrid) placeholder() {
+ start := g.currentPage * g.itemsPerPage
+ end := min(start+g.itemsPerPage, len(g.images))
+ items := make([]fyne.CanvasObject, end-start)
+ for i := range items {
+ // Use a lightweight placeholder (no URL text, just a rectangle)
+ cellBg := canvas.NewRectangle(color.RGBA{R: 241, G: 241, B: 241, A: 255})
+ cellBg.StrokeColor = color.Black
+ cellBg.StrokeWidth = 1
+ // Optionally, add a spinner or "Loading..." label for better UX
+ label := canvas.NewText("Loading...", color.Gray{Y: 128})
+ label.Alignment = fyne.TextAlignCenter
+ cell := container.NewStack(cellBg, label)
+ items[i] = cell
+ }
+ g.gridItems = items
+ if g.grid != nil {
+ g.grid.Objects = g.gridItems
+ g.grid.Refresh()
+ }
+}
+
+func ImageFromURL(url string) (*canvas.Image, error) {
+ resp, err := http.Get(url)
+ if err != nil {
+ log.Println("Failed to fetch image:", err)
+ return nil, err
+ }
+ defer resp.Body.Close()
+ imgData, _, err := image.Decode(resp.Body)
+ if err != nil {
+ log.Println("Failed to decode image:", err)
+ return nil, err
+ }
+ img := canvas.NewImageFromImage(imgData)
+ img.FillMode = canvas.ImageFillOriginal
+ return img, nil
+}
+
+func cropToAspect(imgBuf bytes.Buffer, targetW, targetH int) *bytes.Buffer {
+ // Decode image from buffer
+ srcImg, _, err := image.Decode(&imgBuf)
+ if err != nil {
+ return &imgBuf // fallback: return original if decode fails
+ }
+ srcBounds := srcImg.Bounds()
+ srcW := srcBounds.Dx()
+ srcH := srcBounds.Dy()
+ targetAspect := float64(targetW) / float64(targetH)
+ srcAspect := float64(srcW) / float64(srcH)
+
+ var cropW, cropH int
+ if srcAspect > targetAspect {
+ // Source is wider than target: crop width
+ cropH = srcH
+ cropW = int(float64(cropH) * targetAspect)
+ } else {
+ // Source is taller than target: crop height
+ cropW = srcW
+ cropH = int(float64(cropW) / targetAspect)
+ }
+ x0 := srcBounds.Min.X + (srcW-cropW)/2
+ y0 := srcBounds.Min.Y + (srcH-cropH)/2
+ cropRect := image.Rect(x0, y0, x0+cropW, y0+cropH)
+
+ // Crop and copy to a new RGBA image
+ cropped := image.NewRGBA(image.Rect(0, 0, cropW, cropH))
+ draw.Draw(cropped, cropped.Bounds(), srcImg, cropRect.Min, draw.Src)
+
+ // Encode cropped image back to buffer
+ var outBuf bytes.Buffer
+ jpeg.Encode(&outBuf, cropped, nil)
+ return &outBuf
+}
+
+func (g *ImageGrid) Thumbnail(pic datastore.Picture) (*canvas.Image, error) {
+ size := "small" // Default size
+
+ if file, err := g.ImageCache.Get(pic.Id, config.JPEG, size); err == nil {
+ var buf bytes.Buffer
+ if _, err := io.Copy(&buf, file); err != nil {
+ return nil, fmt.Errorf("failed to read cached image %s: %w", pic.Id, err)
+ }
+ img := canvas.NewImageFromReader(cropToAspect(buf, 6, 4), "")
+ img.FillMode = canvas.ImageFillOriginal // Use Stretch to fill cell, will crop via layout
+ img.SetMinSize(fyne.NewSize(0, 0)) // Let layout control size
+ return img, nil
+ }
+ src, err := pic.Load()
+ if err != nil {
+ return nil, fmt.Errorf("failed to load image %s: %w", pic.Id, err)
+ }
+
+ cache, err := g.ImageCache.Writer(pic.Id, config.JPEG, size)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get cache writer: %w", err)
+ }
+
+ var buf bytes.Buffer
+ multi := io.MultiWriter(cache, &buf)
+ pipeline.ProcessImage(src, 400, config.JPEG, multi)
+
+ img := canvas.NewImageFromReader(cropToAspect(buf, 6, 4), "")
+ img.FillMode = canvas.ImageFillStretch // Use Stretch to fill cell, will crop via layout
+ img.SetMinSize(fyne.NewSize(0, 0)) // Let layout control size
+ return img, nil
+}
+
+// Load replace the placeholder with actual images
+// This function can be used to load images asynchronously or on demand.
+func (g *ImageGrid) LoadImages() {
+ g.placeholder()
+
+ start := g.currentPage * g.itemsPerPage
+ end := min((g.currentPage+1)*g.itemsPerPage, len(g.images))
+ for i := start; i < end; i++ {
+ idx := i - start
+ pic := g.images[i]
+ go func(i, idx int, pic datastore.Picture) {
+ img, err := g.Thumbnail(pic)
+ if err != nil {
+ log.Println("Error loading image:", err)
+ return
+ }
+ // Make the image fill the cell and be fully clickable, with a border on hover
+ img.FillMode = canvas.ImageFillContain
+ img.SetMinSize(fyne.NewSize(180, 180)) // Adjust as needed for your grid cell size
+ cell := NewImageCell(img, func() {
+ if g.OnImageSelected != nil {
+ g.OnImageSelected(pic)
+ }
+ })
+ if idx < len(g.grid.Objects) {
+ g.grid.Objects[idx] = cell
+ }
+ fyne.Do(func() {
+ g.grid.Refresh()
+ })
+ }(i, idx, pic)
+ }
+ log.Println("Started loading images for page", g.currentPage+1)
+}
+func (g *ImageGrid) galleryHeader() fyne.CanvasObject {
+ leftPad := canvas.NewRectangle(nil)
+ leftPad.SetMinSize(fyne.NewSize(12, 0))
+ const allPhotosLabel = "All Photos"
+
+ albms, _ := g.Albums.GetLatestAlbums()
+ albumOptions := make([]string, len(albms)+1)
+ albumOptions[0] = allPhotosLabel // First option for all photos
+ for i, album := range albms {
+ albumOptions[i+1] = album.Name
+ }
+
+ albumSelect := widget.NewSelect(albumOptions, func(selected string) {
+ log.Printf("Selected album: %s", selected)
+ if selected == allPhotosLabel {
+ pics, err := g.DataStore.Pictures.GetAll()
+ if err != nil {
+ return
+ }
+ g.SetImages(pics)
+ return
+ }
+ g.filterByAlbum(selected)
+ })
+ albumSelect.PlaceHolder = allPhotosLabel
+ return (container.NewHBox(leftPad, albumSelect))
+}
+
+func (g *ImageGrid) Layout() fyne.CanvasObject {
+ stack := container.NewVBox(g.galleryHeader(), g.grid)
+ return container.NewBorder(
+ nil, // top
+ g.paginationBar, // bottom (footer)
+ nil, // left
+ nil, // right
+ container.NewVScroll(stack), // center (main scroll area)
+ )
+}
+
+func (g *ImageGrid) Refresh() {
+ g.pageLabel.Text = fmt.Sprintf("Page %d / %d", g.currentPage+1, g.totalPages)
+ g.pageLabel.Refresh()
+ g.LoadImages()
+}
diff --git a/pkg/ui/components/sidebar.go b/pkg/ui/components/sidebar.go
new file mode 100644
index 0000000..a3fc21d
--- /dev/null
+++ b/pkg/ui/components/sidebar.go
@@ -0,0 +1,236 @@
+package components
+
+import (
+ "bytes"
+ "fmt"
+ "gogallery/pkg/ai"
+ "gogallery/pkg/config"
+ "gogallery/pkg/datastore"
+ "gogallery/pkg/ui/utils"
+ "io"
+ "log"
+
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/canvas"
+ "fyne.io/fyne/v2/container"
+ "fyne.io/fyne/v2/layout"
+ "fyne.io/fyne/v2/theme"
+ "fyne.io/fyne/v2/widget"
+)
+
+type Sidebar struct {
+ *datastore.DataStore
+ visible bool
+ selectedPic datastore.Picture // Reference to the currently selected picture
+ titleEntry *widget.Entry
+ captionEntry *widget.Entry
+ image *canvas.Image // Placeholder for the image to be displayed
+ imageStack *fyne.Container // Direct reference to the image stack
+ container fyne.CanvasObject // Reference to the sidebar container for refresh
+ exifCard *fyne.Container // Reference to the EXIF card for updates
+ OnClose func() // Callback for close button
+}
+
+func NewSidebar(db *datastore.DataStore, onClose func()) *Sidebar {
+ titleEntry := widget.NewEntry()
+ titleEntry.SetPlaceHolder("Enter image title")
+
+ captionEntry := widget.NewMultiLineEntry()
+ captionEntry.SetPlaceHolder("Enter image caption")
+ captionEntry.Wrapping = fyne.TextWrapWord
+
+ // Create a persistent image widget (bigger size)
+ img := canvas.NewImageFromImage(nil) // Start with nil image
+ img.FillMode = canvas.ImageFillContain
+ img.SetMinSize(fyne.NewSize(600, 400))
+ imageStack := container.NewStack(img)
+
+ return &Sidebar{
+ visible: false,
+ titleEntry: titleEntry,
+ DataStore: db,
+ captionEntry: captionEntry,
+ image: img,
+ imageStack: imageStack,
+ OnClose: onClose,
+ }
+}
+
+func NewTextEntry(textStr string, size float32) *canvas.Text {
+ text := canvas.NewText(textStr, nil)
+ text.TextStyle = fyne.TextStyle{Bold: true}
+ text.TextSize = size // Larger font size
+ return text
+}
+
+func (s *Sidebar) Layout() fyne.CanvasObject {
+ // Close button
+ closeBtn := widget.NewButtonWithIcon("", theme.ContentClearIcon(), s.OnClose)
+ closeBtn.Importance = widget.LowImportance
+ closeBtn.Alignment = widget.ButtonAlignTrailing
+ closeBtnBox := container.NewVBox(
+ layout.NewSpacer(),
+ closeBtn,
+ layout.NewSpacer(),
+ )
+
+ titleRow := container.NewHBox(
+ NewTextEntry("Image Details", 22),
+ layout.NewSpacer(),
+ closeBtnBox,
+ )
+
+ // EXIF info section (populated in ShowImage)
+ form := widget.NewForm(
+ widget.NewFormItem("Title", s.titleEntry),
+ widget.NewFormItem("Caption", s.captionEntry),
+ )
+ form.OnSubmit = func() {
+ log.Println("Form submitted with title:", s.titleEntry.Text, "and caption:", s.captionEntry.Text)
+ // Update the selected picture with new title and caption
+ s.selectedPic.Name = s.titleEntry.Text
+ s.selectedPic.Caption = s.captionEntry.Text
+ if err := s.DataStore.Pictures.Update(s.selectedPic.Id, s.selectedPic); err != nil {
+ log.Println("Error updating picture:", err)
+ } else {
+ log.Println("Picture updated successfully")
+ utils.Notify("Update Successful", "Picture details updated successfully")
+ }
+ }
+ s.exifCard = container.NewVBox()
+
+ //AI button
+ var scrollContent *fyne.Container
+ if ai.IsAi() {
+ var aiButton *widget.Button
+ aiButton = widget.NewButtonWithIcon("Generate Caption", theme.ContentAddIcon(), func() {
+ go func() {
+ fyne.Do(func() {
+ aiButton.Disable()
+ aiButton.SetText("Generating...")
+ })
+ cap, err := ai.GenerateCaption(s.DataStore, s.selectedPic.Id)
+ if err != nil {
+ return
+ }
+ fyne.Do(func() {
+ s.titleEntry.SetText(cap.Title)
+ s.captionEntry.SetText(cap.Caption)
+ aiButton.Enable()
+ aiButton.SetText("Generate Caption")
+ })
+ }()
+
+ })
+ scrollContent = container.NewVBox(
+ titleRow,
+ s.imageStack,
+ aiButton,
+ form,
+ widget.NewSeparator(),
+ NewTextEntry("EXIF Details", 20),
+ s.exifCard,
+ )
+ } else {
+ scrollContent = container.NewVBox(
+ titleRow,
+ s.imageStack,
+ form,
+ widget.NewSeparator(),
+ NewTextEntry("EXIF Details", 20),
+ s.exifCard,
+ )
+ }
+
+ // Add padding and border
+ // padded := container.NewPadded(scrollContent)
+ card := widget.NewCard("", "", scrollContent)
+ s.container = container.NewVScroll(card)
+ s.container.Hide()
+ return s.container
+}
+
+// Refresh the sidebar UI (call this after ShowImage)
+func (s *Sidebar) Refresh() {
+ log.Println("Refreshing sidebar")
+ if s.visible {
+ s.container.Show()
+ } else {
+ s.container.Hide()
+ }
+}
+
+func (s *Sidebar) Hide() {
+ s.visible = false
+ s.container.Hide()
+}
+
+func (s *Sidebar) loadImage(pic datastore.Picture) {
+ file, err := s.ImageCache.Get(pic.Id, config.JPEG, "small")
+ if err != nil {
+ log.Println("Error loading image from cache:", err)
+ return
+ }
+ data, err := io.ReadAll(file)
+ if err != nil {
+ log.Println("Error reading image file:", err)
+ return
+ }
+ log.Printf("[Sidebar] Loaded image bytes: %d for %s", len(data), pic.Name)
+ if len(data) < 16 {
+ log.Println("[Sidebar] Image data too small or empty, not displaying.")
+ return
+ }
+ s.updateImageStack(data, pic)
+}
+
+func (s *Sidebar) updateImageStack(data []byte, pic datastore.Picture) {
+ newImg := canvas.NewImageFromReader(bytes.NewReader(data), pic.Name)
+ newImg.FillMode = canvas.ImageFillContain
+ width := float32(500)
+ height := width / pic.AspectRatio
+ newImg.SetMinSize(fyne.NewSize(width, height))
+ newImg.Resize(fyne.NewSize(width, height))
+
+ if s.imageStack != nil {
+ s.imageStack.Objects = []fyne.CanvasObject{newImg}
+ s.imageStack.Refresh()
+ }
+ s.image = newImg
+
+ if s.container != nil {
+ s.container.Refresh()
+ }
+}
+
+// ShowImage sets the sidebar image to the selected picture and refreshes the sidebar
+func (s *Sidebar) ShowImage(pic datastore.Picture) {
+ log.Println("Showing image:", pic.Id, pic.Name)
+ s.selectedPic = pic
+ s.visible = true
+ s.loadImage(pic)
+ s.titleEntry.SetText(pic.Name)
+ s.captionEntry.Text = pic.Caption
+
+ // Build EXIF info section
+ exifLabels := []fyne.CanvasObject{
+ widget.NewLabel("Camera: " + pic.Camera),
+ widget.NewLabel("Lens: " + pic.LensModel),
+ widget.NewLabel("F-Stop: " + pic.FStop),
+ widget.NewLabel("Shutter: " + pic.ShutterSpeed),
+ widget.NewLabel("ISO: " + pic.ISO),
+ widget.NewLabel("Focal Length: " + pic.FocalLength),
+ widget.NewLabel("Date Taken: " + pic.DateTaken.Format("2006-01-02 15:04:05")),
+ widget.NewLabel("Dimensions: " + pic.Dimension),
+ widget.NewLabel("Aspect Ratio: " + fmt.Sprintf("%.2f", pic.AspectRatio)),
+ widget.NewLabel("GPS: " + fmt.Sprintf("%.6f, %.6f", pic.GPSLat, pic.GPSLng)),
+ }
+ if s.exifCard != nil {
+ s.exifCard.Objects = exifLabels
+ s.exifCard.Refresh()
+ }
+ if s.container != nil {
+ s.container.Refresh()
+ }
+ s.Refresh()
+}
diff --git a/pkg/ui/monitors/ui_monitor.go b/pkg/ui/monitors/ui_monitor.go
new file mode 100644
index 0000000..7bc7aca
--- /dev/null
+++ b/pkg/ui/monitors/ui_monitor.go
@@ -0,0 +1,93 @@
+package uiMonitor
+
+import (
+ "gogallery/pkg/config"
+ "gogallery/pkg/monitor"
+ "sort"
+ "sync"
+
+ "fyne.io/fyne/v2"
+)
+
+type TaskUpdateListener func()
+
+// UIMonitor is a Monitor that notifies listeners on task changes
+// for UI updates.
+type UIMonitor struct {
+ Tasks map[string]*monitor.ProgressStats
+ listeners []TaskUpdateListener
+ mu sync.Mutex
+}
+
+func NewUIMonitor() *UIMonitor {
+ return &UIMonitor{
+ Tasks: make(map[string]*monitor.ProgressStats),
+ }
+}
+
+func (m *UIMonitor) NewTask(name string, total int) monitor.MonitorStat {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+ stat := monitor.NewProgressStats(name, total)
+ m.Tasks[name] = stat
+ m.notifyListeners()
+ return &uiProgressStat{ProgressStats: stat, parent: m}
+}
+
+func (m *UIMonitor) GetTasks() []monitor.MonitorStat {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+ keys := make([]string, 0, len(m.Tasks))
+ for k := range m.Tasks {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ values := make([]monitor.MonitorStat, 0, len(m.Tasks))
+ for _, k := range keys {
+ values = append(values, m.Tasks[k])
+ }
+ return values
+}
+
+// RegisterListener adds a callback to be called on task updates
+func (m *UIMonitor) RegisterListener(listener TaskUpdateListener) {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+ m.listeners = append(m.listeners, listener)
+}
+
+func (m *UIMonitor) notifyListeners() {
+ for _, l := range m.listeners {
+ go l() // call in goroutine to avoid blocking
+ }
+}
+
+// uiProgressStat wraps ProgressStats to notify parent monitor on update/complete
+// Implements MonitorStat
+
+type uiProgressStat struct {
+ *monitor.ProgressStats
+ parent *UIMonitor
+}
+
+func (u *uiProgressStat) Start() {
+ u.ProgressStats.Start()
+ u.parent.notifyListeners()
+}
+
+func (u *uiProgressStat) Update() {
+ u.ProgressStats.Update()
+ u.parent.notifyListeners()
+}
+
+func (u *uiProgressStat) Complete() {
+ u.ProgressStats.Complete()
+ // Send Fyne notification on task complete
+ if config.Config.UI.Notification {
+ fyne.CurrentApp().SendNotification(&fyne.Notification{
+ Title: "Task Complete",
+ Content: u.ProgressStats.Name + " finished successfully.",
+ })
+ }
+ u.parent.notifyListeners()
+}
diff --git a/pkg/ui/pages/gallery.go b/pkg/ui/pages/gallery.go
new file mode 100644
index 0000000..9e842db
--- /dev/null
+++ b/pkg/ui/pages/gallery.go
@@ -0,0 +1,81 @@
+package pages
+
+import (
+ "gogallery/pkg/datastore"
+ "gogallery/pkg/ui/components"
+ "log"
+
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/container"
+)
+
+type Page interface {
+ Layout() fyne.CanvasObject // Layout returns the main content
+}
+
+type GalleryPage struct {
+ Title string
+ db *datastore.DataStore
+ sidebar *components.Sidebar
+ gallery *components.ImageGrid
+ content *fyne.Container
+ galleryWidget fyne.CanvasObject
+ sidebarWidget fyne.CanvasObject
+}
+
+func NewGalleryPage(db *datastore.DataStore) *GalleryPage {
+ page := &GalleryPage{
+ Title: "Gallery",
+ db: db,
+ }
+ page.sidebar = components.NewSidebar(db, page.CloseSidebar)
+ // Pass the image selection callback to ImageGrid
+ page.gallery = components.NewImageGrid(db)
+ page.gallery.OnImageSelected = page.OnImageSelected
+ return page
+}
+
+// OnImageSelected is called when an image is clicked in the gallery
+func (g *GalleryPage) OnImageSelected(img datastore.Picture) {
+ g.sidebar.ShowImage(img)
+ if g.content != nil {
+ sidebarBox := container.NewStack(g.sidebarWidget)
+ g.content.Objects = []fyne.CanvasObject{
+ container.NewBorder(nil, nil, nil, sidebarBox, g.galleryWidget),
+ }
+ g.content.Refresh()
+ }
+}
+
+func (g *GalleryPage) FilterByAlbum(alb string) {
+ if pics, err := g.db.Pictures.FindByField("album_name", alb); err == nil {
+ g.gallery.SetImages(pics)
+ } else {
+ log.Println("Error filtering by album:", err)
+ }
+}
+
+func (g *GalleryPage) CloseSidebar() {
+ g.sidebar.Hide()
+ if g.content != nil {
+ g.content.Objects = []fyne.CanvasObject{
+ container.NewBorder(nil, nil, nil, nil, g.galleryWidget),
+ }
+ g.content.Refresh()
+ }
+}
+func (g *GalleryPage) Refresh() {
+ pics, err := g.db.Pictures.GetAll()
+ if err != nil {
+ panic(err)
+ }
+ g.gallery.SetImages(pics)
+}
+
+func (g *GalleryPage) Layout() fyne.CanvasObject {
+ g.Refresh()
+ g.galleryWidget = g.gallery.Layout()
+ g.sidebarWidget = g.sidebar.Layout()
+ g.content = container.NewBorder(nil, nil, nil, nil, g.galleryWidget)
+ return g.content
+}
diff --git a/pkg/ui/pages/settings.go b/pkg/ui/pages/settings.go
new file mode 100644
index 0000000..f8ba672
--- /dev/null
+++ b/pkg/ui/pages/settings.go
@@ -0,0 +1,297 @@
+package pages
+
+import (
+ "fmt"
+ "gogallery/pkg/ai"
+ "gogallery/pkg/config"
+ "gogallery/pkg/datastore"
+ "gogallery/pkg/ui/components"
+ "sort"
+ "strconv"
+
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/container"
+ "fyne.io/fyne/v2/widget"
+)
+
+type SettingsPage struct {
+ Title string
+ db *datastore.DataStore
+}
+
+func NewSettingsPage(db *datastore.DataStore) *SettingsPage {
+ return &SettingsPage{
+ Title: "Settings",
+ db: db,
+ }
+}
+
+func (s *SettingsPage) Layout() fyne.CanvasObject {
+ // Sidebar nav
+ nav := map[string]fyne.CanvasObject{
+ "Gallery": galleryConfigForm(),
+ "Author": aboutConfigForm(),
+ "Deployment": deployConfigForm(),
+ "Application": uiConfigForm(),
+ // "Albums": s.Albums(),
+ }
+
+ navItems := make([]string, 0, len(nav))
+ for item := range nav {
+ navItems = append(navItems, item)
+ }
+
+ sort.Slice(navItems, func(i, j int) bool {
+ return navItems[i] < navItems[j]
+ })
+
+ navList := widget.NewList(
+ func() int { return len(navItems) },
+ func() fyne.CanvasObject {
+ return widget.NewLabel("")
+ },
+ func(i widget.ListItemID, o fyne.CanvasObject) {
+ o.(*widget.Label).SetText(navItems[i])
+ },
+ )
+
+ // Fyne's widget.List does not support SetMinSize directly. Wrap in a container with MinSize.
+
+ // Content panels for each nav item
+
+ contentStack := container.NewPadded(nav["Gallery"])
+ navList.OnSelected = func(id int) {
+ contentStack.Objects = []fyne.CanvasObject{nav[navItems[id]]}
+ contentStack.Refresh()
+ }
+ navList.Select(0)
+
+ split := container.NewHSplit(container.NewPadded(navList), contentStack)
+ split.Offset = 0.33 // Reduce sidebar width
+ return split
+}
+
+func galleryConfigForm() fyne.CanvasObject {
+ cfg := config.Config.Gallery
+ name := widget.NewEntry()
+ name.SetText(cfg.Name)
+ theme := widget.NewEntry()
+ theme.SetText(cfg.Theme)
+ imagesPerPage := widget.NewEntry()
+ imagesPerPage.SetText(fmt.Sprintf("%d", cfg.ImagesPerPage))
+
+ basePath := widget.NewEntry()
+ basePath.SetText(cfg.Basepath)
+
+ destpath := widget.NewEntry()
+ destpath.SetText(cfg.Destpath)
+
+ form := &widget.Form{
+ Items: []*widget.FormItem{
+ {Text: "Name", Widget: name, HintText: "Gallery name"},
+ {Text: "Theme", Widget: theme, HintText: "Path to the theme dir"},
+ {Text: "Base Path", Widget: basePath, HintText: "Path to the gallery base directory"},
+ {Text: "Destination Path", Widget: destpath, HintText: "Path to the destination directory for images"},
+ {Text: "Images Per Page", Widget: imagesPerPage, HintText: "Number of images to display per page"},
+ },
+ OnCancel: func() {
+ name.SetText(cfg.Name)
+ theme.SetText(cfg.Theme)
+ imagesPerPage.SetText(fmt.Sprintf("%d", cfg.ImagesPerPage))
+ },
+ OnSubmit: func() {
+ cfg.Name = name.Text
+ cfg.Theme = theme.Text
+ if n, err := strconv.Atoi(imagesPerPage.Text); err == nil {
+ cfg.ImagesPerPage = n
+ }
+ cfg.Save()
+ },
+ }
+ title := components.NewTextEntry("Gallery Settings", 20)
+ return container.NewVBox(title, widget.NewSeparator(), form)
+}
+
+func aboutConfigForm() fyne.CanvasObject {
+ cfg := config.Config.About
+ twitter := widget.NewEntry()
+ twitter.SetText(cfg.Twitter)
+ facebook := widget.NewEntry()
+ facebook.SetText(cfg.Facebook)
+ email := widget.NewEntry()
+ email.SetText(cfg.Email)
+ instagram := widget.NewEntry()
+ instagram.SetText(cfg.Instagram)
+ description := widget.NewEntry()
+ description.MultiLine = true
+ description.Wrapping = fyne.TextWrapWord
+ description.SetMinRowsVisible(5)
+ description.SetText(cfg.Description)
+ footer := widget.NewEntry()
+ footer.SetText(cfg.Footer)
+ photographer := widget.NewEntry()
+ photographer.SetText(cfg.Photographer)
+ profilePhoto := widget.NewEntry()
+ profilePhoto.SetText(cfg.ProfilePhoto)
+ backgroundPhoto := widget.NewEntry()
+ backgroundPhoto.SetText(cfg.BackgroundPhoto)
+ blog := widget.NewEntry()
+ blog.SetText(cfg.Blog)
+ website := widget.NewEntry()
+ website.SetText(cfg.Website)
+ github := widget.NewEntry()
+ github.SetText(cfg.Github)
+
+ form := &widget.Form{
+ Items: []*widget.FormItem{
+ {Text: "Twitter", Widget: twitter, HintText: "Twitter handle"},
+ {Text: "Facebook", Widget: facebook, HintText: "Facebook page URL"},
+ {Text: "Email", Widget: email, HintText: "Contact email address"},
+ {Text: "Instagram", Widget: instagram, HintText: "Instagram handle"},
+ {Text: "Description", Widget: description, HintText: "Short description of the gallery"},
+ {Text: "Footer", Widget: footer, HintText: "Footer text for the gallery"},
+ {Text: "Photographer", Widget: photographer, HintText: "Name of the photographer"},
+ {Text: "Profile Photo", Widget: profilePhoto, HintText: "Path to the profile photo"},
+ {Text: "Background Photo", Widget: backgroundPhoto, HintText: "Path to the background photo"},
+ {Text: "Blog", Widget: blog, HintText: "Link to the photographer's blog"},
+ {Text: "Website", Widget: website, HintText: "Link to the photographer's website"},
+ {Text: "Github", Widget: github, HintText: "Link to the photographer's GitHub profile"},
+ },
+ OnCancel: func() {
+ twitter.SetText(cfg.Twitter)
+ facebook.SetText(cfg.Facebook)
+ email.SetText(cfg.Email)
+ instagram.SetText(cfg.Instagram)
+ description.SetText(cfg.Description)
+ footer.SetText(cfg.Footer)
+ photographer.SetText(cfg.Photographer)
+ profilePhoto.SetText(cfg.ProfilePhoto)
+ backgroundPhoto.SetText(cfg.BackgroundPhoto)
+ blog.SetText(cfg.Blog)
+ website.SetText(cfg.Website)
+ github.SetText(cfg.Github)
+ },
+ OnSubmit: func() {
+ cfg.Twitter = twitter.Text
+ cfg.Facebook = facebook.Text
+ cfg.Email = email.Text
+ cfg.Instagram = instagram.Text
+ cfg.Description = description.Text
+ cfg.Footer = footer.Text
+ cfg.Photographer = photographer.Text
+ cfg.ProfilePhoto = profilePhoto.Text
+ cfg.BackgroundPhoto = backgroundPhoto.Text
+ cfg.Blog = blog.Text
+ cfg.Website = website.Text
+ cfg.Github = github.Text
+ cfg.Save()
+ },
+ }
+ title := components.NewTextEntry("Author Settings", 20)
+ scrollForm := container.NewVScroll(container.NewPadded(form))
+ return container.NewBorder(
+ container.NewVBox(title, widget.NewSeparator()), // top
+ nil, // bottom
+ nil, // left
+ nil, // right
+ scrollForm, // center (fills remaining space)
+ )
+}
+
+func deployConfigForm() fyne.CanvasObject {
+ cfg := config.Config.Deploy
+ siteId := widget.NewEntry()
+ siteId.SetText(cfg.SiteId)
+ authToken := widget.NewEntry()
+ authToken.SetText(cfg.AuthToken)
+ draft := widget.NewCheck("Draft", func(b bool) { cfg.Draft = b })
+ draft.SetChecked(cfg.Draft)
+ form := &widget.Form{
+ Items: []*widget.FormItem{
+ {Text: "Site ID", Widget: siteId, HintText: "Your site ID from the deployment service"},
+ {Text: "Auth Token", Widget: authToken, HintText: "Your authentication token for the deployment service"},
+ {Text: "Draft", Widget: draft, HintText: "Enable draft mode for deployments"},
+ },
+ OnCancel: func() {
+ siteId.SetText(cfg.SiteId)
+ authToken.SetText(cfg.AuthToken)
+ draft.SetChecked(cfg.Draft)
+ },
+ OnSubmit: func() {
+ cfg.SiteId = siteId.Text
+ cfg.AuthToken = authToken.Text
+ cfg.Draft = draft.Checked
+ cfg.Save()
+ },
+ }
+ title := components.NewTextEntry("Deployment Settings", 20)
+ return container.NewVBox(title, widget.NewSeparator(), form)
+}
+
+func uiConfigForm() fyne.CanvasObject {
+ cfg := &config.Config.UI
+ // Theme selection: Light or Dark
+ themeOptions := []string{"light", "dark"}
+ themeSelect := widget.NewRadioGroup(themeOptions, func(selected string) {
+ if selected == "light" {
+ cfg.Theme = "light"
+ } else {
+ cfg.Theme = "dark"
+ }
+ })
+ themeSelect.SetSelected(cfg.Theme)
+
+ notifications := widget.NewCheck("Enable Notifications", func(b bool) {
+ cfg.Notification = b
+ })
+ notifications.SetChecked(cfg.Notification)
+
+ // Preview public checkbox (add PublicPreview to UIConfiguration if not present)
+ previewPublic := widget.NewCheck("Preview Public", func(b bool) {
+ cfg.Public = b
+ })
+ if v, ok := any(cfg).(interface{ GetPublicPreview() bool }); ok {
+ previewPublic.SetChecked(v.GetPublicPreview())
+ } else {
+ previewPublic.SetChecked(cfg.Public)
+ }
+ ApiKeyEntry := widget.NewEntry()
+ ApiKeyEntry.SetPlaceHolder("Enter Gemini API Key")
+ ApiKeyEntry.SetText(cfg.GeminiApiKey)
+
+ // Setting the number of images per page
+ imagesPerPage := widget.NewEntry()
+ imagesPerPage.SetText(fmt.Sprintf("%d", cfg.ImagesPerPage))
+ imagesPerPage.OnChanged = func(text string) {
+ if n, err := strconv.Atoi(text); err == nil {
+ cfg.ImagesPerPage = n
+ } else {
+ imagesPerPage.SetText(fmt.Sprintf("%d", cfg.ImagesPerPage))
+ }
+ }
+
+ form := &widget.Form{
+ Items: []*widget.FormItem{
+ {Text: "Theme", Widget: themeSelect, HintText: "Select the application theme requires application restart"},
+ {Text: "Notifications", Widget: notifications, HintText: "Enable or disable notifications"},
+ {Text: "Public Preview", Widget: previewPublic, HintText: "Enable or disable public preview"},
+ {Text: "Images Per Page", Widget: imagesPerPage, HintText: "Number of images to display per page"},
+ {Text: "Gemini API Key", Widget: ApiKeyEntry, HintText: "Enter your Gemini API key to enable AI features"},
+ },
+ OnCancel: func() {
+ notifications.SetChecked(cfg.Notification)
+ previewPublic.SetChecked(cfg.Public)
+ },
+ OnSubmit: func() {
+ cfg.Notification = notifications.Checked
+ cfg.Public = previewPublic.Checked
+ cfg.GeminiApiKey = ApiKeyEntry.Text
+ config.Config.Save()
+ if ApiKeyEntry.Text != "" {
+ ai.RegisterGeminiClient()
+ }
+ },
+ }
+ title := components.NewTextEntry("Application Settings", 20)
+ return container.NewVBox(title, widget.NewSeparator(), form)
+}
diff --git a/pkg/ui/pages/tasks.go b/pkg/ui/pages/tasks.go
new file mode 100644
index 0000000..fe0f89f
--- /dev/null
+++ b/pkg/ui/pages/tasks.go
@@ -0,0 +1,187 @@
+package pages
+
+import (
+ "gogallery/pkg/config"
+ "gogallery/pkg/datastore"
+ "gogallery/pkg/deploy"
+ "gogallery/pkg/monitor"
+ "gogallery/pkg/pipeline"
+ "gogallery/pkg/preview"
+ "time"
+
+ uiMonitor "gogallery/pkg/ui/monitors"
+
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/container"
+ "fyne.io/fyne/v2/widget"
+)
+
+type TasksPage struct {
+ *datastore.DataStore
+ Title string
+ monitor *uiMonitor.UIMonitor
+ table *fyne.Container
+ btnGrid *fyne.Container // Action buttons grid
+ server *preview.Server
+}
+
+var cfg = config.Config
+
+func NewTasksPage(db *datastore.DataStore, server *preview.Server) *TasksPage {
+ uiMonitor, ok := db.Monitor.(*uiMonitor.UIMonitor)
+ if !ok {
+ panic("db.Monitor is not of type UIMonitor")
+ }
+ page := &TasksPage{
+ Title: "Tasks",
+ DataStore: db,
+ server: server,
+ monitor: uiMonitor, // Ensure db.Monitor is of type UIMonitor
+ table: nil, // Will be initialized in Layout
+ }
+ page.init() // Initialize buttons and table
+
+ uiMonitor.RegisterListener(func() {
+ fyne.Do(func() {
+ page.Refresh() // Refresh the page when tasks are updated
+ })
+ })
+ return page
+}
+
+func (t *TasksPage) init() {
+ // --- Action Buttons ---
+ rescanBtn := widget.NewButton("Rescan", func() {
+ go t.ScanPath(cfg.Gallery.Basepath)
+ })
+ deleteBtn := widget.NewButton("Delete Site", func() {
+ stat := t.monitor.NewTask("Delete Site", 0)
+ go func() {
+ stat.Start()
+ defer stat.Complete()
+ pipeline.NewRenderPipeline(&cfg.Gallery, t.DataStore).DeleteSite()
+ t.DataStore.Reset() // Reset datastore after deletion
+ }()
+ })
+ buildBtn := widget.NewButton("Build Site", func() {
+ go pipeline.NewRenderPipeline(&cfg.Gallery, t.DataStore).BuildSite()
+ })
+ deployBtn := widget.NewButton("Deploy Site", func() {
+ go deploy.DeploySite(*cfg, t.NewTask("netify deploy", 1))
+ })
+
+ startServerBtn := widget.NewButton("Start Preview Server", func() {
+ go t.server.Start()
+ })
+ stopServerBtn := widget.NewButton("Stop Preview Server", func() {
+ go t.server.Stop()
+ })
+
+ // Button grid in a centered, fixed-width box
+ t.btnGrid = container.NewGridWithColumns(2,
+ rescanBtn, deleteBtn,
+ buildBtn, deployBtn,
+ startServerBtn, stopServerBtn,
+ )
+
+ t.table = craeteTable([]fyne.CanvasObject{})
+}
+
+func tableHeader() fyne.CanvasObject {
+ return container.NewGridWithColumns(5,
+ widget.NewLabelWithStyle("Task Name", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
+ widget.NewLabelWithStyle("Status", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
+ widget.NewLabelWithStyle("Started At", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
+ widget.NewLabelWithStyle("Time Taken", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
+ widget.NewLabelWithStyle("Progress", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
+ )
+}
+
+func craeteTable(r []fyne.CanvasObject) *fyne.Container {
+ rows := []fyne.CanvasObject{
+ tableHeader(),
+ }
+ rows = append(rows, r...)
+ return container.NewVBox(rows...)
+}
+
+func (t *TasksPage) Refresh() {
+ tasks := t.monitor.GetTasks()
+ rows := []fyne.CanvasObject{
+ tableHeader(),
+ }
+ for _, task := range tasks {
+ rows = append(rows, t.createTaskRow(task))
+ }
+ t.table.Objects = rows
+ t.table.Refresh() // Refresh the table to show new rows
+}
+
+func (t *TasksPage) createTaskRow(task interface{}) fyne.CanvasObject {
+ var (
+ name, status, startedAt, timeTaken string
+ percent float64
+ )
+ if ps, ok := task.(*monitor.ProgressStats); ok {
+ name = ps.Name
+ status = t.getTaskStatus(ps.State)
+ startedAt = t.getTaskStartTime(ps)
+ timeTaken = t.getTaskTimeTaken(ps)
+ percent = ps.Percent() / 100.0
+ } else {
+ name = "Unknown"
+ status = "-"
+ startedAt = "-"
+ timeTaken = "-"
+ percent = 0
+ }
+ progress := widget.NewProgressBar()
+ progress.SetValue(percent)
+ return container.NewGridWithColumns(5,
+ widget.NewLabel(name),
+ widget.NewLabel(status),
+ widget.NewLabel(startedAt),
+ widget.NewLabel(timeTaken),
+ progress,
+ )
+}
+
+func (t *TasksPage) getTaskStatus(state monitor.ProssesState) string {
+ switch state {
+ case monitor.COMPLETE:
+ return "Complete"
+ case monitor.RUNNING:
+ return "In Progress"
+ case monitor.ERROR:
+ return "Error"
+ default:
+ return string(state)
+ }
+}
+
+func (t *TasksPage) getTaskStartTime(ps *monitor.ProgressStats) string {
+ if !ps.StartTime.IsZero() {
+ return ps.StartTime.Format("15:04:05")
+ }
+ return "-"
+}
+
+func (t *TasksPage) getTaskTimeTaken(ps *monitor.ProgressStats) string {
+ if ps.State == monitor.COMPLETE && !ps.EndTime.IsZero() {
+ return ps.Duration.String()
+ } else if ps.State == monitor.RUNNING && !ps.StartTime.IsZero() {
+ return time.Since(ps.StartTime).Truncate(time.Second).String()
+ }
+ return "-"
+}
+
+func (t *TasksPage) Layout() fyne.CanvasObject {
+
+ content := container.NewBorder(
+ container.NewPadded(t.btnGrid),
+ nil,
+ nil, nil,
+ container.NewPadded(container.NewVScroll(t.table)),
+ )
+ return content
+}
diff --git a/pkg/ui/theme.go b/pkg/ui/theme.go
new file mode 100644
index 0000000..182e154
--- /dev/null
+++ b/pkg/ui/theme.go
@@ -0,0 +1,100 @@
+package ui
+
+import (
+ "image/color"
+
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/theme"
+)
+
+// ComfortableTheme is a custom theme with increased spacing and padding for a less compact UI
+type ComfortableTheme struct {
+ fyne.Theme
+ variant string // "light" or "dark"
+}
+
+// NewComfortableTheme creates a new ComfortableTheme with the specified variant
+func NewComfortableTheme(variant string) *ComfortableTheme {
+ return &ComfortableTheme{
+ variant: variant,
+ }
+}
+
+func (c *ComfortableTheme) Color(name fyne.ThemeColorName, v fyne.ThemeVariant) color.Color {
+ // Refactored: use a map for color lookups to reduce complexity
+ isDark := c.variant == "dark"
+ if isDark {
+ colorMap := map[fyne.ThemeColorName]color.Color{
+ theme.ColorNameButton: color.RGBA{R: 0, G: 168, B: 89, A: 255}, // Rich Green (dark)
+ theme.ColorNamePrimary: color.RGBA{R: 0, G: 168, B: 89, A: 255}, // Less bright green for progress bar
+ theme.ColorNameSelection: color.RGBA{R: 0, G: 168, B: 89, A: 180}, // Green selection for list highlight (dark)
+ theme.ColorNameBackground: color.RGBA{R: 8, G: 12, B: 8, A: 255}, // Nearly black background (dark)
+ theme.ColorNameForeground: color.RGBA{R: 255, G: 255, B: 255, A: 255}, // White text
+ theme.ColorNameInputBackground: color.RGBA{R: 20, G: 28, B: 24, A: 255}, // Deep, neutral green-gray for input background
+ theme.ColorNameInputBorder: color.RGBA{R: 0, G: 100, B: 50, A: 255}, // Darker green border
+ theme.ColorNameScrollBar: color.RGBA{R: 0, G: 168, B: 89, A: 255}, // Consistent green scroll bar
+ theme.ColorNameShadow: color.RGBA{R: 0, G: 0, B: 0, A: 0}, // Transparent shadows
+ theme.ColorNameSeparator: color.RGBA{R: 30, G: 30, B: 30, A: 200}, // Blackish separator
+ theme.ColorNameMenuBackground: color.RGBA{R: 12, G: 24, B: 12, A: 255}, // Sidebar bg
+ theme.ColorNameOverlayBackground: color.RGBA{R: 0, G: 200, B: 83, A: 100}, // Card border
+ theme.ColorNameHover: color.RGBA{R: 40, G: 40, B: 40, A: 10}, // Darker gray for button/input hover
+ }
+ if col, ok := colorMap[name]; ok {
+ return col
+ }
+ } else {
+ colorMapLight := map[fyne.ThemeColorName]color.Color{
+ theme.ColorNameButton: color.RGBA{R: 0, G: 168, B: 89, A: 255}, // Rich Green (light)
+ theme.ColorNamePrimary: color.RGBA{R: 0, G: 168, B: 89, A: 255}, // Less bright green for progress bar
+ theme.ColorNameSelection: color.RGBA{R: 0, G: 168, B: 89, A: 180}, // Green selection for list highlight (light)
+ theme.ColorNameBackground: color.RGBA{R: 250, G: 252, B: 245, A: 255}, // Offwhite background (light)
+ theme.ColorNameForeground: color.RGBA{R: 30, G: 60, B: 30, A: 255}, // Deep green text
+ theme.ColorNameInputBackground: color.RGBA{R: 240, G: 255, B: 240, A: 255}, // Slightly green-tinted offwhite
+ theme.ColorNameInputBorder: color.RGBA{R: 0, G: 200, B: 83, A: 255}, // Consistent Rich Green border
+ theme.ColorNameScrollBar: color.RGBA{R: 0, G: 168, B: 89, A: 255}, // Consistent green scroll bar
+ theme.ColorNameShadow: color.RGBA{R: 0, G: 0, B: 0, A: 0}, // Transparent shadows
+ theme.ColorNameSeparator: color.RGBA{R: 30, G: 30, B: 30, A: 200}, // Blackish separator
+ theme.ColorNameMenuBackground: color.RGBA{R: 220, G: 255, B: 220, A: 255}, // Sidebar bg
+ theme.ColorNameOverlayBackground: color.RGBA{R: 0, G: 200, B: 83, A: 100}, // Card border
+ theme.ColorNameHover: color.RGBA{R: 40, G: 40, B: 40, A: 100}, // Darker gray for button/input hover
+ }
+ if col, ok := colorMapLight[name]; ok {
+ return col
+ }
+ }
+ return theme.DefaultTheme().Color(name, v)
+}
+
+func (c *ComfortableTheme) Font(style fyne.TextStyle) fyne.Resource {
+ // Use default theme fonts
+ return theme.DefaultTheme().Font(style)
+}
+
+func (c *ComfortableTheme) Icon(name fyne.ThemeIconName) fyne.Resource {
+ // Use default theme icons
+ return theme.DefaultTheme().Icon(name)
+}
+
+func (c *ComfortableTheme) Size(name fyne.ThemeSizeName) float32 {
+ switch name {
+ case theme.SizeNamePadding:
+ return 10 // Increased padding for a more comfortable layout
+ case theme.SizeNameInnerPadding:
+ return 12 // Increased inner padding for select and similar widgets
+ case theme.SizeNameInputRadius:
+ return 14
+ case theme.SizeNameSeparatorThickness:
+ return 1 // Thicker separators for sidebar borders
+ case theme.SizeNameText:
+ return 16 // Slightly larger text for better readability
+ case theme.SizeNameScrollBar:
+ return 4 // Thicker scroll bar for easier interaction
+ case theme.SizeNameScrollBarSmall:
+ return 2 // Smaller scroll bar
+ default:
+ return theme.DefaultTheme().Size(name)
+ }
+}
+func (c *ComfortableTheme) Variant() string {
+ return c.variant
+}
diff --git a/pkg/ui/utils/notify.go b/pkg/ui/utils/notify.go
new file mode 100644
index 0000000..3ddab1b
--- /dev/null
+++ b/pkg/ui/utils/notify.go
@@ -0,0 +1,16 @@
+package utils
+
+import (
+ "gogallery/pkg/config"
+
+ "fyne.io/fyne/v2"
+)
+
+func Notify(tite string, msg string) {
+ if config.Config.UI.Notification {
+ fyne.CurrentApp().SendNotification(&fyne.Notification{
+ Title: tite,
+ Content: msg,
+ })
+ }
+}
diff --git a/themes/DEVELOPER_README.md b/themes/DEVELOPER_README.md
new file mode 100644
index 0000000..0070197
--- /dev/null
+++ b/themes/DEVELOPER_README.md
@@ -0,0 +1,180 @@
+# Developer Guide: Building New GoGallery Templates
+
+This guide explains how to create and structure new templates for GoGallery themes. GoGallery uses Go's `html/template` engine with files ending in `.tmpl.html` for all template pages and partials.
+
+## Template File Structure
+
+A typical theme directory looks like this:
+
+```
+themes/
+ ThemeName/
+ default.tmpl.html # Base layout template (required)
+ pages/
+ index.tmpl.html # Home page
+ albums.tmpl.html # Albums page
+ photo.tmpl.html # Photo detail page
+ ...
+ partials/
+ header.tmpl.html # Header partial
+ footer.tmpl.html # Footer partial
+ ...
+ assets/ # Static assets (css, js, images)
+```
+
+## Naming Conventions
+- All template files must use the `.tmpl.html` extension.
+- Page templates go in the `pages/` directory.
+- Reusable partials go in the `partials/` directory.
+- The base layout must be named `default.tmpl.html` and reside at the root of the theme.
+
+## Template Syntax
+- GoGallery templates use Go's `html/template` syntax:
+ - `{{ define "main" }}` ... `{{ end }}` for main content blocks.
+ - Use `{{ template "partialName" . }}` to include partials.
+ - Access data via dot notation, e.g., `{{.Picture.Name}}`.
+- You can use Go template logic: `{{if ...}}`, `{{range ...}}`, etc.
+
+## Adding a New Page Template
+1. Create a new file in `pages/`, e.g., `about.tmpl.html`.
+2. Start with a `define` block:
+ ```gotmpl
+ {{ define "main" }}
+
+ {{ end }}
+ ```
+3. Reference the new page in your Go code or navigation as needed.
+
+## Adding/Using Partials
+- Place reusable components in `partials/` (e.g., `header.tmpl.html`).
+- Include them in your pages or layout with:
+ ```gotmpl
+ {{ template "header" . }}
+ ```
+
+## Data Available in Templates
+
+Below are the most common variables available in templates, depending on the page type:
+
+### Home Page (`index.tmpl.html`)
+- `.Albums` — List of all albums (array of Album objects)
+- `.FeaturedAlbum` — The featured album (Album object)
+- `.Title` — Page title (string)
+
+### Albums Page (`albums.tmpl.html`)
+- `.Albums` — List of all albums (array of Album objects)
+- `.Title` — Page title (string)
+
+### Collections Page (`collections.tmpl.html`)
+- `.Collections` — List of all collections (array of Collection objects)
+- `.Title` — Page title (string)
+
+### Photo Page (`photo.tmpl.html`)
+- `.Picture` — The current photo (Picture object)
+ - `.Name` — Photo name/title
+ - `.Caption` — Photo caption/description
+ - `.Id` — Photo ID
+ - `.Album` — Album ID
+ - `.AlbumName` — Album name
+ - `.DateTaken` — Date/time photo was taken
+ - `.Camera`, `.LensModel`, `.FStop`, `.ShutterSpeed`, `.FocalLength`, `.ISO`, `.ColorSpace`, `.MeteringMode`, `.Software`, `.Saturation`, `.Contrast`, `.Sharpness`, `.Temperature`, `.WhiteBalance` — EXIF/technical fields
+- `.PreImagePath` — Path to previous photo (string)
+- `.NextImagePath` — Path to next photo (string)
+
+### Pagination Page (`pagination.tmpl.html`)
+- `.Items` — List of paginated items (array)
+- `.CurrentPage` — Current page number (int)
+- `.TotalPages` — Total number of pages (int)
+
+### Common Variables (all pages)
+- `.Theme` — Current theme name (string)
+- `.BaseURL` — Base URL of the site (string)
+- `.User` — Current user (if applicable)
+
+> For a full list of available fields, see the Go structs in the GoGallery codebase (e.g., `Album`, `Picture`, `Collection`).
+
+## Image Usage and Optimization
+
+GoGallery automatically generates multiple image sizes for each photo to optimize loading and display across devices. To ensure the best performance and user experience, follow these guidelines:
+
+### Available Image Sizes
+- `xsmall.webp` — Extra small (thumbnails, mobile)
+- `small.webp` — Small (previews, grid views)
+- `medium.webp` — Medium (main content, cards)
+- `large.webp` — Large (featured images, banners)
+- `xlarge.webp` — Extra large (fullscreen, detail views)
+
+All images are served in modern formats (WebP) for best compression and quality.
+
+### How to Use Responsive Images
+Use the `` element and `srcset` to serve the appropriate image size for each device:
+
+```html
+
+
+
+
+```
+- The browser will pick the best image size based on device and layout.
+- Always provide an `alt` attribute for accessibility and SEO.
+- Use `loading="lazy"` for images not immediately visible on page load.
+
+### Tips for Template Authors
+- Use the smallest image size that looks good for the context (e.g., `xsmall` for thumbnails, `large` or `xlarge` for fullscreen or hero images).
+- Use the `` element for art direction or to provide multiple formats.
+- Use CSS classes like `object-cover` or `object-contain` for proper scaling.
+- Avoid using original, unoptimized images directly in templates.
+
+### Example: Responsive Gallery Grid
+```gotmpl
+{{ range .Albums }}
+
+
+
+
+
+ {{.Name}}
+
+{{ end }}
+```
+
+### Example: Looping Over All Image Sizes
+
+GoGallery provides an `ImgSizes` function in templates, which returns a map of available image sizes. You can use this to generate `` elements for all sizes:
+
+```gotmpl
+
+ {{ range $size, $info := ImgSizes }}
+
+ {{ end }}
+
+
+```
+- `$size` is the size key (e.g., `xsmall`, `small`, `medium`, etc.).
+- `$info.Media` is an optional media query string for responsive art direction (if defined in your Go code).
+- Always provide a fallback ` ` tag for browsers that do not support ``.
+
+> See your Go code for the exact structure of `ImgSizes` and available media queries.
+
+By looping over `ImgSizes`, your templates will automatically support all configured image sizes and future changes.
+
+## Tips
+- Use Tailwind CSS classes for styling (if your theme supports it).
+- Test your templates by running the GoGallery server and navigating to the relevant pages.
+- Use Go template comments (`{{/* comment */}}`) to annotate your templates.
+
+## Example: Simple Page Template
+```gotmpl
+{{ define "main" }}
+{{.Title}}
+Welcome to my custom page!
+{{ end }}
+```
+
+## Troubleshooting
+- If your template does not render, check for syntax errors or missing `define` blocks.
+- Ensure all template files use the `.tmpl.html` extension.
+- Review the GoGallery logs for error messages.
+
+---
+For more advanced usage, see the Go `html/template` documentation: https://pkg.go.dev/html/template
diff --git a/themes/eastnor/assets/css/gallery.css b/themes/Eastnor/assets/css/gallery.css
similarity index 100%
rename from themes/eastnor/assets/css/gallery.css
rename to themes/Eastnor/assets/css/gallery.css
diff --git a/themes/eastnor/assets/css/spinner.css b/themes/Eastnor/assets/css/spinner.css
similarity index 100%
rename from themes/eastnor/assets/css/spinner.css
rename to themes/Eastnor/assets/css/spinner.css
diff --git a/themes/eastnor/assets/img/icons/albums.svg b/themes/Eastnor/assets/img/icons/albums.svg
similarity index 100%
rename from themes/eastnor/assets/img/icons/albums.svg
rename to themes/Eastnor/assets/img/icons/albums.svg
diff --git a/themes/eastnor/assets/img/icons/apature.svg b/themes/Eastnor/assets/img/icons/apature.svg
similarity index 100%
rename from themes/eastnor/assets/img/icons/apature.svg
rename to themes/Eastnor/assets/img/icons/apature.svg
diff --git a/themes/eastnor/assets/img/icons/camera.svg b/themes/Eastnor/assets/img/icons/camera.svg
similarity index 100%
rename from themes/eastnor/assets/img/icons/camera.svg
rename to themes/Eastnor/assets/img/icons/camera.svg
diff --git a/themes/eastnor/assets/img/icons/collection.svg b/themes/Eastnor/assets/img/icons/collection.svg
similarity index 100%
rename from themes/eastnor/assets/img/icons/collection.svg
rename to themes/Eastnor/assets/img/icons/collection.svg
diff --git a/themes/eastnor/assets/img/icons/focal-length.svg b/themes/Eastnor/assets/img/icons/focal-length.svg
similarity index 100%
rename from themes/eastnor/assets/img/icons/focal-length.svg
rename to themes/Eastnor/assets/img/icons/focal-length.svg
diff --git a/themes/eastnor/assets/img/icons/iso.svg b/themes/Eastnor/assets/img/icons/iso.svg
similarity index 100%
rename from themes/eastnor/assets/img/icons/iso.svg
rename to themes/Eastnor/assets/img/icons/iso.svg
diff --git a/themes/eastnor/assets/img/icons/lens.svg b/themes/Eastnor/assets/img/icons/lens.svg
similarity index 100%
rename from themes/eastnor/assets/img/icons/lens.svg
rename to themes/Eastnor/assets/img/icons/lens.svg
diff --git a/themes/eastnor/assets/img/icons/teapot.svg b/themes/Eastnor/assets/img/icons/teapot.svg
similarity index 100%
rename from themes/eastnor/assets/img/icons/teapot.svg
rename to themes/Eastnor/assets/img/icons/teapot.svg
diff --git a/themes/eastnor/assets/img/icons/timer.svg b/themes/Eastnor/assets/img/icons/timer.svg
similarity index 100%
rename from themes/eastnor/assets/img/icons/timer.svg
rename to themes/Eastnor/assets/img/icons/timer.svg
diff --git a/themes/eastnor/assets/img/logo.png b/themes/Eastnor/assets/img/logo.png
similarity index 100%
rename from themes/eastnor/assets/img/logo.png
rename to themes/Eastnor/assets/img/logo.png
diff --git a/frontend/src/components/loading/placeholder.png b/themes/Eastnor/assets/img/placeholder.png
similarity index 100%
rename from frontend/src/components/loading/placeholder.png
rename to themes/Eastnor/assets/img/placeholder.png
diff --git a/themes/eastnor/assets/js/fslightbox.js b/themes/Eastnor/assets/js/fslightbox.js
similarity index 100%
rename from themes/eastnor/assets/js/fslightbox.js
rename to themes/Eastnor/assets/js/fslightbox.js
diff --git a/themes/eastnor/assets/js/sw.js b/themes/Eastnor/assets/js/sw.js
similarity index 100%
rename from themes/eastnor/assets/js/sw.js
rename to themes/Eastnor/assets/js/sw.js
diff --git a/themes/eastnor/assets/logos/favicon.ico b/themes/Eastnor/assets/logos/favicon.ico
similarity index 100%
rename from themes/eastnor/assets/logos/favicon.ico
rename to themes/Eastnor/assets/logos/favicon.ico
diff --git a/themes/Eastnor/assets/logos/logo192.png b/themes/Eastnor/assets/logos/logo192.png
new file mode 100644
index 0000000..683e86e
Binary files /dev/null and b/themes/Eastnor/assets/logos/logo192.png differ
diff --git a/themes/eastnor/assets/logos/logo512.png b/themes/Eastnor/assets/logos/logo512.png
similarity index 100%
rename from themes/eastnor/assets/logos/logo512.png
rename to themes/Eastnor/assets/logos/logo512.png
diff --git a/themes/eastnor/default.hbs b/themes/Eastnor/default.tmpl.html
similarity index 97%
rename from themes/eastnor/default.hbs
rename to themes/Eastnor/default.tmpl.html
index 246afda..eb91dea 100644
--- a/themes/eastnor/default.hbs
+++ b/themes/Eastnor/default.tmpl.html
@@ -29,7 +29,7 @@
-
+
diff --git a/themes/Eastnor/package.json b/themes/Eastnor/package.json
new file mode 100644
index 0000000..7344b40
--- /dev/null
+++ b/themes/Eastnor/package.json
@@ -0,0 +1,6 @@
+{
+ "scripts": {
+ "build": "echo 'No build script defined for this theme.'",
+ "clean": "rm -rf ./node_modules && rm -rf ./package-lock.json"
+ }
+}
diff --git a/themes/Eastnor/pages/404.tmpl.html b/themes/Eastnor/pages/404.tmpl.html
new file mode 100644
index 0000000..839ac8c
--- /dev/null
+++ b/themes/Eastnor/pages/404.tmpl.html
@@ -0,0 +1 @@
+// ...existing code...
diff --git a/themes/Eastnor/pages/albums.tmpl.html b/themes/Eastnor/pages/albums.tmpl.html
new file mode 100644
index 0000000..839ac8c
--- /dev/null
+++ b/themes/Eastnor/pages/albums.tmpl.html
@@ -0,0 +1 @@
+// ...existing code...
diff --git a/themes/Eastnor/pages/collections.tmpl.html b/themes/Eastnor/pages/collections.tmpl.html
new file mode 100644
index 0000000..839ac8c
--- /dev/null
+++ b/themes/Eastnor/pages/collections.tmpl.html
@@ -0,0 +1 @@
+// ...existing code...
diff --git a/themes/Eastnor/pages/index.tmpl.html b/themes/Eastnor/pages/index.tmpl.html
new file mode 100644
index 0000000..839ac8c
--- /dev/null
+++ b/themes/Eastnor/pages/index.tmpl.html
@@ -0,0 +1 @@
+// ...existing code...
diff --git a/themes/Eastnor/pages/pagination.tmpl.html b/themes/Eastnor/pages/pagination.tmpl.html
new file mode 100644
index 0000000..839ac8c
--- /dev/null
+++ b/themes/Eastnor/pages/pagination.tmpl.html
@@ -0,0 +1 @@
+// ...existing code...
diff --git a/themes/Eastnor/pages/photo.tmpl.html b/themes/Eastnor/pages/photo.tmpl.html
new file mode 100644
index 0000000..839ac8c
--- /dev/null
+++ b/themes/Eastnor/pages/photo.tmpl.html
@@ -0,0 +1 @@
+// ...existing code...
diff --git a/themes/Eastnor/partials/album.tmpl.html b/themes/Eastnor/partials/album.tmpl.html
new file mode 100644
index 0000000..839ac8c
--- /dev/null
+++ b/themes/Eastnor/partials/album.tmpl.html
@@ -0,0 +1 @@
+// ...existing code...
diff --git a/themes/Eastnor/partials/gallery.tmpl.html b/themes/Eastnor/partials/gallery.tmpl.html
new file mode 100644
index 0000000..839ac8c
--- /dev/null
+++ b/themes/Eastnor/partials/gallery.tmpl.html
@@ -0,0 +1 @@
+// ...existing code...
diff --git a/themes/Eastnor/partials/header.tmpl.html b/themes/Eastnor/partials/header.tmpl.html
new file mode 100644
index 0000000..839ac8c
--- /dev/null
+++ b/themes/Eastnor/partials/header.tmpl.html
@@ -0,0 +1 @@
+// ...existing code...
diff --git a/themes/EmeraldNoir/assets/css/style.css b/themes/EmeraldNoir/assets/css/style.css
new file mode 100644
index 0000000..fc61cd2
--- /dev/null
+++ b/themes/EmeraldNoir/assets/css/style.css
@@ -0,0 +1,2 @@
+/*! tailwindcss v4.1.11 | MIT License | https://tailwindcss.com */
+@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-emerald-100:oklch(95% .052 163.051);--color-emerald-200:oklch(90.5% .093 164.15);--color-emerald-300:oklch(84.5% .143 164.978);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-900:oklch(37.8% .077 168.94);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-2xl:42rem;--container-3xl:48rem;--container-6xl:72rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25/1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5/2.25);--text-5xl:3rem;--text-5xl--line-height:1;--text-7xl:4.5rem;--text-7xl--line-height:1;--font-weight-light:300;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wide:.025em;--tracking-wider:.05em;--leading-tight:1.25;--leading-relaxed:1.625;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--blur-sm:8px;--blur-xl:24px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.inset-0{inset:calc(var(--spacing)*0)}.top-1\/2{top:50%}.top-4{top:calc(var(--spacing)*4)}.top-20{top:calc(var(--spacing)*20)}.right-0{right:calc(var(--spacing)*0)}.right-1\/3{right:33.3333%}.right-1\/4{right:25%}.right-4{right:calc(var(--spacing)*4)}.bottom-0{bottom:calc(var(--spacing)*0)}.bottom-20{bottom:calc(var(--spacing)*20)}.left-0{left:calc(var(--spacing)*0)}.left-1\/4{left:25%}.left-4{left:calc(var(--spacing)*4)}.z-10{z-index:10}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-4{margin-top:calc(var(--spacing)*4)}.mt-6{margin-top:calc(var(--spacing)*6)}.mt-12{margin-top:calc(var(--spacing)*12)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-5{margin-bottom:calc(var(--spacing)*5)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.mb-8{margin-bottom:calc(var(--spacing)*8)}.mb-12{margin-bottom:calc(var(--spacing)*12)}.ml-4{margin-left:calc(var(--spacing)*4)}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline-flex{display:inline-flex}.aspect-\[3\/2\]{aspect-ratio:3/2}.aspect-\[16\/9\]{aspect-ratio:16/9}.h-2{height:calc(var(--spacing)*2)}.h-4{height:calc(var(--spacing)*4)}.h-5{height:calc(var(--spacing)*5)}.h-8{height:calc(var(--spacing)*8)}.h-16{height:calc(var(--spacing)*16)}.h-24{height:calc(var(--spacing)*24)}.h-32{height:calc(var(--spacing)*32)}.h-full{height:100%}.h-px{height:1px}.min-h-screen{min-height:100vh}.w-2{width:calc(var(--spacing)*2)}.w-4{width:calc(var(--spacing)*4)}.w-5{width:calc(var(--spacing)*5)}.w-8{width:calc(var(--spacing)*8)}.w-12{width:calc(var(--spacing)*12)}.w-16{width:calc(var(--spacing)*16)}.w-24{width:calc(var(--spacing)*24)}.w-32{width:calc(var(--spacing)*32)}.w-\[120px\]{width:120px}.w-fit{width:fit-content}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-6xl{max-width:var(--container-6xl)}.flex-shrink-0{flex-shrink:0}.-translate-y-1\/2{--tw-translate-y:calc(calc(1/2*100%)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.rotate-12{rotate:12deg}.rotate-45{rotate:45deg}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-row{flex-direction:row}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-items-center{justify-items:center}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-8{gap:calc(var(--spacing)*8)}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*8)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*8)*calc(1 - var(--tw-space-y-reverse)))}.overflow-hidden{overflow:hidden}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-emerald-400{border-color:var(--color-emerald-400)}.border-emerald-400\/15{border-color:#00d29426}@supports (color:color-mix(in lab, red, red)){.border-emerald-400\/15{border-color:color-mix(in oklab,var(--color-emerald-400)15%,transparent)}}.border-emerald-500\/20{border-color:#00bb7f33}@supports (color:color-mix(in lab, red, red)){.border-emerald-500\/20{border-color:color-mix(in oklab,var(--color-emerald-500)20%,transparent)}}.border-emerald-500\/30{border-color:#00bb7f4d}@supports (color:color-mix(in lab, red, red)){.border-emerald-500\/30{border-color:color-mix(in oklab,var(--color-emerald-500)30%,transparent)}}.border-gray-700{border-color:var(--color-gray-700)}.border-gray-800{border-color:var(--color-gray-800)}.bg-black{background-color:var(--color-black)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab, red, red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black)50%,transparent)}}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab, red, red)){.bg-black\/60{background-color:color-mix(in oklab,var(--color-black)60%,transparent)}}.bg-emerald-400{background-color:var(--color-emerald-400)}.bg-emerald-500{background-color:var(--color-emerald-500)}.bg-emerald-500\/10{background-color:#00bb7f1a}@supports (color:color-mix(in lab, red, red)){.bg-emerald-500\/10{background-color:color-mix(in oklab,var(--color-emerald-500)10%,transparent)}}.bg-emerald-500\/20{background-color:#00bb7f33}@supports (color:color-mix(in lab, red, red)){.bg-emerald-500\/20{background-color:color-mix(in oklab,var(--color-emerald-500)20%,transparent)}}.bg-emerald-500\/50{background-color:#00bb7f80}@supports (color:color-mix(in lab, red, red)){.bg-emerald-500\/50{background-color:color-mix(in oklab,var(--color-emerald-500)50%,transparent)}}.bg-gray-900\/50{background-color:#10182880}@supports (color:color-mix(in lab, red, red)){.bg-gray-900\/50{background-color:color-mix(in oklab,var(--color-gray-900)50%,transparent)}}.bg-gradient-to-b{--tw-gradient-position:to bottom in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-br{--tw-gradient-position:to bottom right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-r{--tw-gradient-position:to right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-t{--tw-gradient-position:to top in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-\[url\(\'\/img\/\{\{\.Picture\.Id\}\}\/xlarge\.webp\'\)\]{background-image:url(/img/{{.Picture.Id}}/xlarge.webp)}.from-black{--tw-gradient-from:var(--color-black);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-black\/80{--tw-gradient-from:#000c}@supports (color:color-mix(in lab, red, red)){.from-black\/80{--tw-gradient-from:color-mix(in oklab,var(--color-black)80%,transparent)}}.from-black\/80{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-emerald-900{--tw-gradient-from:var(--color-emerald-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-emerald-900\/40{--tw-gradient-from:#004e3b66}@supports (color:color-mix(in lab, red, red)){.from-emerald-900\/40{--tw-gradient-from:color-mix(in oklab,var(--color-emerald-900)40%,transparent)}}.from-emerald-900\/40{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-gray-900{--tw-gradient-from:var(--color-gray-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.via-black\/20{--tw-gradient-via:#0003}@supports (color:color-mix(in lab, red, red)){.via-black\/20{--tw-gradient-via:color-mix(in oklab,var(--color-black)20%,transparent)}}.via-black\/20{--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-emerald-900\/10{--tw-gradient-via:#004e3b1a}@supports (color:color-mix(in lab, red, red)){.via-emerald-900\/10{--tw-gradient-via:color-mix(in oklab,var(--color-emerald-900)10%,transparent)}}.via-emerald-900\/10{--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-900{--tw-gradient-via:var(--color-gray-900);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-transparent{--tw-gradient-via:transparent;--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.to-black{--tw-gradient-to:var(--color-black);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-transparent{--tw-gradient-to:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.bg-cover{background-size:cover}.bg-center{background-position:50%}.object-contain{object-fit:contain}.object-cover{object-fit:cover}.p-2{padding:calc(var(--spacing)*2)}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.p-8{padding:calc(var(--spacing)*8)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-6{padding-inline:calc(var(--spacing)*6)}.px-8{padding-inline:calc(var(--spacing)*8)}.py-1{padding-block:calc(var(--spacing)*1)}.py-4{padding-block:calc(var(--spacing)*4)}.py-8{padding-block:calc(var(--spacing)*8)}.py-16{padding-block:calc(var(--spacing)*16)}.py-24{padding-block:calc(var(--spacing)*24)}.py-42{padding-block:calc(var(--spacing)*42)}.text-center{text-align:center}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.text-emerald-100{color:var(--color-emerald-100)}.text-emerald-200{color:var(--color-emerald-200)}.text-emerald-300{color:var(--color-emerald-300)}.text-emerald-400{color:var(--color-emerald-400)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-white{color:var(--color-white)}.capitalize{text-transform:capitalize}.uppercase{text-transform:uppercase}.opacity-0{opacity:0}.opacity-20{opacity:.2}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-100{opacity:1}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0px_0px_25px_0px_\#276749\]{--tw-shadow:0px 0px 25px 0px var(--tw-shadow-color,#276749);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a),0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.blur-xl{--tw-blur:blur(var(--blur-xl));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-500{--tw-duration:.5s;transition-duration:.5s}.duration-700{--tw-duration:.7s;transition-duration:.7s}@media (hover:hover){.group-hover\:translate-x-0\.5:is(:where(.group):hover *){--tw-translate-x:calc(var(--spacing)*.5);translate:var(--tw-translate-x)var(--tw-translate-y)}.group-hover\:translate-x-1:is(:where(.group):hover *){--tw-translate-x:calc(var(--spacing)*1);translate:var(--tw-translate-x)var(--tw-translate-y)}.group-hover\:scale-110:is(:where(.group):hover *){--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x)var(--tw-scale-y)}.group-hover\:bg-emerald-500\/30:is(:where(.group):hover *){background-color:#00bb7f4d}@supports (color:color-mix(in lab, red, red)){.group-hover\:bg-emerald-500\/30:is(:where(.group):hover *){background-color:color-mix(in oklab,var(--color-emerald-500)30%,transparent)}}.group-hover\:text-emerald-100:is(:where(.group):hover *){color:var(--color-emerald-100)}.group-hover\:text-emerald-300:is(:where(.group):hover *){color:var(--color-emerald-300)}.group-hover\:opacity-40:is(:where(.group):hover *){opacity:.4}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.hover\:border-emerald-400\/50:hover{border-color:#00d29480}@supports (color:color-mix(in lab, red, red)){.hover\:border-emerald-400\/50:hover{border-color:color-mix(in oklab,var(--color-emerald-400)50%,transparent)}}.hover\:border-emerald-500\/30:hover{border-color:#00bb7f4d}@supports (color:color-mix(in lab, red, red)){.hover\:border-emerald-500\/30:hover{border-color:color-mix(in oklab,var(--color-emerald-500)30%,transparent)}}.hover\:border-emerald-500\/50:hover{border-color:#00bb7f80}@supports (color:color-mix(in lab, red, red)){.hover\:border-emerald-500\/50:hover{border-color:color-mix(in oklab,var(--color-emerald-500)50%,transparent)}}.hover\:bg-black\/80:hover{background-color:#000c}@supports (color:color-mix(in lab, red, red)){.hover\:bg-black\/80:hover{background-color:color-mix(in oklab,var(--color-black)80%,transparent)}}.hover\:bg-emerald-500\/20:hover{background-color:#00bb7f33}@supports (color:color-mix(in lab, red, red)){.hover\:bg-emerald-500\/20:hover{background-color:color-mix(in oklab,var(--color-emerald-500)20%,transparent)}}.hover\:bg-gray-800\/50:hover{background-color:#1e293980}@supports (color:color-mix(in lab, red, red)){.hover\:bg-gray-800\/50:hover{background-color:color-mix(in oklab,var(--color-gray-800)50%,transparent)}}.hover\:bg-white\/10:hover{background-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.hover\:bg-white\/10:hover{background-color:color-mix(in oklab,var(--color-white)10%,transparent)}}.hover\:text-emerald-200:hover{color:var(--color-emerald-200)}.hover\:text-emerald-300:hover{color:var(--color-emerald-300)}.hover\:shadow-emerald-500\/10:hover{--tw-shadow-color:#00bb7f1a}@supports (color:color-mix(in lab, red, red)){.hover\:shadow-emerald-500\/10:hover{--tw-shadow-color:color-mix(in oklab,color-mix(in oklab,var(--color-emerald-500)10%,transparent)var(--tw-shadow-alpha),transparent)}}}@media (min-width:48rem){.md\:aspect-\[21\/9\]{aspect-ratio:21/9}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:p-12{padding:calc(var(--spacing)*12)}.md\:text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.md\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.md\:text-7xl{font-size:var(--text-7xl);line-height:var(--tw-leading,var(--text-7xl--line-height))}}@media (min-width:64rem){.lg\:col-span-1{grid-column:span 1/span 1}.lg\:col-span-2{grid-column:span 2/span 2}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:columns-3{columns:3}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:opacity-0{opacity:0}}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}
\ No newline at end of file
diff --git a/themes/EmeraldNoir/assets/logos/favicon.ico b/themes/EmeraldNoir/assets/logos/favicon.ico
new file mode 100644
index 0000000..b5be095
Binary files /dev/null and b/themes/EmeraldNoir/assets/logos/favicon.ico differ
diff --git a/themes/EmeraldNoir/assets/logos/logo192.png b/themes/EmeraldNoir/assets/logos/logo192.png
new file mode 100644
index 0000000..683e86e
Binary files /dev/null and b/themes/EmeraldNoir/assets/logos/logo192.png differ
diff --git a/themes/EmeraldNoir/assets/logos/logo512.png b/themes/EmeraldNoir/assets/logos/logo512.png
new file mode 100644
index 0000000..6206923
Binary files /dev/null and b/themes/EmeraldNoir/assets/logos/logo512.png differ
diff --git a/themes/EmeraldNoir/default.tmpl.html b/themes/EmeraldNoir/default.tmpl.html
new file mode 100644
index 0000000..1d9767f
--- /dev/null
+++ b/themes/EmeraldNoir/default.tmpl.html
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+ {{if .SEO.Description}}
+
+ {{else}}
+
+ {{end}}
+
+
+ {{.Settings.Name}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{template "header" . }}
+
+ {{template "main" .}}
+
+ {{template "social" .}}
+
+
\ No newline at end of file
diff --git a/themes/EmeraldNoir/input.css b/themes/EmeraldNoir/input.css
new file mode 100644
index 0000000..6fdcf92
--- /dev/null
+++ b/themes/EmeraldNoir/input.css
@@ -0,0 +1,2 @@
+@config "./tailwind.config.js";
+@import "tailwindcss";
diff --git a/themes/EmeraldNoir/package.json b/themes/EmeraldNoir/package.json
new file mode 100644
index 0000000..8beb614
--- /dev/null
+++ b/themes/EmeraldNoir/package.json
@@ -0,0 +1,10 @@
+{
+ "dependencies": {
+ "@tailwindcss/cli": "^4.1.11",
+ "tailwindcss": "^4.1.11"
+ },
+ "scripts": {
+ "build": "tailwindcss -i ./input.css -o ./assets/css/style.css --minify",
+ "clean": "rm -rf ./node_modules && rm -rf ./package-lock.json"
+ }
+}
diff --git a/themes/EmeraldNoir/pages/albums.tmpl.html b/themes/EmeraldNoir/pages/albums.tmpl.html
new file mode 100644
index 0000000..3f88f50
--- /dev/null
+++ b/themes/EmeraldNoir/pages/albums.tmpl.html
@@ -0,0 +1,11 @@
+{{ define "main" }}
+
+
+ {{range .Albums}}
+
+ {{template "albums" .}}
+
+ {{end}}
+
+
+{{end}}
\ No newline at end of file
diff --git a/themes/EmeraldNoir/pages/collections.tmpl.html b/themes/EmeraldNoir/pages/collections.tmpl.html
new file mode 100644
index 0000000..ae2c063
--- /dev/null
+++ b/themes/EmeraldNoir/pages/collections.tmpl.html
@@ -0,0 +1,30 @@
+{{ define "main" }}
+
+
+ {{if .Album.Children }}
+
+ {{range .Album.Children}}
+
+ {{template "albums" .}}
+
+ {{end}}
+
+ {{end}}
+ {{template "gallery" . }}
+
+{{end}}
diff --git a/themes/EmeraldNoir/pages/index.tmpl.html b/themes/EmeraldNoir/pages/index.tmpl.html
new file mode 100644
index 0000000..8d870f4
--- /dev/null
+++ b/themes/EmeraldNoir/pages/index.tmpl.html
@@ -0,0 +1,62 @@
+{{ define "main" }}
+
+
+
+
+ {{range .Albums}}
+
+ {{template "albums" .}}
+
+ {{end}}
+
+
+
+
+
+
+
+ {{template "gallery" . }}
+
+
+{{end}}
\ No newline at end of file
diff --git a/themes/EmeraldNoir/pages/photo.tmpl.html b/themes/EmeraldNoir/pages/photo.tmpl.html
new file mode 100644
index 0000000..80faf61
--- /dev/null
+++ b/themes/EmeraldNoir/pages/photo.tmpl.html
@@ -0,0 +1,384 @@
+{{ define "main" }}
+
+
+
+
+
+
+
+
+
+
+
+ {{if .PreImagePath }}
+
+
+
+
+
+
+ {{end}}
+
+ {{if .NextImagePath }}
+
+
+
+
+
+
+ {{end}}
+
+
+
+
+
+ {{.Picture.Name}}
+
+
+ {{.Picture.Caption}}
+
+
+
+
+
+
+
+
+ {{if and .Picture.Camera .Picture.LensModel}}
+
+
+
+
+
+
{{.Picture.Camera}}
+
{{.Picture.LensModel}}
+
+
+
+
+
+ {{end}}
+
+
+
+
+
+
+
+ {{if .Picture.FStop}}
+
+
f/{{.Picture.FStop}}
+
Aperture
+
+ {{end}}
+ {{if .Picture.ShutterSpeed}}
+
+
{{.Picture.ShutterSpeed}}
+
+
Shutter
+
+ {{end}}
+ {{if .Picture.FocalLength}}
+
+
{{.Picture.FocalLength}}
+
+
Focal Length
+
+ {{end}}
+ {{if .Picture.ISO}}
+
+
{{.Picture.ISO}}
+
ISO
+
+ {{end}}
+
+
+
+
+
+
+
+
+
+ {{if .Picture.DateTaken}}
+
+
+
+
+
+
+
+
+
+ {{.Picture.DateTaken.Format "02 Jan 2006"}}
+
+
+
+ {{end}}
+
+
+
+
+
+
+
+
+
Technical Details
+
+ {{if .Picture.ColorSpace}}
+
+ Color Space
+ {{.Picture.ColorSpace}}
+
+ {{end}}
+ {{if .Picture.MeteringMode}}
+
+ Metering Mode
+ {{.Picture.MeteringMode}}
+
+ {{end}}
+ {{if .Picture.Software}}
+
+ Software
+ {{.Picture.Software}}
+
+ {{end}}
+ {{with .Picture.Saturation}}
+
+ Saturation
+ {{.}}
+
+ {{end}}
+ {{with .Picture.Contrast}}
+
+ Contrast
+ {{.}}
+
+ {{end}}
+ {{with .Picture.Sharpness}}
+
+ Sharpness
+ {{.}}
+
+ {{end}}
+ {{if .Picture.Temperature}}
+
+ Temperature
+ {{.Picture.Temperature}}
+
+ {{end}}
+ {{with .Picture.WhiteBalance}}
+
+ White Balance Mode
+ {{.}}
+
+ {{end}}
+
+
+
+
+
+
+
+
+{{end}}
\ No newline at end of file
diff --git a/themes/EmeraldNoir/partials/album.tmpl.html b/themes/EmeraldNoir/partials/album.tmpl.html
new file mode 100644
index 0000000..fb2cfae
--- /dev/null
+++ b/themes/EmeraldNoir/partials/album.tmpl.html
@@ -0,0 +1,31 @@
+{{ define "albums" }}
+
+
+
+
+ {{range $key, $val := ImgSizes}}
+
+ {{end}}
+
+
+
+
+
+
+{{end}}
\ No newline at end of file
diff --git a/themes/EmeraldNoir/partials/gallery.tmpl.html b/themes/EmeraldNoir/partials/gallery.tmpl.html
new file mode 100644
index 0000000..d1b399a
--- /dev/null
+++ b/themes/EmeraldNoir/partials/gallery.tmpl.html
@@ -0,0 +1,31 @@
+{{ define "gallery" }}
+
+{{end}}
\ No newline at end of file
diff --git a/themes/EmeraldNoir/partials/header.tmpl.html b/themes/EmeraldNoir/partials/header.tmpl.html
new file mode 100644
index 0000000..b02690f
--- /dev/null
+++ b/themes/EmeraldNoir/partials/header.tmpl.html
@@ -0,0 +1,51 @@
+{{ define "header" }}
+
+
+{{end}}
\ No newline at end of file
diff --git a/themes/EmeraldNoir/partials/icons.tmpl.html b/themes/EmeraldNoir/partials/icons.tmpl.html
new file mode 100644
index 0000000..2623f4c
--- /dev/null
+++ b/themes/EmeraldNoir/partials/icons.tmpl.html
@@ -0,0 +1,23 @@
+{{define "instagram"}}
+
+{{end}}
+
+{{define "facebook"}}
+
+{{end}}
+
+{{define "twitter"}}
+
+{{end}}
+
+{{define "github"}}
+
+{{end}}
+
+{{define "website"}}
+
+{{end}}
+
+{{define "albumIcon"}}
+
+{{end}}
\ No newline at end of file
diff --git a/themes/EmeraldNoir/partials/social.tmpl.html b/themes/EmeraldNoir/partials/social.tmpl.html
new file mode 100644
index 0000000..52034c0
--- /dev/null
+++ b/themes/EmeraldNoir/partials/social.tmpl.html
@@ -0,0 +1,77 @@
+{{define "social"}}
+
+
+
+
+
+
+
+
+
+
© 2025 {{.Settings.Name}}. All rights reserved.
+
+
+
+{{end}}
\ No newline at end of file
diff --git a/themes/EmeraldNoir/tailwind.config.js b/themes/EmeraldNoir/tailwind.config.js
new file mode 100644
index 0000000..f039854
--- /dev/null
+++ b/themes/EmeraldNoir/tailwind.config.js
@@ -0,0 +1,8 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: ['./**/*.tmpl.html'],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+}
\ No newline at end of file
diff --git a/themes/eastnor/assets/img/placeholder.png b/themes/eastnor/assets/img/placeholder.png
deleted file mode 100644
index 41d321d..0000000
Binary files a/themes/eastnor/assets/img/placeholder.png and /dev/null differ
diff --git a/themes/eastnor/pages/404.hbs b/themes/eastnor/pages/404.hbs
deleted file mode 100644
index f31000c..0000000
--- a/themes/eastnor/pages/404.hbs
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
Oh Dear
-
Seems like something has gone wrong somewhere, maybe is us, either we can not find what you are looking for or something fellover on the server.
-
Anyhow Make a cup of tea and try again
-
-
-
\ No newline at end of file
diff --git a/themes/eastnor/pages/albums.hbs b/themes/eastnor/pages/albums.hbs
deleted file mode 100644
index 3603646..0000000
--- a/themes/eastnor/pages/albums.hbs
+++ /dev/null
@@ -1,11 +0,0 @@
-{{ define "main" }}
-
-
- {{range .Albums}}
-
- {{template "albums" .}}
-
- {{end}}
-
-
-{{end}}
\ No newline at end of file
diff --git a/themes/eastnor/pages/collections.hbs b/themes/eastnor/pages/collections.hbs
deleted file mode 100644
index afb3f75..0000000
--- a/themes/eastnor/pages/collections.hbs
+++ /dev/null
@@ -1,35 +0,0 @@
-{{ define "main" }}
-
- {{if .Album.Name}}
-
- {{end}}
-
-
- {{if .Album.Children }}
- {{range .Album.Children}}
-
- {{template "albums" . }}
-
- {{end}}
- {{end}}
-
- {{template "gallery" . }}
-
-{{end}}
\ No newline at end of file
diff --git a/themes/eastnor/pages/index.hbs b/themes/eastnor/pages/index.hbs
deleted file mode 100644
index 9894bf6..0000000
--- a/themes/eastnor/pages/index.hbs
+++ /dev/null
@@ -1,62 +0,0 @@
-{{ define "main" }}
-
-
- {{template "gallery" . }}
-
-
-
-
-
-
-
-
-{{end}}
\ No newline at end of file
diff --git a/themes/eastnor/pages/pagination.hbs b/themes/eastnor/pages/pagination.hbs
deleted file mode 100644
index 100fe22..0000000
--- a/themes/eastnor/pages/pagination.hbs
+++ /dev/null
@@ -1,7 +0,0 @@
-{{ define "main" }}
-
-
- {{template "gallery" . }}
-
-
-{{end}}
\ No newline at end of file
diff --git a/themes/eastnor/pages/photo.hbs b/themes/eastnor/pages/photo.hbs
deleted file mode 100644
index 4be9dca..0000000
--- a/themes/eastnor/pages/photo.hbs
+++ /dev/null
@@ -1,156 +0,0 @@
-{{ define "main" }}
-
-
-
-
-
-
-
-
-
-
-
-
- {{if .Picture.Exif.GPS.Lat }}
-
- {{end}}
-
-
-{{end}}
\ No newline at end of file
diff --git a/themes/eastnor/partials/album.hbs b/themes/eastnor/partials/album.hbs
deleted file mode 100644
index 8196e62..0000000
--- a/themes/eastnor/partials/album.hbs
+++ /dev/null
@@ -1,25 +0,0 @@
-{{ define "albums" }}
-
-
-
- {{if .ProfileID}}
-
- {{else}}
-
- {{end}}
-
-
-
{{.Name}}
-
-
-
-{{end}}
\ No newline at end of file
diff --git a/themes/eastnor/partials/gallery.hbs b/themes/eastnor/partials/gallery.hbs
deleted file mode 100644
index 798b47f..0000000
--- a/themes/eastnor/partials/gallery.hbs
+++ /dev/null
@@ -1,27 +0,0 @@
-{{ define "gallery" }}
-
- {{range $img := .Images}}
-
-
- {{end}}
-
-{{end}}
\ No newline at end of file
diff --git a/themes/eastnor/partials/header.hbs b/themes/eastnor/partials/header.hbs
deleted file mode 100644
index d9e618f..0000000
--- a/themes/eastnor/partials/header.hbs
+++ /dev/null
@@ -1,32 +0,0 @@
-{{ define "header" }}
-
- {{end}}
\ No newline at end of file
diff --git a/wails.json b/wails.json
deleted file mode 100644
index 787dbb5..0000000
--- a/wails.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "name": "gogallery",
- "outputfilename": "gogallery",
- "frontend:install": "npm install",
- "frontend:build": "npm run build",
- "frontend:dev:watcher": "npm run dev",
- "frontend:dev:serverUrl": "auto",
- "author": {
- "name": "robrotheram",
- "email": "robrotheram@gmail.com"
- },
- "appargs": "dashboard"
-}