Compare commits
89 Commits
67dedaa886
...
templ
| Author | SHA1 | Date | |
|---|---|---|---|
| 946e158d25 | |||
| 7ac9c3d573 | |||
|
|
580d64227d | ||
| acf4119d9a | |||
| f6dd8cee50 | |||
| a981d98ba5 | |||
| a193f97d29 | |||
| 841b29c425 | |||
| 3d61d0f5ef | |||
| 5e388730a5 | |||
| 0a1dfeab65 | |||
| d4c8e4d2da | |||
| bbd3a00102 | |||
| 3a633235ea | |||
| 9809a09d2e | |||
| f37bff365f | |||
| 77527bfb05 | |||
| 8de6fed5df | |||
| f9277d3b32 | |||
| db9629a618 | |||
| 546600db93 | |||
| 7c6acad689 | |||
| 5482899075 | |||
| 5a64ff7029 | |||
| a7ecb1a6f8 | |||
| 2d206826d6 | |||
| f1414e3e4e | |||
| 8e81acd381 | |||
| 6c6a6dd329 | |||
| c4602c8c3b | |||
| fe81b57a34 | |||
| a69b7452ce | |||
| 75ed394f8d | |||
| 803c187a00 | |||
| da1baeb4cd | |||
| 5865fe3c13 | |||
| 4a5464853b | |||
| 622dcd5702 | |||
| a86e2520ef | |||
| b1cfd16627 | |||
| 015ca30ac5 | |||
| 9792a6ff19 | |||
| 8c4c1022c3 | |||
| fd8b6bcdc1 | |||
| 0bbd5986cb | |||
| 45cef2f4af | |||
| e33a64db96 | |||
| 35ca021649 | |||
| 760b9ca0a0 | |||
| c9edcd8f5a | |||
| 2d63a7d109 | |||
| 9bd6bf7727 | |||
| f0a2d2cf69 | |||
| a65750ae21 | |||
| 14b930781e | |||
| 8a8f12c07a | |||
| c5b181dda4 | |||
| d3d89b36f6 | |||
| a69f20d5a9 | |||
| c66a6c8499 | |||
| 3057b86002 | |||
| 2c240f2f5c | |||
| 39fd7ab1f1 | |||
| e9f2e3a5a0 | |||
| a34906c266 | |||
| 756db7a493 | |||
| bb837dd30e | |||
| e823a794cf | |||
| 3c6f3ae237 | |||
| ca1cce1ff1 | |||
| c3410b7833 | |||
| 1403bae036 | |||
| af41946a65 | |||
| 243ae2a001 | |||
| d94e79f39c | |||
| 856bc7e2e6 | |||
| 5cc1e2d71c | |||
| ffc5462326 | |||
| 3cbe4b1c0d | |||
| c213b3b09f | |||
| 7d45bb0253 | |||
| a8bcd0f588 | |||
| bc3e9cbaf0 | |||
| e6ad51ed70 | |||
| cce0ef2de1 | |||
| 71898c39e7 | |||
| 985b6e0851 | |||
| 425f469097 | |||
| 761163d666 |
14
.djlintrc
Normal file
14
.djlintrc
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"profile": "golang",
|
||||
"indent": 2,
|
||||
"close_void_tags": true,
|
||||
"format_attribute_template_tags": true,
|
||||
"format_js": true,
|
||||
"js": {
|
||||
"indent_size": 2
|
||||
},
|
||||
"format_css": true,
|
||||
"css": {
|
||||
"indent_size": 2
|
||||
}
|
||||
}
|
||||
27
.drone.yml
27
.drone.yml
@@ -2,29 +2,28 @@ kind: pipeline
|
||||
type: kubernetes
|
||||
name: default
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- master
|
||||
|
||||
steps:
|
||||
# Unit Tests
|
||||
- name: unit test
|
||||
- name: tests
|
||||
image: golang
|
||||
commands:
|
||||
- make tests_unit
|
||||
- make tests
|
||||
|
||||
# Integration Tests (Every Month)
|
||||
- name: integration test
|
||||
image: golang
|
||||
# Fetch tags
|
||||
- name: fetch tags
|
||||
image: alpine/git
|
||||
commands:
|
||||
- make tests_integration
|
||||
when:
|
||||
event:
|
||||
- cron
|
||||
cron:
|
||||
- integration-test
|
||||
- git fetch --tags
|
||||
|
||||
# Publish Dev Docker Image
|
||||
- name: publish_docker
|
||||
# Publish docker image
|
||||
- name: publish docker
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: gitea.va.reichard.io/evan/bookmanager
|
||||
repo: gitea.va.reichard.io/evan/antholume
|
||||
registry: gitea.va.reichard.io
|
||||
tags:
|
||||
- dev
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,7 @@
|
||||
TODO.md
|
||||
.DS_Store
|
||||
data/
|
||||
build/
|
||||
.direnv/
|
||||
cover.html
|
||||
node_modules
|
||||
|
||||
29
Dockerfile
29
Dockerfile
@@ -1,26 +1,27 @@
|
||||
# Certificate Store
|
||||
FROM alpine AS certs
|
||||
RUN apk update && apk add ca-certificates
|
||||
# Certificates & Timezones
|
||||
FROM alpine AS alpine
|
||||
RUN apk update && apk add --no-cache ca-certificates tzdata
|
||||
|
||||
# Build Image
|
||||
FROM golang:1.20 AS build
|
||||
FROM golang:1.21 AS build
|
||||
|
||||
# Create Package Directory
|
||||
RUN mkdir -p /opt/antholume
|
||||
|
||||
# Copy Source
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
|
||||
# Create Package Directory
|
||||
RUN mkdir -p /opt/bookmanager
|
||||
|
||||
# Compile
|
||||
RUN go build -o /opt/bookmanager/server; \
|
||||
cp -a ./templates /opt/bookmanager/templates; \
|
||||
cp -a ./assets /opt/bookmanager/assets;
|
||||
RUN go build \
|
||||
-ldflags "-X reichard.io/antholume/config.version=`git describe --tags`" \
|
||||
-o /opt/antholume/server
|
||||
|
||||
# Create Image
|
||||
FROM busybox:1.36
|
||||
COPY --from=certs /etc/ssl/certs /etc/ssl/certs
|
||||
COPY --from=build /opt/bookmanager /opt/bookmanager
|
||||
WORKDIR /opt/bookmanager
|
||||
COPY --from=alpine /etc/ssl/certs /etc/ssl/certs
|
||||
COPY --from=alpine /usr/share/zoneinfo /usr/share/zoneinfo
|
||||
COPY --from=build /opt/antholume /opt/antholume
|
||||
WORKDIR /opt/antholume
|
||||
EXPOSE 8585
|
||||
ENTRYPOINT ["/opt/bookmanager/server", "serve"]
|
||||
ENTRYPOINT ["/opt/antholume/server", "serve"]
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# Certificate Store
|
||||
FROM alpine AS certs
|
||||
RUN apk update && apk add ca-certificates
|
||||
# Certificates & Timezones
|
||||
FROM alpine AS alpine
|
||||
RUN apk update && apk add --no-cache ca-certificates tzdata
|
||||
|
||||
# Build Image
|
||||
FROM --platform=$BUILDPLATFORM golang:1.20 AS build
|
||||
FROM --platform=$BUILDPLATFORM golang:1.21 AS build
|
||||
|
||||
# Create Package Directory
|
||||
WORKDIR /src
|
||||
RUN mkdir -p /opt/bookmanager
|
||||
RUN mkdir -p /opt/antholume
|
||||
|
||||
# Cache Dependencies & Compile
|
||||
ARG TARGETOS
|
||||
@@ -15,14 +15,15 @@ ARG TARGETARCH
|
||||
RUN --mount=target=. \
|
||||
--mount=type=cache,target=/root/.cache/go-build \
|
||||
--mount=type=cache,target=/go/pkg \
|
||||
GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /opt/bookmanager/server; \
|
||||
cp -a ./templates /opt/bookmanager/templates; \
|
||||
cp -a ./assets /opt/bookmanager/assets;
|
||||
GOOS=$TARGETOS GOARCH=$TARGETARCH go build \
|
||||
-ldflags "-X reichard.io/antholume/config.version=`git describe --tags`" \
|
||||
-o /opt/antholume/server
|
||||
|
||||
# Create Image
|
||||
FROM busybox:1.36
|
||||
COPY --from=certs /etc/ssl/certs /etc/ssl/certs
|
||||
COPY --from=build /opt/bookmanager /opt/bookmanager
|
||||
WORKDIR /opt/bookmanager
|
||||
COPY --from=alpine /etc/ssl/certs /etc/ssl/certs
|
||||
COPY --from=alpine /usr/share/zoneinfo /usr/share/zoneinfo
|
||||
COPY --from=build /opt/antholume /opt/antholume
|
||||
WORKDIR /opt/antholume
|
||||
EXPOSE 8585
|
||||
ENTRYPOINT ["/opt/bookmanager/server", "serve"]
|
||||
ENTRYPOINT ["/opt/antholume/server", "serve"]
|
||||
|
||||
40
Makefile
40
Makefile
@@ -1,42 +1,48 @@
|
||||
build_local: build_tailwind
|
||||
go mod download
|
||||
rm -r ./build
|
||||
rm -r ./build || true
|
||||
mkdir -p ./build
|
||||
cp -a ./templates ./build/templates
|
||||
cp -a ./assets ./build/assets
|
||||
|
||||
env GOOS=linux GOARCH=amd64 go build -o ./build/server_linux_amd64
|
||||
env GOOS=linux GOARCH=arm64 go build -o ./build/server_linux_arm64
|
||||
env GOOS=darwin GOARCH=arm64 go build -o ./build/server_darwin_arm64
|
||||
env GOOS=darwin GOARCH=amd64 go build -o ./build/server_darwin_amd64
|
||||
env GOOS=linux GOARCH=amd64 go build -ldflags "-X reichard.io/antholume/config.version=`git describe --tags`" -o ./build/server_linux_amd64
|
||||
env GOOS=linux GOARCH=arm64 go build -ldflags "-X reichard.io/antholume/config.version=`git describe --tags`" -o ./build/server_linux_arm64
|
||||
env GOOS=darwin GOARCH=arm64 go build -ldflags "-X reichard.io/antholume/config.version=`git describe --tags`" -o ./build/server_darwin_arm64
|
||||
env GOOS=darwin GOARCH=amd64 go build -ldflags "-X reichard.io/antholume/config.version=`git describe --tags`" -o ./build/server_darwin_amd64
|
||||
|
||||
docker_build_local: build_tailwind
|
||||
docker build -t bookmanager:latest .
|
||||
docker build -t antholume:latest .
|
||||
|
||||
docker_build_release_dev: build_tailwind
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
-t gitea.va.reichard.io/evan/bookmanager:dev \
|
||||
-t gitea.va.reichard.io/evan/antholume:dev \
|
||||
-f Dockerfile-BuildKit \
|
||||
--push .
|
||||
|
||||
docker_build_release_latest: build_tailwind
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
-t gitea.va.reichard.io/evan/bookmanager:latest \
|
||||
-t gitea.va.reichard.io/evan/bookmanager:`git describe --tags` \
|
||||
-t gitea.va.reichard.io/evan/antholume:latest \
|
||||
-t gitea.va.reichard.io/evan/antholume:`git describe --tags` \
|
||||
-f Dockerfile-BuildKit \
|
||||
--push .
|
||||
|
||||
build_tailwind:
|
||||
tailwind build -o ./assets/style.css
|
||||
tailwind build -o ./assets/style.css --minify
|
||||
|
||||
dev: build_tailwind
|
||||
GIN_MODE=release \
|
||||
CONFIG_PATH=./data \
|
||||
DATA_PATH=./data \
|
||||
SEARCH_ENABLED=true \
|
||||
REGISTRATION_ENABLED=true \
|
||||
COOKIE_SECURE=false \
|
||||
COOKIE_AUTH_KEY=1234 \
|
||||
LOG_LEVEL=debug go run main.go serve
|
||||
|
||||
clean:
|
||||
rm -rf ./build
|
||||
|
||||
tests_integration:
|
||||
go test -v -tags=integration -coverpkg=./... ./metadata
|
||||
|
||||
tests_unit:
|
||||
SET_TEST=set_val go test -v -coverpkg=./... ./...
|
||||
tests:
|
||||
SET_TEST=set_val go test -coverpkg=./... ./... -coverprofile=./cover.out
|
||||
go tool cover -html=./cover.out -o ./cover.html
|
||||
rm ./cover.out
|
||||
|
||||
124
README.md
124
README.md
@@ -1,108 +1,102 @@
|
||||
# Book Manager
|
||||
<p><img align="center" src="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/banner.png"></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/login.png">
|
||||
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/login.png" width="19%">
|
||||
<a href="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/pwa/login.png">
|
||||
<img src="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/pwa/login.png" width="19%">
|
||||
</a>
|
||||
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/home.png">
|
||||
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/home.png" width="19%">
|
||||
<a href="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/pwa/home.png">
|
||||
<img src="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/pwa/home.png" width="19%">
|
||||
</a>
|
||||
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/documents.png">
|
||||
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/documents.png" width="19%">
|
||||
<a href="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/pwa/documents.png">
|
||||
<img src="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/pwa/documents.png" width="19%">
|
||||
</a>
|
||||
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/document.png">
|
||||
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/document.png" width="19%">
|
||||
<a href="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/pwa/document.png">
|
||||
<img src="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/pwa/document.png" width="19%">
|
||||
</a>
|
||||
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/metadata.png">
|
||||
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/metadata.png" width="19%">
|
||||
<a href="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/pwa/metadata.png">
|
||||
<img src="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/pwa/metadata.png" width="19%">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">Screenshots</p>
|
||||
<p align="center">
|
||||
<a href="https://gitea.va.reichard.io/evan/BookManager/src/branch/master/screenshots/web/README.md">Web App</a> - <a href="https://gitea.va.reichard.io/evan/BookManager/src/branch/master/screenshots/pwa/README.md">PWA</a>
|
||||
<strong><a href="https://gitea.va.reichard.io/evan/AnthoLume/src/branch/master/screenshots">Screenshots</a></strong> •
|
||||
<strong><a href="https://antholume-demo.cloud.reichard.io/">Demo Server</a></strong>
|
||||
</p>
|
||||
<p align="center"><strong>user:</strong> demo • <strong>pass:</strong> demo</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://drone.va.reichard.io/evan/BookManager" target="_blank">
|
||||
<img src="https://drone.va.reichard.io/api/badges/evan/BookManager/status.svg">
|
||||
<a href="https://drone.va.reichard.io/evan/AnthoLume" target="_blank">
|
||||
<img src="https://drone.va.reichard.io/api/badges/evan/AnthoLume/status.svg">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
**TL;DR:** Show me the [demo](https://books-demo.cloud.reichard.io/)!
|
||||
AnthoLume is a Progressive Web App (PWA) that manages your EPUB documents, provides an EPUB reader, and tracks your reading activity! It also has a [KOReader KOSync](https://github.com/koreader/koreader-sync-server) compatible API, and a [KOReader](https://github.com/koreader/koreader) Plugin used to sync activity from your Kindle. Some additional features include:
|
||||
|
||||
- Username: `demo`
|
||||
- Password: `demo`
|
||||
- OPDS API Endpoint
|
||||
- Local / Offline Reader (via ServiceWorker)
|
||||
- Metadata Scraping (Thanks [OpenLibrary](https://openlibrary.org/) & [Google Books API](https://developers.google.com/books/docs/v1/getting_started))
|
||||
- Words / Minute (WPM) Tracking & Leaderboard (Amongst Server Users)
|
||||
|
||||
This is BookManager! Will probably be renamed at some point. This repository contains:
|
||||
While some features require JavaScript (Service Worker & EPUB Reader), we make an effort to limit JavaScript usage. Outside of the two aforementioned features, no JavaScript is used.
|
||||
|
||||
- Web App / Progressive Web App (PWA)
|
||||
- [KOReader](https://github.com/koreader/koreader) Plugin (See `client` subfolder)
|
||||
- [KOReader KOSync](https://github.com/koreader/koreader-sync-server) compatible API
|
||||
- OPDS API endpoint that provides access to the uploaded documents
|
||||
## Server
|
||||
|
||||
In additional to the compatible KOSync API's, we add:
|
||||
Docker Image: `docker pull gitea.va.reichard.io/evan/antholume:latest`
|
||||
|
||||
- Additional APIs to automatically upload reading statistics
|
||||
- Upload documents to the server (can download in the "Documents" view or via OPDS)
|
||||
- Book metadata scraping (Thanks [OpenLibrary](https://openlibrary.org/) & [Google Books API](https://developers.google.com/books/docs/v1/getting_started))
|
||||
- Limited JavaScript use. Server-Side Rendering is used wherever possible. The main app is fully operational without any JS. JS is only required for:
|
||||
- EPUB Reader
|
||||
- Local / Offline Mode
|
||||
- Service Worker
|
||||
### Local / Offline Reader
|
||||
|
||||
# Server
|
||||
|
||||
Docker Image: `docker pull gitea.va.reichard.io/evan/bookmanager:latest`
|
||||
|
||||
## Local / Offline Reader
|
||||
|
||||
The Local / Offline reader allows you to use any BookManager server as a standalone offline accessible reading app! Some features:
|
||||
The Local / Offline reader allows you to use any AnthoLume server as a standalone offline accessible reading app! Some features:
|
||||
|
||||
- Add local EPUB documents
|
||||
- Read both local and any cached server documents
|
||||
- Maintains progress for all types of documents (server / local)
|
||||
- Uploads any progress or activity for cached server documents once the internet is accessible
|
||||
|
||||
## KOSync API
|
||||
### KOSync API
|
||||
|
||||
The KOSync compatible API endpoint is located at: `http(s)://<SERVER>/api/ko`
|
||||
|
||||
## OPDS API
|
||||
### OPDS API
|
||||
|
||||
The OPDS API endpoint is located at: `http(s)://<SERVER>/api/opds`
|
||||
|
||||
## Quick Start
|
||||
### Quick Start
|
||||
|
||||
**NOTE**: If you're accessing your instance over HTTP (not HTTPS), you must set `COOKIE_SECURE=false`, otherwise you will not be able to login.
|
||||
|
||||
```bash
|
||||
# Make Data Directory
|
||||
mkdir -p bookmanager_data
|
||||
mkdir -p antholume_data
|
||||
|
||||
# Run Server
|
||||
docker run \
|
||||
-p 8585:8585 \
|
||||
-e COOKIE_SECURE=false \
|
||||
-e REGISTRATION_ENABLED=true \
|
||||
-v ./bookmanager_data:/config \
|
||||
-v ./bookmanager_data:/data \
|
||||
gitea.va.reichard.io/evan/bookmanager:latest
|
||||
-v ./antholume_data:/config \
|
||||
-v ./antholume_data:/data \
|
||||
gitea.va.reichard.io/evan/antholume:latest
|
||||
```
|
||||
|
||||
The service is now accessible at: `http://localhost:8585`. I recommend registering an account and then disabling registration unless you expect more users.
|
||||
|
||||
## Configuration
|
||||
### Configuration
|
||||
|
||||
| Environment Variable | Default Value | Description |
|
||||
| -------------------- | ------------- | ------------------------------------------------------------------- |
|
||||
| DATABASE_TYPE | SQLite | Currently only "SQLite" is supported |
|
||||
| DATABASE_NAME | book_manager | The database name, or in SQLite's case, the filename |
|
||||
| CONFIG_PATH | /config | Directory where to store SQLite's DB |
|
||||
| DATA_PATH | /data | Directory where to store the documents and cover metadata |
|
||||
| LISTEN_PORT | 8585 | Port the server listens at |
|
||||
| REGISTRATION_ENABLED | false | Whether to allow registration (applies to both WebApp & KOSync API) |
|
||||
| COOKIE_SESSION_KEY | <EMPTY> | Optional secret cookie session key (auto generated if not provided) |
|
||||
| COOKIE_SECURE | true | Set Cookie `Secure` attribute (i.e. only works over HTTPS) |
|
||||
| COOKIE_HTTP_ONLY | true | Set Cookie `HttpOnly` attribute (i.e. inacessible via JavaScript) |
|
||||
| Environment Variable | Default Value | Description |
|
||||
| -------------------- | ------------- | -------------------------------------------------------------------------- |
|
||||
| DATABASE_TYPE | SQLite | Currently only "SQLite" is supported |
|
||||
| DATABASE_NAME | antholume | The database name, or in SQLite's case, the filename |
|
||||
| CONFIG_PATH | /config | Directory where to store SQLite's DB |
|
||||
| DATA_PATH | /data | Directory where to store the documents and cover metadata |
|
||||
| LISTEN_PORT | 8585 | Port the server listens at |
|
||||
| LOG_LEVEL | info | Set server log level |
|
||||
| REGISTRATION_ENABLED | false | Whether to allow registration (applies to both WebApp & KOSync API) |
|
||||
| COOKIE_AUTH_KEY | <EMPTY> | Optional secret cookie authentication key (auto generated if not provided) |
|
||||
| COOKIE_ENC_KEY | <EMPTY> | Optional secret cookie encryption key (16 or 32 bytes) |
|
||||
| COOKIE_SECURE | true | Set Cookie `Secure` attribute (i.e. only works over HTTPS) |
|
||||
| COOKIE_HTTP_ONLY | true | Set Cookie `HttpOnly` attribute (i.e. inacessible via JavaScript) |
|
||||
|
||||
## Security
|
||||
|
||||
@@ -118,26 +112,32 @@ The service is now accessible at: `http://localhost:8585`. I recommend registeri
|
||||
- The native KOSync plugin sends an MD5 hash of the password. Due to that:
|
||||
- We store an Argon2 hash _and_ per-password salt of the MD5 hashed original password
|
||||
|
||||
# Client (KOReader Plugin)
|
||||
## Client (KOReader Plugin)
|
||||
|
||||
See documentation in the `client` subfolder: [SyncNinja](https://gitea.va.reichard.io/evan/BookManager/src/branch/master/client/)
|
||||
See documentation in the `client` subfolder: [SyncNinja](https://gitea.va.reichard.io/evan/AnthoLume/src/branch/master/client/)
|
||||
|
||||
# Development
|
||||
## Development
|
||||
|
||||
SQLC Generation (v1.21.0):
|
||||
SQLC Generation (v1.26.0):
|
||||
|
||||
```bash
|
||||
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
|
||||
~/go/bin/sqlc generate
|
||||
```
|
||||
|
||||
Goose Migrations:
|
||||
|
||||
```bash
|
||||
go install github.com/pressly/goose/v3/cmd/goose@latest
|
||||
```
|
||||
|
||||
Run Development:
|
||||
|
||||
```bash
|
||||
CONFIG_PATH=./data DATA_PATH=./data REGISTRATION_ENABLED=true go run main.go serve
|
||||
```
|
||||
|
||||
# Building
|
||||
## Building
|
||||
|
||||
The `Dockerfile` and `Makefile` contain the build information:
|
||||
|
||||
|
||||
409
api/api.go
409
api/api.go
@@ -1,179 +1,374 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/multitemplate"
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/cookie"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"reichard.io/bbank/config"
|
||||
"reichard.io/bbank/database"
|
||||
"reichard.io/antholume/api/renderer"
|
||||
"reichard.io/antholume/config"
|
||||
"reichard.io/antholume/database"
|
||||
"reichard.io/antholume/utils"
|
||||
)
|
||||
|
||||
type API struct {
|
||||
Router *gin.Engine
|
||||
Config *config.Config
|
||||
DB *database.DBManager
|
||||
HTMLPolicy *bluemonday.Policy
|
||||
db *database.DBManager
|
||||
cfg *config.Config
|
||||
assets fs.FS
|
||||
httpServer *http.Server
|
||||
templates map[string]*template.Template
|
||||
userAuthCache map[string]string
|
||||
}
|
||||
|
||||
func NewApi(db *database.DBManager, c *config.Config) *API {
|
||||
var htmlPolicy = bluemonday.StrictPolicy()
|
||||
|
||||
func NewApi(db *database.DBManager, c *config.Config, assets fs.FS) *API {
|
||||
api := &API{
|
||||
HTMLPolicy: bluemonday.StrictPolicy(),
|
||||
Router: gin.Default(),
|
||||
Config: c,
|
||||
DB: db,
|
||||
db: db,
|
||||
cfg: c,
|
||||
assets: assets,
|
||||
templates: make(map[string]*template.Template),
|
||||
userAuthCache: make(map[string]string),
|
||||
}
|
||||
|
||||
// Assets & Web App Templates
|
||||
api.Router.Static("/assets", "./assets")
|
||||
// Create router
|
||||
router := gin.New()
|
||||
|
||||
// Generate Secure Token
|
||||
// Override renderer
|
||||
ginRenderer := router.HTMLRender
|
||||
router.HTMLRender = &renderer.HTMLTemplRenderer{FallbackHtmlRenderer: ginRenderer}
|
||||
|
||||
// Add server
|
||||
api.httpServer = &http.Server{
|
||||
Handler: router,
|
||||
Addr: (":" + c.ListenPort),
|
||||
}
|
||||
|
||||
// Add global logging middleware
|
||||
router.Use(loggingMiddleware)
|
||||
|
||||
// Add global template loader middleware (develop)
|
||||
if c.Version == "develop" {
|
||||
log.Info("utilizing debug template loader")
|
||||
router.Use(api.templateMiddleware(router))
|
||||
}
|
||||
|
||||
// Assets & web app templates
|
||||
assetsDir, _ := fs.Sub(assets, "assets")
|
||||
router.StaticFS("/assets", http.FS(assetsDir))
|
||||
|
||||
// Generate auth token
|
||||
var newToken []byte
|
||||
var err error
|
||||
|
||||
if c.CookieSessionKey != "" {
|
||||
log.Info("[NewApi] Utilizing Environment Cookie Session Key")
|
||||
newToken = []byte(c.CookieSessionKey)
|
||||
if c.CookieAuthKey != "" {
|
||||
log.Info("utilizing environment cookie auth key")
|
||||
newToken = []byte(c.CookieAuthKey)
|
||||
} else {
|
||||
log.Info("[NewApi] Generating Cookie Session Key")
|
||||
newToken, err = generateToken(64)
|
||||
log.Info("generating cookie auth key")
|
||||
newToken, err = utils.GenerateToken(64)
|
||||
if err != nil {
|
||||
panic("Unable to generate secure token")
|
||||
log.Panic("unable to generate cookie auth key")
|
||||
}
|
||||
}
|
||||
|
||||
// Configure Cookie Session Store
|
||||
// Set enc token
|
||||
store := cookie.NewStore(newToken)
|
||||
if c.CookieEncKey != "" {
|
||||
if len(c.CookieEncKey) == 16 || len(c.CookieEncKey) == 32 {
|
||||
log.Info("utilizing environment cookie encryption key")
|
||||
store = cookie.NewStore(newToken, []byte(c.CookieEncKey))
|
||||
} else {
|
||||
log.Panic("invalid cookie encryption key (must be 16 or 32 bytes)")
|
||||
}
|
||||
}
|
||||
|
||||
// Configure cookie session store
|
||||
store.Options(sessions.Options{
|
||||
MaxAge: 60 * 60 * 24 * 7,
|
||||
Secure: c.CookieSecure,
|
||||
HttpOnly: c.CookieHTTPOnly,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
})
|
||||
api.Router.Use(sessions.Sessions("token", store))
|
||||
router.Use(sessions.Sessions("token", store))
|
||||
|
||||
// Register Web App Route
|
||||
api.registerWebAppRoutes()
|
||||
// Register web app route
|
||||
api.registerWebAppRoutes(router)
|
||||
|
||||
// Register API Routes
|
||||
apiGroup := api.Router.Group("/api")
|
||||
// Register API routes
|
||||
apiGroup := router.Group("/api")
|
||||
api.registerKOAPIRoutes(apiGroup)
|
||||
api.registerOPDSRoutes(apiGroup)
|
||||
|
||||
return api
|
||||
}
|
||||
|
||||
func (api *API) registerWebAppRoutes() {
|
||||
// Define Templates & Helper Functions
|
||||
render := multitemplate.NewRenderer()
|
||||
helperFuncs := template.FuncMap{
|
||||
"GetSVGGraphData": getSVGGraphData,
|
||||
"GetUTCOffsets": getUTCOffsets,
|
||||
"NiceSeconds": niceSeconds,
|
||||
func (api *API) Start() error {
|
||||
return api.httpServer.ListenAndServe()
|
||||
}
|
||||
|
||||
func (api *API) Stop() error {
|
||||
// Stop server
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
err := api.httpServer.Shutdown(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Templates
|
||||
render.AddFromFiles("error", "templates/error.html")
|
||||
render.AddFromFilesFuncs("activity", helperFuncs, "templates/base.html", "templates/activity.html")
|
||||
render.AddFromFilesFuncs("document", helperFuncs, "templates/base.html", "templates/document.html")
|
||||
render.AddFromFilesFuncs("documents", helperFuncs, "templates/base.html", "templates/documents.html")
|
||||
render.AddFromFilesFuncs("home", helperFuncs, "templates/base.html", "templates/home.html")
|
||||
render.AddFromFilesFuncs("login", helperFuncs, "templates/login.html")
|
||||
render.AddFromFilesFuncs("search", helperFuncs, "templates/base.html", "templates/search.html")
|
||||
render.AddFromFilesFuncs("settings", helperFuncs, "templates/base.html", "templates/settings.html")
|
||||
// Close DB
|
||||
return api.db.DB.Close()
|
||||
}
|
||||
|
||||
api.Router.HTMLRender = render
|
||||
func (api *API) registerWebAppRoutes(router *gin.Engine) {
|
||||
// Generate templates
|
||||
router.HTMLRender = *api.generateTemplates()
|
||||
|
||||
// Static Assets (Required @ Root)
|
||||
api.Router.GET("/manifest.json", api.webManifest)
|
||||
api.Router.GET("/sw.js", api.serviceWorker)
|
||||
// Static assets (required @ root)
|
||||
router.GET("/manifest.json", api.appWebManifest)
|
||||
router.GET("/favicon.ico", api.appFaviconIcon)
|
||||
router.GET("/sw.js", api.appServiceWorker)
|
||||
|
||||
// Local / Offline Static Pages (No Template, No Auth)
|
||||
api.Router.GET("/local", api.localDocuments)
|
||||
api.Router.GET("/reader", api.documentReader)
|
||||
// Local / offline static pages (no template, no auth)
|
||||
router.GET("/local", api.appLocalDocuments)
|
||||
|
||||
// Web App
|
||||
api.Router.GET("/", api.authWebAppMiddleware, api.createAppResourcesRoute("home"))
|
||||
api.Router.GET("/activity", api.authWebAppMiddleware, api.createAppResourcesRoute("activity"))
|
||||
api.Router.GET("/documents", api.authWebAppMiddleware, api.createAppResourcesRoute("documents"))
|
||||
api.Router.GET("/documents/:document", api.authWebAppMiddleware, api.createAppResourcesRoute("document"))
|
||||
api.Router.GET("/documents/:document/cover", api.authWebAppMiddleware, api.getDocumentCover)
|
||||
api.Router.GET("/documents/:document/file", api.authWebAppMiddleware, api.downloadDocument)
|
||||
api.Router.GET("/documents/:document/progress", api.authWebAppMiddleware, api.getDocumentProgress)
|
||||
api.Router.GET("/login", api.createAppResourcesRoute("login"))
|
||||
api.Router.GET("/logout", api.authWebAppMiddleware, api.authLogout)
|
||||
api.Router.GET("/register", api.createAppResourcesRoute("login", gin.H{"Register": true}))
|
||||
api.Router.GET("/settings", api.authWebAppMiddleware, api.createAppResourcesRoute("settings"))
|
||||
api.Router.POST("/login", api.authFormLogin)
|
||||
api.Router.POST("/register", api.authFormRegister)
|
||||
// Reader (reader page, document progress, devices)
|
||||
router.GET("/reader", api.appDocumentReader)
|
||||
router.GET("/reader/devices", api.authWebAppMiddleware, api.appGetDevices)
|
||||
router.GET("/reader/progress/:document", api.authWebAppMiddleware, api.appGetDocumentProgress)
|
||||
|
||||
// Demo Mode Enabled Configuration
|
||||
if api.Config.DemoMode {
|
||||
api.Router.POST("/documents", api.authWebAppMiddleware, api.demoModeAppError)
|
||||
api.Router.POST("/documents/:document/delete", api.authWebAppMiddleware, api.demoModeAppError)
|
||||
api.Router.POST("/documents/:document/edit", api.authWebAppMiddleware, api.demoModeAppError)
|
||||
api.Router.POST("/documents/:document/identify", api.authWebAppMiddleware, api.demoModeAppError)
|
||||
api.Router.POST("/settings", api.authWebAppMiddleware, api.demoModeAppError)
|
||||
// Web app
|
||||
router.GET("/", api.authWebAppMiddleware, api.appGetHome)
|
||||
router.GET("/activity", api.authWebAppMiddleware, api.appGetActivity)
|
||||
router.GET("/progress", api.authWebAppMiddleware, api.appGetProgress)
|
||||
router.GET("/documents", api.authWebAppMiddleware, api.appGetDocuments)
|
||||
router.GET("/documents/:document", api.authWebAppMiddleware, api.appGetDocument)
|
||||
router.GET("/documents/:document/cover", api.authWebAppMiddleware, api.createGetCoverHandler(appErrorPage))
|
||||
router.GET("/documents/:document/file", api.authWebAppMiddleware, api.createDownloadDocumentHandler(appErrorPage))
|
||||
router.GET("/login", api.appGetLogin)
|
||||
router.GET("/logout", api.authWebAppMiddleware, api.appAuthLogout)
|
||||
router.GET("/register", api.appGetRegister)
|
||||
router.GET("/settings", api.authWebAppMiddleware, api.appGetSettings)
|
||||
router.GET("/admin/logs", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdminLogs)
|
||||
router.GET("/admin/import", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdminImport)
|
||||
router.POST("/admin/import", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appPerformAdminImport)
|
||||
router.GET("/admin/users", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdminUsers)
|
||||
router.POST("/admin/users", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appUpdateAdminUsers)
|
||||
router.GET("/admin", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdmin)
|
||||
router.POST("/admin", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appPerformAdminAction)
|
||||
router.POST("/login", api.appAuthLogin)
|
||||
router.POST("/register", api.appAuthRegister)
|
||||
|
||||
// Demo mode enabled configuration
|
||||
if api.cfg.DemoMode {
|
||||
router.POST("/documents", api.authWebAppMiddleware, api.appDemoModeError)
|
||||
router.POST("/documents/:document/delete", api.authWebAppMiddleware, api.appDemoModeError)
|
||||
router.POST("/documents/:document/edit", api.authWebAppMiddleware, api.appDemoModeError)
|
||||
router.POST("/documents/:document/identify", api.authWebAppMiddleware, api.appDemoModeError)
|
||||
router.POST("/settings", api.authWebAppMiddleware, api.appDemoModeError)
|
||||
} else {
|
||||
api.Router.POST("/documents", api.authWebAppMiddleware, api.uploadNewDocument)
|
||||
api.Router.POST("/documents/:document/delete", api.authWebAppMiddleware, api.deleteDocument)
|
||||
api.Router.POST("/documents/:document/edit", api.authWebAppMiddleware, api.editDocument)
|
||||
api.Router.POST("/documents/:document/identify", api.authWebAppMiddleware, api.identifyDocument)
|
||||
api.Router.POST("/settings", api.authWebAppMiddleware, api.editSettings)
|
||||
router.POST("/documents", api.authWebAppMiddleware, api.appUploadNewDocument)
|
||||
router.POST("/documents/:document/delete", api.authWebAppMiddleware, api.appDeleteDocument)
|
||||
router.POST("/documents/:document/edit", api.authWebAppMiddleware, api.appEditDocument)
|
||||
router.POST("/documents/:document/identify", api.authWebAppMiddleware, api.appIdentifyDocument)
|
||||
router.POST("/settings", api.authWebAppMiddleware, api.appEditSettings)
|
||||
}
|
||||
|
||||
// Search Enabled Configuration
|
||||
if api.Config.SearchEnabled {
|
||||
api.Router.GET("/search", api.authWebAppMiddleware, api.createAppResourcesRoute("search"))
|
||||
api.Router.POST("/search", api.authWebAppMiddleware, api.saveNewDocument)
|
||||
// Search enabled configuration
|
||||
if api.cfg.SearchEnabled {
|
||||
router.GET("/search", api.authWebAppMiddleware, api.appGetSearch)
|
||||
router.POST("/search", api.authWebAppMiddleware, api.appSaveNewDocument)
|
||||
}
|
||||
}
|
||||
|
||||
func (api *API) registerKOAPIRoutes(apiGroup *gin.RouterGroup) {
|
||||
koGroup := apiGroup.Group("/ko")
|
||||
|
||||
// KO Sync Routes (WebApp Uses - Progress & Activity)
|
||||
koGroup.GET("/documents/:document/file", api.authKOMiddleware, api.downloadDocument)
|
||||
koGroup.GET("/syncs/progress/:document", api.authKOMiddleware, api.getProgress)
|
||||
koGroup.GET("/users/auth", api.authKOMiddleware, api.authorizeUser)
|
||||
koGroup.POST("/activity", api.authKOMiddleware, api.addActivities)
|
||||
koGroup.POST("/syncs/activity", api.authKOMiddleware, api.checkActivitySync)
|
||||
koGroup.POST("/users/create", api.createUser)
|
||||
koGroup.PUT("/syncs/progress", api.authKOMiddleware, api.setProgress)
|
||||
// KO sync routes (webapp uses - progress & activity)
|
||||
koGroup.GET("/documents/:document/file", api.authKOMiddleware, api.createDownloadDocumentHandler(apiErrorPage))
|
||||
koGroup.GET("/syncs/progress/:document", api.authKOMiddleware, api.koGetProgress)
|
||||
koGroup.GET("/users/auth", api.authKOMiddleware, api.koAuthorizeUser)
|
||||
koGroup.POST("/activity", api.authKOMiddleware, api.koAddActivities)
|
||||
koGroup.POST("/syncs/activity", api.authKOMiddleware, api.koCheckActivitySync)
|
||||
koGroup.POST("/users/create", api.koAuthRegister)
|
||||
koGroup.PUT("/syncs/progress", api.authKOMiddleware, api.koSetProgress)
|
||||
|
||||
// Demo Mode Enabled Configuration
|
||||
if api.Config.DemoMode {
|
||||
koGroup.POST("/documents", api.authKOMiddleware, api.demoModeJSONError)
|
||||
koGroup.POST("/syncs/documents", api.authKOMiddleware, api.demoModeJSONError)
|
||||
koGroup.PUT("/documents/:document/file", api.authKOMiddleware, api.demoModeJSONError)
|
||||
// Demo mode enabled configuration
|
||||
if api.cfg.DemoMode {
|
||||
koGroup.POST("/documents", api.authKOMiddleware, api.koDemoModeJSONError)
|
||||
koGroup.POST("/syncs/documents", api.authKOMiddleware, api.koDemoModeJSONError)
|
||||
koGroup.PUT("/documents/:document/file", api.authKOMiddleware, api.koDemoModeJSONError)
|
||||
} else {
|
||||
koGroup.POST("/documents", api.authKOMiddleware, api.addDocuments)
|
||||
koGroup.POST("/syncs/documents", api.authKOMiddleware, api.checkDocumentsSync)
|
||||
koGroup.PUT("/documents/:document/file", api.authKOMiddleware, api.uploadExistingDocument)
|
||||
koGroup.POST("/documents", api.authKOMiddleware, api.koAddDocuments)
|
||||
koGroup.POST("/syncs/documents", api.authKOMiddleware, api.koCheckDocumentsSync)
|
||||
koGroup.PUT("/documents/:document/file", api.authKOMiddleware, api.koUploadExistingDocument)
|
||||
}
|
||||
}
|
||||
|
||||
func (api *API) registerOPDSRoutes(apiGroup *gin.RouterGroup) {
|
||||
opdsGroup := apiGroup.Group("/opds")
|
||||
|
||||
// OPDS Routes
|
||||
opdsGroup.GET("/", api.authOPDSMiddleware, api.opdsDocuments)
|
||||
opdsGroup.GET("/documents/:document/cover", api.authOPDSMiddleware, api.getDocumentCover)
|
||||
opdsGroup.GET("/documents/:document/file", api.authOPDSMiddleware, api.downloadDocument)
|
||||
// OPDS routes
|
||||
opdsGroup.GET("", api.authOPDSMiddleware, api.opdsEntry)
|
||||
opdsGroup.GET("/", api.authOPDSMiddleware, api.opdsEntry)
|
||||
opdsGroup.GET("/search.xml", api.authOPDSMiddleware, api.opdsSearchDescription)
|
||||
opdsGroup.GET("/documents", api.authOPDSMiddleware, api.opdsDocuments)
|
||||
opdsGroup.GET("/documents/:document/cover", api.authOPDSMiddleware, api.createGetCoverHandler(apiErrorPage))
|
||||
opdsGroup.GET("/documents/:document/file", api.authOPDSMiddleware, api.createDownloadDocumentHandler(apiErrorPage))
|
||||
}
|
||||
|
||||
func generateToken(n int) ([]byte, error) {
|
||||
b := make([]byte, n)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func (api *API) generateTemplates() *multitemplate.Renderer {
|
||||
// Define templates & helper functions
|
||||
render := multitemplate.NewRenderer()
|
||||
templates := make(map[string]*template.Template)
|
||||
helperFuncs := template.FuncMap{
|
||||
"dict": dict,
|
||||
"slice": slice,
|
||||
"fields": fields,
|
||||
"getSVGGraphData": getSVGGraphData,
|
||||
"getTimeZones": getTimeZones,
|
||||
"hasPrefix": strings.HasPrefix,
|
||||
"niceNumbers": niceNumbers,
|
||||
"niceSeconds": niceSeconds,
|
||||
}
|
||||
|
||||
// Load Base
|
||||
b, err := fs.ReadFile(api.assets, "templates/base.tmpl")
|
||||
if err != nil {
|
||||
log.Errorf("error reading base template: %v", err)
|
||||
return &render
|
||||
}
|
||||
|
||||
// Parse Base
|
||||
baseTemplate, err := template.New("base").Funcs(helperFuncs).Parse(string(b))
|
||||
if err != nil {
|
||||
log.Errorf("error parsing base template: %v", err)
|
||||
return &render
|
||||
}
|
||||
|
||||
// Load SVGs
|
||||
err = api.loadTemplates("svg", baseTemplate, templates, false)
|
||||
if err != nil {
|
||||
log.Errorf("error loading svg templates: %v", err)
|
||||
return &render
|
||||
}
|
||||
|
||||
// Load Components
|
||||
err = api.loadTemplates("component", baseTemplate, templates, false)
|
||||
if err != nil {
|
||||
log.Errorf("error loading component templates: %v", err)
|
||||
return &render
|
||||
}
|
||||
|
||||
// Load Pages
|
||||
err = api.loadTemplates("page", baseTemplate, templates, true)
|
||||
if err != nil {
|
||||
log.Errorf("error loading page templates: %v", err)
|
||||
return &render
|
||||
}
|
||||
|
||||
// Populate Renderer
|
||||
api.templates = templates
|
||||
for templateName, templateValue := range templates {
|
||||
render.Add(templateName, templateValue)
|
||||
}
|
||||
|
||||
return &render
|
||||
}
|
||||
|
||||
func (api *API) loadTemplates(
|
||||
basePath string,
|
||||
baseTemplate *template.Template,
|
||||
allTemplates map[string]*template.Template,
|
||||
cloneBase bool,
|
||||
) error {
|
||||
// Load Templates (Pluralize)
|
||||
templateDirectory := fmt.Sprintf("templates/%ss", basePath)
|
||||
allFiles, err := fs.ReadDir(api.assets, templateDirectory)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("unable to read template dir: %s", templateDirectory))
|
||||
}
|
||||
|
||||
// Generate Templates
|
||||
for _, item := range allFiles {
|
||||
templateFile := item.Name()
|
||||
templatePath := path.Join(templateDirectory, templateFile)
|
||||
templateName := fmt.Sprintf("%s/%s", basePath, strings.TrimSuffix(templateFile, filepath.Ext(templateFile)))
|
||||
|
||||
// Read Template
|
||||
b, err := fs.ReadFile(api.assets, templatePath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("unable to read template: %s", templateName))
|
||||
}
|
||||
|
||||
// Clone? (Pages - Don't Stomp)
|
||||
if cloneBase {
|
||||
baseTemplate = template.Must(baseTemplate.Clone())
|
||||
}
|
||||
|
||||
// Parse Template
|
||||
baseTemplate, err = baseTemplate.New(templateName).Parse(string(b))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("unable to parse template: %s", templateName))
|
||||
}
|
||||
|
||||
allTemplates[templateName] = baseTemplate
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loggingMiddleware(c *gin.Context) {
|
||||
// Start timer
|
||||
startTime := time.Now()
|
||||
|
||||
// Process request
|
||||
c.Next()
|
||||
|
||||
// End timer
|
||||
endTime := time.Now()
|
||||
latency := endTime.Sub(startTime).Round(time.Microsecond)
|
||||
|
||||
// Log data
|
||||
logData := log.Fields{
|
||||
"type": "access",
|
||||
"ip": c.ClientIP(),
|
||||
"latency": latency.String(),
|
||||
"status": c.Writer.Status(),
|
||||
"method": c.Request.Method,
|
||||
"path": c.Request.URL.Path,
|
||||
}
|
||||
|
||||
// Get username
|
||||
var auth authData
|
||||
if data, _ := c.Get("Authorization"); data != nil {
|
||||
auth = data.(authData)
|
||||
}
|
||||
|
||||
// Log user
|
||||
if auth.UserName != "" {
|
||||
logData["user"] = auth.UserName
|
||||
}
|
||||
|
||||
// Log result
|
||||
log.WithFields(logData).Info(fmt.Sprintf("%s %s", c.Request.Method, c.Request.URL.Path))
|
||||
}
|
||||
|
||||
func (api *API) templateMiddleware(router *gin.Engine) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
router.HTMLRender = *api.generateTemplates()
|
||||
c.Next()
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
949
api/app-admin-routes.go
Normal file
949
api/app-admin-routes.go
Normal file
@@ -0,0 +1,949 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bufio"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
argon2 "github.com/alexedwards/argon2id"
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/itchyny/gojq"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"reichard.io/antholume/database"
|
||||
"reichard.io/antholume/metadata"
|
||||
"reichard.io/antholume/utils"
|
||||
)
|
||||
|
||||
type adminAction string
|
||||
|
||||
const (
|
||||
adminBackup adminAction = "BACKUP"
|
||||
adminRestore adminAction = "RESTORE"
|
||||
adminMetadataMatch adminAction = "METADATA_MATCH"
|
||||
adminCacheTables adminAction = "CACHE_TABLES"
|
||||
)
|
||||
|
||||
type requestAdminAction struct {
|
||||
Action adminAction `form:"action"`
|
||||
|
||||
// Backup Action
|
||||
BackupTypes []backupType `form:"backup_types"`
|
||||
|
||||
// Restore Action
|
||||
RestoreFile *multipart.FileHeader `form:"restore_file"`
|
||||
}
|
||||
|
||||
type importType string
|
||||
|
||||
const (
|
||||
importDirect importType = "DIRECT"
|
||||
importCopy importType = "COPY"
|
||||
)
|
||||
|
||||
type requestAdminImport struct {
|
||||
Directory string `form:"directory"`
|
||||
Select string `form:"select"`
|
||||
Type importType `form:"type"`
|
||||
}
|
||||
|
||||
type operationType string
|
||||
|
||||
const (
|
||||
opUpdate operationType = "UPDATE"
|
||||
opCreate operationType = "CREATE"
|
||||
opDelete operationType = "DELETE"
|
||||
)
|
||||
|
||||
type requestAdminUpdateUser struct {
|
||||
User string `form:"user"`
|
||||
Password *string `form:"password"`
|
||||
IsAdmin *bool `form:"is_admin"`
|
||||
Operation operationType `form:"operation"`
|
||||
}
|
||||
|
||||
type requestAdminLogs struct {
|
||||
Filter string `form:"filter"`
|
||||
}
|
||||
|
||||
type importStatus string
|
||||
|
||||
const (
|
||||
importFailed importStatus = "FAILED"
|
||||
importSuccess importStatus = "SUCCESS"
|
||||
importExists importStatus = "EXISTS"
|
||||
)
|
||||
|
||||
type importResult struct {
|
||||
ID string
|
||||
Name string
|
||||
Path string
|
||||
Status importStatus
|
||||
Error error
|
||||
}
|
||||
|
||||
func (api *API) appPerformAdminAction(c *gin.Context) {
|
||||
templateVars, _ := api.getBaseTemplateVars("admin", c)
|
||||
|
||||
var rAdminAction requestAdminAction
|
||||
if err := c.ShouldBind(&rAdminAction); err != nil {
|
||||
log.Error("Invalid Form Bind: ", err)
|
||||
appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values")
|
||||
return
|
||||
}
|
||||
|
||||
switch rAdminAction.Action {
|
||||
case adminMetadataMatch:
|
||||
// TODO
|
||||
// 1. Documents xref most recent metadata table?
|
||||
// 2. Select all / deselect?
|
||||
case adminCacheTables:
|
||||
go func() {
|
||||
err := api.db.CacheTempTables()
|
||||
if err != nil {
|
||||
log.Error("Unable to cache temp tables: ", err)
|
||||
}
|
||||
}()
|
||||
case adminRestore:
|
||||
api.processRestoreFile(rAdminAction, c)
|
||||
return
|
||||
case adminBackup:
|
||||
// Vacuum
|
||||
_, err := api.db.DB.ExecContext(api.db.Ctx, "VACUUM;")
|
||||
if err != nil {
|
||||
log.Error("Unable to vacuum DB: ", err)
|
||||
appErrorPage(c, http.StatusInternalServerError, "Unable to vacuum database")
|
||||
return
|
||||
}
|
||||
|
||||
// Set Headers
|
||||
c.Header("Content-type", "application/octet-stream")
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"AnthoLumeBackup_%s.zip\"", time.Now().Format("20060102150405")))
|
||||
|
||||
// Stream Backup ZIP Archive
|
||||
c.Stream(func(w io.Writer) bool {
|
||||
var directories []string
|
||||
for _, item := range rAdminAction.BackupTypes {
|
||||
if item == backupCovers {
|
||||
directories = append(directories, "covers")
|
||||
} else if item == backupDocuments {
|
||||
directories = append(directories, "documents")
|
||||
}
|
||||
}
|
||||
|
||||
err := api.createBackup(w, directories)
|
||||
if err != nil {
|
||||
log.Error("Backup Error: ", err)
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "page/admin", templateVars)
|
||||
}
|
||||
|
||||
func (api *API) appGetAdmin(c *gin.Context) {
|
||||
templateVars, _ := api.getBaseTemplateVars("admin", c)
|
||||
c.HTML(http.StatusOK, "page/admin", templateVars)
|
||||
}
|
||||
|
||||
func (api *API) appGetAdminLogs(c *gin.Context) {
|
||||
templateVars, _ := api.getBaseTemplateVars("admin-logs", c)
|
||||
|
||||
var rAdminLogs requestAdminLogs
|
||||
if err := c.ShouldBindQuery(&rAdminLogs); err != nil {
|
||||
log.Error("Invalid URI Bind")
|
||||
appErrorPage(c, http.StatusNotFound, "Invalid URI parameters")
|
||||
return
|
||||
}
|
||||
rAdminLogs.Filter = strings.TrimSpace(rAdminLogs.Filter)
|
||||
|
||||
var jqFilter *gojq.Code
|
||||
var basicFilter string
|
||||
if strings.HasPrefix(rAdminLogs.Filter, "\"") && strings.HasSuffix(rAdminLogs.Filter, "\"") {
|
||||
basicFilter = rAdminLogs.Filter[1 : len(rAdminLogs.Filter)-1]
|
||||
} else if rAdminLogs.Filter != "" {
|
||||
parsed, err := gojq.Parse(rAdminLogs.Filter)
|
||||
if err != nil {
|
||||
log.Error("Unable to parse JQ filter")
|
||||
appErrorPage(c, http.StatusNotFound, "Unable to parse JQ filter")
|
||||
return
|
||||
}
|
||||
|
||||
jqFilter, err = gojq.Compile(parsed)
|
||||
if err != nil {
|
||||
log.Error("Unable to compile JQ filter")
|
||||
appErrorPage(c, http.StatusNotFound, "Unable to compile JQ filter")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Open Log File
|
||||
logPath := filepath.Join(api.cfg.ConfigPath, "logs/antholume.log")
|
||||
logFile, err := os.Open(logPath)
|
||||
if err != nil {
|
||||
appErrorPage(c, http.StatusBadRequest, "Missing AnthoLume log file")
|
||||
return
|
||||
}
|
||||
defer logFile.Close()
|
||||
|
||||
// Log Lines
|
||||
var logLines []string
|
||||
scanner := bufio.NewScanner(logFile)
|
||||
for scanner.Scan() {
|
||||
rawLog := scanner.Text()
|
||||
|
||||
// Attempt JSON Pretty
|
||||
var jsonMap map[string]any
|
||||
err := json.Unmarshal([]byte(rawLog), &jsonMap)
|
||||
if err != nil {
|
||||
logLines = append(logLines, scanner.Text())
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse JSON
|
||||
rawData, err := json.MarshalIndent(jsonMap, "", " ")
|
||||
if err != nil {
|
||||
logLines = append(logLines, scanner.Text())
|
||||
continue
|
||||
}
|
||||
|
||||
// Basic Filter
|
||||
if basicFilter != "" && strings.Contains(string(rawData), basicFilter) {
|
||||
logLines = append(logLines, string(rawData))
|
||||
continue
|
||||
}
|
||||
|
||||
// No JQ Filter
|
||||
if jqFilter == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Error or nil
|
||||
result, _ := jqFilter.Run(jsonMap).Next()
|
||||
if _, ok := result.(error); ok {
|
||||
logLines = append(logLines, string(rawData))
|
||||
continue
|
||||
} else if result == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Attempt filtered json
|
||||
filteredData, err := json.MarshalIndent(result, "", " ")
|
||||
if err == nil {
|
||||
rawData = filteredData
|
||||
}
|
||||
|
||||
logLines = append(logLines, string(rawData))
|
||||
}
|
||||
|
||||
templateVars["Data"] = logLines
|
||||
templateVars["Filter"] = rAdminLogs.Filter
|
||||
|
||||
c.HTML(http.StatusOK, "page/admin-logs", templateVars)
|
||||
}
|
||||
|
||||
func (api *API) appGetAdminUsers(c *gin.Context) {
|
||||
templateVars, _ := api.getBaseTemplateVars("admin-users", c)
|
||||
|
||||
users, err := api.db.Queries.GetUsers(api.db.Ctx)
|
||||
if err != nil {
|
||||
log.Error("GetUsers DB Error: ", err)
|
||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUsers DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
templateVars["Data"] = users
|
||||
|
||||
c.HTML(http.StatusOK, "page/admin-users", templateVars)
|
||||
}
|
||||
|
||||
func (api *API) appUpdateAdminUsers(c *gin.Context) {
|
||||
templateVars, _ := api.getBaseTemplateVars("admin-users", c)
|
||||
|
||||
var rUpdate requestAdminUpdateUser
|
||||
if err := c.ShouldBind(&rUpdate); err != nil {
|
||||
log.Error("Invalid URI Bind")
|
||||
appErrorPage(c, http.StatusNotFound, "Invalid user parameters")
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure Username
|
||||
if rUpdate.User == "" {
|
||||
appErrorPage(c, http.StatusInternalServerError, "User cannot be empty")
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
switch rUpdate.Operation {
|
||||
case opCreate:
|
||||
err = api.createUser(rUpdate.User, rUpdate.Password, rUpdate.IsAdmin)
|
||||
case opUpdate:
|
||||
err = api.updateUser(rUpdate.User, rUpdate.Password, rUpdate.IsAdmin)
|
||||
case opDelete:
|
||||
err = api.deleteUser(rUpdate.User)
|
||||
default:
|
||||
appErrorPage(c, http.StatusNotFound, "Unknown user operation")
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("Unable to create or update user: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
users, err := api.db.Queries.GetUsers(api.db.Ctx)
|
||||
if err != nil {
|
||||
log.Error("GetUsers DB Error: ", err)
|
||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUsers DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
templateVars["Data"] = users
|
||||
|
||||
c.HTML(http.StatusOK, "page/admin-users", templateVars)
|
||||
}
|
||||
|
||||
func (api *API) appGetAdminImport(c *gin.Context) {
|
||||
templateVars, _ := api.getBaseTemplateVars("admin-import", c)
|
||||
|
||||
var rImportFolder requestAdminImport
|
||||
if err := c.ShouldBindQuery(&rImportFolder); err != nil {
|
||||
log.Error("Invalid URI Bind")
|
||||
appErrorPage(c, http.StatusNotFound, "Invalid directory")
|
||||
return
|
||||
}
|
||||
|
||||
if rImportFolder.Select != "" {
|
||||
templateVars["SelectedDirectory"] = rImportFolder.Select
|
||||
c.HTML(http.StatusOK, "page/admin-import", templateVars)
|
||||
return
|
||||
}
|
||||
|
||||
// Default Path
|
||||
if rImportFolder.Directory == "" {
|
||||
dPath, err := filepath.Abs(api.cfg.DataPath)
|
||||
if err != nil {
|
||||
log.Error("Absolute filepath error: ", rImportFolder.Directory)
|
||||
appErrorPage(c, http.StatusNotFound, "Unable to get data directory absolute path")
|
||||
return
|
||||
}
|
||||
|
||||
rImportFolder.Directory = dPath
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(rImportFolder.Directory)
|
||||
if err != nil {
|
||||
log.Error("Invalid directory: ", rImportFolder.Directory)
|
||||
appErrorPage(c, http.StatusNotFound, "Invalid directory")
|
||||
return
|
||||
}
|
||||
|
||||
allDirectories := []string{}
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
allDirectories = append(allDirectories, e.Name())
|
||||
}
|
||||
|
||||
templateVars["CurrentPath"] = filepath.Clean(rImportFolder.Directory)
|
||||
templateVars["Data"] = allDirectories
|
||||
|
||||
c.HTML(http.StatusOK, "page/admin-import", templateVars)
|
||||
}
|
||||
|
||||
func (api *API) appPerformAdminImport(c *gin.Context) {
|
||||
templateVars, _ := api.getBaseTemplateVars("admin-import", c)
|
||||
|
||||
var rAdminImport requestAdminImport
|
||||
if err := c.ShouldBind(&rAdminImport); err != nil {
|
||||
log.Error("Invalid URI Bind")
|
||||
appErrorPage(c, http.StatusNotFound, "Invalid directory")
|
||||
return
|
||||
}
|
||||
|
||||
// Get import directory
|
||||
importDirectory := filepath.Clean(rAdminImport.Directory)
|
||||
|
||||
// Get data directory
|
||||
absoluteDataPath, _ := filepath.Abs(filepath.Join(api.cfg.DataPath, "documents"))
|
||||
|
||||
// Validate different path
|
||||
if absoluteDataPath == importDirectory {
|
||||
appErrorPage(c, http.StatusBadRequest, "Directory is the same as data path")
|
||||
return
|
||||
}
|
||||
|
||||
// Do Transaction
|
||||
tx, err := api.db.DB.Begin()
|
||||
if err != nil {
|
||||
log.Error("Transaction Begin DB Error:", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Unknown error")
|
||||
return
|
||||
}
|
||||
|
||||
// Defer & Start Transaction
|
||||
defer func() {
|
||||
if err := tx.Rollback(); err != nil {
|
||||
log.Error("DB Rollback Error:", err)
|
||||
}
|
||||
}()
|
||||
qtx := api.db.Queries.WithTx(tx)
|
||||
|
||||
// Track imports
|
||||
importResults := make([]importResult, 0)
|
||||
|
||||
// Walk Directory & Import
|
||||
err = filepath.WalkDir(importDirectory, func(importPath string, f fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if f.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get relative path
|
||||
basePath := importDirectory
|
||||
relFilePath, err := filepath.Rel(importDirectory, importPath)
|
||||
if err != nil {
|
||||
log.Warnf("path error: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Track imports
|
||||
iResult := importResult{
|
||||
Path: relFilePath,
|
||||
Status: importFailed,
|
||||
}
|
||||
defer func() {
|
||||
importResults = append(importResults, iResult)
|
||||
}()
|
||||
|
||||
// Get metadata
|
||||
fileMeta, err := metadata.GetMetadata(importPath)
|
||||
if err != nil {
|
||||
log.Errorf("metadata error: %v", err)
|
||||
iResult.Error = err
|
||||
return nil
|
||||
}
|
||||
iResult.ID = *fileMeta.PartialMD5
|
||||
iResult.Name = fmt.Sprintf("%s - %s", *fileMeta.Author, *fileMeta.Title)
|
||||
|
||||
// Check already exists
|
||||
_, err = qtx.GetDocument(api.db.Ctx, *fileMeta.PartialMD5)
|
||||
if err == nil {
|
||||
log.Warnf("document already exists: %s", *fileMeta.PartialMD5)
|
||||
iResult.Status = importExists
|
||||
return nil
|
||||
}
|
||||
|
||||
// Import Copy
|
||||
if rAdminImport.Type == importCopy {
|
||||
// Derive & Sanitize File Name
|
||||
relFilePath = deriveBaseFileName(fileMeta)
|
||||
safePath := filepath.Join(api.cfg.DataPath, "documents", relFilePath)
|
||||
|
||||
// Open Source File
|
||||
srcFile, err := os.Open(importPath)
|
||||
if err != nil {
|
||||
log.Errorf("unable to open current file: %v", err)
|
||||
iResult.Error = err
|
||||
return nil
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
// Open Destination File
|
||||
destFile, err := os.Create(safePath)
|
||||
if err != nil {
|
||||
log.Errorf("unable to open destination file: %v", err)
|
||||
iResult.Error = err
|
||||
return nil
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
// Copy File
|
||||
if _, err = io.Copy(destFile, srcFile); err != nil {
|
||||
log.Errorf("unable to save file: %v", err)
|
||||
iResult.Error = err
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update Base & Path
|
||||
basePath = filepath.Join(api.cfg.DataPath, "documents")
|
||||
iResult.Path = relFilePath
|
||||
}
|
||||
|
||||
// Upsert document
|
||||
if _, err = qtx.UpsertDocument(api.db.Ctx, database.UpsertDocumentParams{
|
||||
ID: *fileMeta.PartialMD5,
|
||||
Title: fileMeta.Title,
|
||||
Author: fileMeta.Author,
|
||||
Description: fileMeta.Description,
|
||||
Md5: fileMeta.MD5,
|
||||
Words: fileMeta.WordCount,
|
||||
Filepath: &relFilePath,
|
||||
Basepath: &basePath,
|
||||
}); err != nil {
|
||||
log.Errorf("UpsertDocument DB Error: %v", err)
|
||||
iResult.Error = err
|
||||
return nil
|
||||
}
|
||||
|
||||
iResult.Status = importSuccess
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("Import Failed: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Commit transaction
|
||||
if err := tx.Commit(); err != nil {
|
||||
log.Error("Transaction Commit DB Error: ", err)
|
||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("Import DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Sort import results
|
||||
sort.Slice(importResults, func(i int, j int) bool {
|
||||
return importStatusPriority(importResults[i].Status) <
|
||||
importStatusPriority(importResults[j].Status)
|
||||
})
|
||||
|
||||
templateVars["Data"] = importResults
|
||||
c.HTML(http.StatusOK, "page/admin-import-results", templateVars)
|
||||
}
|
||||
|
||||
func (api *API) processRestoreFile(rAdminAction requestAdminAction, c *gin.Context) {
|
||||
// Validate Type & Derive Extension on MIME
|
||||
uploadedFile, err := rAdminAction.RestoreFile.Open()
|
||||
if err != nil {
|
||||
log.Error("File Error: ", err)
|
||||
appErrorPage(c, http.StatusInternalServerError, "Unable to open file")
|
||||
return
|
||||
}
|
||||
|
||||
fileMime, err := mimetype.DetectReader(uploadedFile)
|
||||
if err != nil {
|
||||
log.Error("MIME Error")
|
||||
appErrorPage(c, http.StatusInternalServerError, "Unable to detect filetype")
|
||||
return
|
||||
}
|
||||
fileExtension := fileMime.Extension()
|
||||
|
||||
// Validate Extension
|
||||
if !slices.Contains([]string{".zip"}, fileExtension) {
|
||||
log.Error("Invalid FileType: ", fileExtension)
|
||||
appErrorPage(c, http.StatusBadRequest, "Invalid filetype")
|
||||
return
|
||||
}
|
||||
|
||||
// Create Temp File
|
||||
tempFile, err := os.CreateTemp("", "restore")
|
||||
if err != nil {
|
||||
log.Warn("Temp File Create Error: ", err)
|
||||
appErrorPage(c, http.StatusInternalServerError, "Unable to create temp file")
|
||||
return
|
||||
}
|
||||
defer os.Remove(tempFile.Name())
|
||||
defer tempFile.Close()
|
||||
|
||||
// Save Temp
|
||||
err = c.SaveUploadedFile(rAdminAction.RestoreFile, tempFile.Name())
|
||||
if err != nil {
|
||||
log.Error("File Error: ", err)
|
||||
appErrorPage(c, http.StatusInternalServerError, "Unable to save file")
|
||||
return
|
||||
}
|
||||
|
||||
// ZIP Info
|
||||
fileInfo, err := tempFile.Stat()
|
||||
if err != nil {
|
||||
log.Error("File Error: ", err)
|
||||
appErrorPage(c, http.StatusInternalServerError, "Unable to read file")
|
||||
return
|
||||
}
|
||||
|
||||
// Create ZIP Reader
|
||||
zipReader, err := zip.NewReader(tempFile, fileInfo.Size())
|
||||
if err != nil {
|
||||
log.Error("ZIP Error: ", err)
|
||||
appErrorPage(c, http.StatusInternalServerError, "Unable to read zip")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate ZIP Contents
|
||||
hasDBFile := false
|
||||
hasUnknownFile := false
|
||||
for _, file := range zipReader.File {
|
||||
fileName := strings.TrimPrefix(file.Name, "/")
|
||||
if fileName == "antholume.db" {
|
||||
hasDBFile = true
|
||||
break
|
||||
} else if !strings.HasPrefix(fileName, "covers/") && !strings.HasPrefix(fileName, "documents/") {
|
||||
hasUnknownFile = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Invalid ZIP
|
||||
if !hasDBFile {
|
||||
log.Error("Invalid ZIP File - Missing DB")
|
||||
appErrorPage(c, http.StatusInternalServerError, "Invalid Restore ZIP - Missing DB")
|
||||
return
|
||||
} else if hasUnknownFile {
|
||||
log.Error("Invalid ZIP File - Invalid File(s)")
|
||||
appErrorPage(c, http.StatusInternalServerError, "Invalid Restore ZIP - Invalid File(s)")
|
||||
return
|
||||
}
|
||||
|
||||
// Create Backup File
|
||||
backupFilePath := filepath.Join(api.cfg.ConfigPath, fmt.Sprintf("backups/AnthoLumeBackup_%s.zip", time.Now().Format("20060102150405")))
|
||||
backupFile, err := os.Create(backupFilePath)
|
||||
if err != nil {
|
||||
log.Error("Unable to create backup file: ", err)
|
||||
appErrorPage(c, http.StatusInternalServerError, "Unable to create backup file")
|
||||
return
|
||||
}
|
||||
defer backupFile.Close()
|
||||
|
||||
// Save Backup File
|
||||
w := bufio.NewWriter(backupFile)
|
||||
err = api.createBackup(w, []string{"covers", "documents"})
|
||||
if err != nil {
|
||||
log.Error("Unable to save backup file: ", err)
|
||||
appErrorPage(c, http.StatusInternalServerError, "Unable to save backup file")
|
||||
return
|
||||
}
|
||||
|
||||
// Remove Data
|
||||
err = api.removeData()
|
||||
if err != nil {
|
||||
log.Error("Unable to delete data: ", err)
|
||||
appErrorPage(c, http.StatusInternalServerError, "Unable to delete data")
|
||||
return
|
||||
}
|
||||
|
||||
// Restore Data
|
||||
err = api.restoreData(zipReader)
|
||||
if err != nil {
|
||||
appErrorPage(c, http.StatusInternalServerError, "Unable to restore data")
|
||||
log.Panic("Unable to restore data: ", err)
|
||||
}
|
||||
|
||||
// Reinit DB
|
||||
if err := api.db.Reload(); err != nil {
|
||||
appErrorPage(c, http.StatusInternalServerError, "Unable to reload DB")
|
||||
log.Panicf("Unable to reload DB: %v", err)
|
||||
}
|
||||
|
||||
// Rotate Auth Hashes
|
||||
if err := api.rotateAllAuthHashes(); err != nil {
|
||||
appErrorPage(c, http.StatusInternalServerError, "Unable to rotate hashes")
|
||||
log.Panicf("Unable to rotate auth hashes: %v", err)
|
||||
}
|
||||
|
||||
// Redirect to login page
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
}
|
||||
|
||||
func (api *API) restoreData(zipReader *zip.Reader) error {
|
||||
// Ensure Directories
|
||||
api.cfg.EnsureDirectories()
|
||||
|
||||
// Restore Data
|
||||
for _, file := range zipReader.File {
|
||||
rc, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
destPath := filepath.Join(api.cfg.DataPath, file.Name)
|
||||
destFile, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
log.Errorf("error creating destination file: %v", err)
|
||||
return err
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
// Copy the contents from the zip file to the destination file.
|
||||
if _, err := io.Copy(destFile, rc); err != nil {
|
||||
log.Errorf("Error copying file contents: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *API) removeData() error {
|
||||
allPaths := []string{
|
||||
"covers",
|
||||
"documents",
|
||||
"antholume.db",
|
||||
"antholume.db-wal",
|
||||
"antholume.db-shm",
|
||||
}
|
||||
|
||||
for _, name := range allPaths {
|
||||
fullPath := filepath.Join(api.cfg.DataPath, name)
|
||||
err := os.RemoveAll(fullPath)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to delete %s: %v", name, err)
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *API) createBackup(w io.Writer, directories []string) error {
|
||||
// Vacuum DB
|
||||
_, err := api.db.DB.ExecContext(api.db.Ctx, "VACUUM;")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Unable to vacuum database")
|
||||
}
|
||||
|
||||
ar := zip.NewWriter(w)
|
||||
exportWalker := func(currentPath string, f fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if f.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Open File on Disk
|
||||
file, err := os.Open(currentPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Derive Export Structure
|
||||
fileName := filepath.Base(currentPath)
|
||||
folderName := filepath.Base(filepath.Dir(currentPath))
|
||||
|
||||
// Create File in Export
|
||||
newF, err := ar.Create(filepath.Join(folderName, fileName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy File in Export
|
||||
_, err = io.Copy(newF, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get DB Path
|
||||
fileName := fmt.Sprintf("%s.db", api.cfg.DBName)
|
||||
dbLocation := filepath.Join(api.cfg.ConfigPath, fileName)
|
||||
|
||||
// Copy Database File
|
||||
dbFile, err := os.Open(dbLocation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dbFile.Close()
|
||||
|
||||
newDbFile, err := ar.Create(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(newDbFile, dbFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Backup Covers & Documents
|
||||
for _, dir := range directories {
|
||||
err = filepath.WalkDir(filepath.Join(api.cfg.DataPath, dir), exportWalker)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ar.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *API) isLastAdmin(userID string) (bool, error) {
|
||||
allUsers, err := api.db.Queries.GetUsers(api.db.Ctx)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, fmt.Sprintf("GetUsers DB Error: %v", err))
|
||||
}
|
||||
|
||||
hasAdmin := false
|
||||
for _, user := range allUsers {
|
||||
if user.Admin && user.ID != userID {
|
||||
hasAdmin = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return !hasAdmin, nil
|
||||
}
|
||||
|
||||
func (api *API) createUser(user string, rawPassword *string, isAdmin *bool) error {
|
||||
// Validate Necessary Parameters
|
||||
if rawPassword == nil || *rawPassword == "" {
|
||||
return fmt.Errorf("password can't be empty")
|
||||
}
|
||||
|
||||
// Base Params
|
||||
createParams := database.CreateUserParams{
|
||||
ID: user,
|
||||
}
|
||||
|
||||
// Handle Admin (Explicit or False)
|
||||
if isAdmin != nil {
|
||||
createParams.Admin = *isAdmin
|
||||
} else {
|
||||
createParams.Admin = false
|
||||
}
|
||||
|
||||
// Parse Password
|
||||
password := fmt.Sprintf("%x", md5.Sum([]byte(*rawPassword)))
|
||||
hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create hashed password")
|
||||
}
|
||||
createParams.Pass = &hashedPassword
|
||||
|
||||
// Generate Auth Hash
|
||||
rawAuthHash, err := utils.GenerateToken(64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create token for user")
|
||||
}
|
||||
authHash := fmt.Sprintf("%x", rawAuthHash)
|
||||
createParams.AuthHash = &authHash
|
||||
|
||||
// Create user in DB
|
||||
if rows, err := api.db.Queries.CreateUser(api.db.Ctx, createParams); err != nil {
|
||||
log.Error("CreateUser DB Error:", err)
|
||||
return fmt.Errorf("unable to create user")
|
||||
} else if rows == 0 {
|
||||
log.Warn("User Already Exists:", createParams.ID)
|
||||
return fmt.Errorf("user already exists")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *API) updateUser(user string, rawPassword *string, isAdmin *bool) error {
|
||||
// Validate Necessary Parameters
|
||||
if rawPassword == nil && isAdmin == nil {
|
||||
return fmt.Errorf("nothing to update")
|
||||
}
|
||||
|
||||
// Base Params
|
||||
updateParams := database.UpdateUserParams{
|
||||
UserID: user,
|
||||
}
|
||||
|
||||
// Handle Admin (Update or Existing)
|
||||
if isAdmin != nil {
|
||||
updateParams.Admin = *isAdmin
|
||||
} else {
|
||||
user, err := api.db.Queries.GetUser(api.db.Ctx, user)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("GetUser DB Error: %v", err))
|
||||
}
|
||||
updateParams.Admin = user.Admin
|
||||
}
|
||||
|
||||
// Check Admins - Disallow Demotion
|
||||
if isLast, err := api.isLastAdmin(user); err != nil {
|
||||
return err
|
||||
} else if isLast && !updateParams.Admin {
|
||||
return fmt.Errorf("unable to demote %s - last admin", user)
|
||||
}
|
||||
|
||||
// Handle Password
|
||||
if rawPassword != nil {
|
||||
if *rawPassword == "" {
|
||||
return fmt.Errorf("password can't be empty")
|
||||
}
|
||||
|
||||
// Parse Password
|
||||
password := fmt.Sprintf("%x", md5.Sum([]byte(*rawPassword)))
|
||||
hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create hashed password")
|
||||
}
|
||||
updateParams.Password = &hashedPassword
|
||||
|
||||
// Generate Auth Hash
|
||||
rawAuthHash, err := utils.GenerateToken(64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create token for user")
|
||||
}
|
||||
authHash := fmt.Sprintf("%x", rawAuthHash)
|
||||
updateParams.AuthHash = &authHash
|
||||
}
|
||||
|
||||
// Update User
|
||||
_, err := api.db.Queries.UpdateUser(api.db.Ctx, updateParams)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("UpdateUser DB Error: %v", err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *API) deleteUser(user string) error {
|
||||
// Check Admins
|
||||
if isLast, err := api.isLastAdmin(user); err != nil {
|
||||
return err
|
||||
} else if isLast {
|
||||
return fmt.Errorf("unable to delete %s - last admin", user)
|
||||
}
|
||||
|
||||
// Create Backup File
|
||||
backupFilePath := filepath.Join(api.cfg.ConfigPath, fmt.Sprintf("backups/AnthoLumeBackup_%s.zip", time.Now().Format("20060102150405")))
|
||||
backupFile, err := os.Create(backupFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer backupFile.Close()
|
||||
|
||||
// Save Backup File (DB Only)
|
||||
w := bufio.NewWriter(backupFile)
|
||||
err = api.createBackup(w, []string{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete User
|
||||
_, err = api.db.Queries.DeleteUser(api.db.Ctx, user)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("DeleteUser DB Error: %v", err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
1149
api/app-routes.go
1149
api/app-routes.go
File diff suppressed because it is too large
Load Diff
393
api/auth.go
393
api/auth.go
@@ -11,39 +11,49 @@ import (
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"reichard.io/bbank/database"
|
||||
"reichard.io/antholume/database"
|
||||
"reichard.io/antholume/utils"
|
||||
)
|
||||
|
||||
// Authorization Data
|
||||
type authData struct {
|
||||
UserName string
|
||||
IsAdmin bool
|
||||
AuthHash string
|
||||
}
|
||||
|
||||
// KOSync API Auth Headers
|
||||
type authKOHeader struct {
|
||||
AuthUser string `header:"x-auth-user"`
|
||||
AuthKey string `header:"x-auth-key"`
|
||||
}
|
||||
|
||||
// OPDS Auth Headers
|
||||
type authOPDSHeader struct {
|
||||
Authorization string `header:"authorization"`
|
||||
}
|
||||
|
||||
func (api *API) authorizeCredentials(username string, password string) (authorized bool) {
|
||||
user, err := api.DB.Queries.GetUser(api.DB.Ctx, username)
|
||||
func (api *API) authorizeCredentials(username string, password string) (auth *authData) {
|
||||
user, err := api.db.Queries.GetUser(api.db.Ctx, username)
|
||||
if err != nil {
|
||||
return false
|
||||
return
|
||||
}
|
||||
|
||||
if match, err := argon2.ComparePasswordAndHash(password, *user.Pass); err != nil || match != true {
|
||||
return false
|
||||
if match, err := argon2.ComparePasswordAndHash(password, *user.Pass); err != nil || !match {
|
||||
return
|
||||
}
|
||||
|
||||
return true
|
||||
// Update auth cache
|
||||
api.userAuthCache[user.ID] = *user.AuthHash
|
||||
|
||||
return &authData{
|
||||
UserName: user.ID,
|
||||
IsAdmin: user.Admin,
|
||||
AuthHash: *user.AuthHash,
|
||||
}
|
||||
}
|
||||
|
||||
func (api *API) authKOMiddleware(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
|
||||
// Check Session First
|
||||
if user, ok := getSession(session); ok == true {
|
||||
c.Set("AuthorizedUser", user)
|
||||
if auth, ok := api.getSession(session); ok {
|
||||
c.Set("Authorization", auth)
|
||||
c.Header("Cache-Control", "private")
|
||||
c.Next()
|
||||
return
|
||||
@@ -61,17 +71,18 @@ func (api *API) authKOMiddleware(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if authorized := api.authorizeCredentials(rHeader.AuthUser, rHeader.AuthKey); authorized != true {
|
||||
authData := api.authorizeCredentials(rHeader.AuthUser, rHeader.AuthKey)
|
||||
if authData == nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := setSession(session, rHeader.AuthUser); err != nil {
|
||||
if err := api.setSession(session, *authData); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("AuthorizedUser", rHeader.AuthUser)
|
||||
c.Set("Authorization", *authData)
|
||||
c.Header("Cache-Control", "private")
|
||||
c.Next()
|
||||
}
|
||||
@@ -82,19 +93,20 @@ func (api *API) authOPDSMiddleware(c *gin.Context) {
|
||||
user, rawPassword, hasAuth := c.Request.BasicAuth()
|
||||
|
||||
// Validate Auth Fields
|
||||
if hasAuth != true || user == "" || rawPassword == "" {
|
||||
if !hasAuth || user == "" || rawPassword == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization Headers"})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate Auth
|
||||
password := fmt.Sprintf("%x", md5.Sum([]byte(rawPassword)))
|
||||
if authorized := api.authorizeCredentials(user, password); authorized != true {
|
||||
authData := api.authorizeCredentials(user, password)
|
||||
if authData == nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("AuthorizedUser", user)
|
||||
c.Set("Authorization", *authData)
|
||||
c.Header("Cache-Control", "private")
|
||||
c.Next()
|
||||
}
|
||||
@@ -103,8 +115,8 @@ func (api *API) authWebAppMiddleware(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
|
||||
// Check Session
|
||||
if user, ok := getSession(session); ok == true {
|
||||
c.Set("AuthorizedUser", user)
|
||||
if auth, ok := api.getSession(session); ok {
|
||||
c.Set("Authorization", auth)
|
||||
c.Header("Cache-Control", "private")
|
||||
c.Next()
|
||||
return
|
||||
@@ -112,38 +124,47 @@ func (api *API) authWebAppMiddleware(c *gin.Context) {
|
||||
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
func (api *API) authFormLogin(c *gin.Context) {
|
||||
func (api *API) authAdminWebAppMiddleware(c *gin.Context) {
|
||||
if data, _ := c.Get("Authorization"); data != nil {
|
||||
auth := data.(authData)
|
||||
if auth.IsAdmin {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
appErrorPage(c, http.StatusUnauthorized, "Admin Permissions Required")
|
||||
c.Abort()
|
||||
}
|
||||
|
||||
func (api *API) appAuthLogin(c *gin.Context) {
|
||||
templateVars, _ := api.getBaseTemplateVars("login", c)
|
||||
|
||||
username := strings.TrimSpace(c.PostForm("username"))
|
||||
rawPassword := strings.TrimSpace(c.PostForm("password"))
|
||||
|
||||
if username == "" || rawPassword == "" {
|
||||
c.HTML(http.StatusUnauthorized, "login", gin.H{
|
||||
"RegistrationEnabled": api.Config.RegistrationEnabled,
|
||||
"Error": "Invalid Credentials",
|
||||
})
|
||||
templateVars["Error"] = "Invalid Credentials"
|
||||
c.HTML(http.StatusUnauthorized, "page/login", templateVars)
|
||||
return
|
||||
}
|
||||
|
||||
// MD5 - KOSync Compatiblity
|
||||
password := fmt.Sprintf("%x", md5.Sum([]byte(rawPassword)))
|
||||
if authorized := api.authorizeCredentials(username, password); authorized != true {
|
||||
c.HTML(http.StatusUnauthorized, "login", gin.H{
|
||||
"RegistrationEnabled": api.Config.RegistrationEnabled,
|
||||
"Error": "Invalid Credentials",
|
||||
})
|
||||
authData := api.authorizeCredentials(username, password)
|
||||
if authData == nil {
|
||||
templateVars["Error"] = "Invalid Credentials"
|
||||
c.HTML(http.StatusUnauthorized, "page/login", templateVars)
|
||||
return
|
||||
}
|
||||
|
||||
// Set Session
|
||||
session := sessions.Default(c)
|
||||
if err := setSession(session, username); err != nil {
|
||||
c.HTML(http.StatusUnauthorized, "login", gin.H{
|
||||
"RegistrationEnabled": api.Config.RegistrationEnabled,
|
||||
"Error": "Unknown Error",
|
||||
})
|
||||
if err := api.setSession(session, *authData); err != nil {
|
||||
templateVars["Error"] = "Invalid Credentials"
|
||||
c.HTML(http.StatusUnauthorized, "page/login", templateVars)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -151,60 +172,93 @@ func (api *API) authFormLogin(c *gin.Context) {
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
}
|
||||
|
||||
func (api *API) authFormRegister(c *gin.Context) {
|
||||
if !api.Config.RegistrationEnabled {
|
||||
errorPage(c, http.StatusUnauthorized, "Nice try. Registration is disabled.")
|
||||
func (api *API) appAuthRegister(c *gin.Context) {
|
||||
if !api.cfg.RegistrationEnabled {
|
||||
appErrorPage(c, http.StatusUnauthorized, "Nice try. Registration is disabled.")
|
||||
return
|
||||
}
|
||||
|
||||
templateVars, _ := api.getBaseTemplateVars("login", c)
|
||||
templateVars["Register"] = true
|
||||
|
||||
username := strings.TrimSpace(c.PostForm("username"))
|
||||
rawPassword := strings.TrimSpace(c.PostForm("password"))
|
||||
|
||||
if username == "" || rawPassword == "" {
|
||||
c.HTML(http.StatusBadRequest, "login", gin.H{
|
||||
"Register": true,
|
||||
"Error": "Registration Disabled or User Already Exists",
|
||||
})
|
||||
templateVars["Error"] = "Invalid User or Password"
|
||||
c.HTML(http.StatusBadRequest, "page/login", templateVars)
|
||||
return
|
||||
}
|
||||
password := fmt.Sprintf("%x", md5.Sum([]byte(rawPassword)))
|
||||
|
||||
hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams)
|
||||
if err != nil {
|
||||
c.HTML(http.StatusBadRequest, "login", gin.H{
|
||||
"Register": true,
|
||||
"Error": "Registration Disabled or User Already Exists",
|
||||
})
|
||||
templateVars["Error"] = "Registration Disabled or User Already Exists"
|
||||
c.HTML(http.StatusBadRequest, "page/login", templateVars)
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := api.DB.Queries.CreateUser(api.DB.Ctx, database.CreateUserParams{
|
||||
ID: username,
|
||||
Pass: &hashedPassword,
|
||||
})
|
||||
|
||||
// SQL Error
|
||||
// Generate auth hash
|
||||
rawAuthHash, err := utils.GenerateToken(64)
|
||||
if err != nil {
|
||||
c.HTML(http.StatusBadRequest, "login", gin.H{
|
||||
"Register": true,
|
||||
"Error": "Registration Disabled or User Already Exists",
|
||||
})
|
||||
log.Error("Failed to generate user token: ", err)
|
||||
templateVars["Error"] = "Failed to Create User"
|
||||
c.HTML(http.StatusBadRequest, "page/login", templateVars)
|
||||
return
|
||||
}
|
||||
|
||||
// User Already Exists
|
||||
if rows == 0 {
|
||||
c.HTML(http.StatusBadRequest, "login", gin.H{
|
||||
"Register": true,
|
||||
"Error": "Registration Disabled or User Already Exists",
|
||||
})
|
||||
// Get current users
|
||||
currentUsers, err := api.db.Queries.GetUsers(api.db.Ctx)
|
||||
if err != nil {
|
||||
log.Error("Failed to check all users: ", err)
|
||||
templateVars["Error"] = "Failed to Create User"
|
||||
c.HTML(http.StatusBadRequest, "page/login", templateVars)
|
||||
return
|
||||
}
|
||||
|
||||
// Set Session
|
||||
// Determine if we should be admin
|
||||
isAdmin := false
|
||||
if len(currentUsers) == 0 {
|
||||
isAdmin = true
|
||||
}
|
||||
|
||||
// Create user in DB
|
||||
authHash := fmt.Sprintf("%x", rawAuthHash)
|
||||
if rows, err := api.db.Queries.CreateUser(api.db.Ctx, database.CreateUserParams{
|
||||
ID: username,
|
||||
Pass: &hashedPassword,
|
||||
AuthHash: &authHash,
|
||||
Admin: isAdmin,
|
||||
}); err != nil {
|
||||
log.Error("CreateUser DB Error:", err)
|
||||
templateVars["Error"] = "Registration Disabled or User Already Exists"
|
||||
c.HTML(http.StatusBadRequest, "page/login", templateVars)
|
||||
return
|
||||
} else if rows == 0 {
|
||||
log.Warn("User Already Exists:", username)
|
||||
templateVars["Error"] = "Registration Disabled or User Already Exists"
|
||||
c.HTML(http.StatusBadRequest, "page/login", templateVars)
|
||||
return
|
||||
}
|
||||
|
||||
// Get user
|
||||
user, err := api.db.Queries.GetUser(api.db.Ctx, username)
|
||||
if err != nil {
|
||||
log.Error("GetUser DB Error:", err)
|
||||
templateVars["Error"] = "Registration Disabled or User Already Exists"
|
||||
c.HTML(http.StatusBadRequest, "page/login", templateVars)
|
||||
return
|
||||
}
|
||||
|
||||
// Set session
|
||||
auth := authData{
|
||||
UserName: user.ID,
|
||||
IsAdmin: user.Admin,
|
||||
AuthHash: *user.AuthHash,
|
||||
}
|
||||
session := sessions.Default(c)
|
||||
if err := setSession(session, username); err != nil {
|
||||
errorPage(c, http.StatusUnauthorized, "Unauthorized.")
|
||||
if err := api.setSession(session, auth); err != nil {
|
||||
appErrorPage(c, http.StatusUnauthorized, "Unauthorized.")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -212,41 +266,206 @@ func (api *API) authFormRegister(c *gin.Context) {
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
}
|
||||
|
||||
func (api *API) authLogout(c *gin.Context) {
|
||||
func (api *API) appAuthLogout(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
session.Clear()
|
||||
session.Save()
|
||||
if err := session.Save(); err != nil {
|
||||
log.Error("unable to save session")
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
}
|
||||
|
||||
func (api *API) demoModeAppError(c *gin.Context) {
|
||||
errorPage(c, http.StatusUnauthorized, "Not Allowed in Demo Mode")
|
||||
func (api *API) koAuthRegister(c *gin.Context) {
|
||||
if !api.cfg.RegistrationEnabled {
|
||||
c.AbortWithStatus(http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
var rUser requestUser
|
||||
if err := c.ShouldBindJSON(&rUser); err != nil {
|
||||
log.Error("Invalid JSON Bind")
|
||||
apiErrorPage(c, http.StatusBadRequest, "Invalid User Data")
|
||||
return
|
||||
}
|
||||
|
||||
if rUser.Username == "" || rUser.Password == "" {
|
||||
log.Error("Invalid User - Empty Username or Password")
|
||||
apiErrorPage(c, http.StatusBadRequest, "Invalid User Data")
|
||||
return
|
||||
}
|
||||
|
||||
// Generate password hash
|
||||
hashedPassword, err := argon2.CreateHash(rUser.Password, argon2.DefaultParams)
|
||||
if err != nil {
|
||||
log.Error("Argon2 Hash Failure:", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Unknown Error")
|
||||
return
|
||||
}
|
||||
|
||||
// Generate auth hash
|
||||
rawAuthHash, err := utils.GenerateToken(64)
|
||||
if err != nil {
|
||||
log.Error("Failed to generate user token: ", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Unknown Error")
|
||||
return
|
||||
}
|
||||
|
||||
// Get current users
|
||||
currentUsers, err := api.db.Queries.GetUsers(api.db.Ctx)
|
||||
if err != nil {
|
||||
log.Error("Failed to check all users: ", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Failed to Create User")
|
||||
return
|
||||
}
|
||||
|
||||
// Determine if we should be admin
|
||||
isAdmin := false
|
||||
if len(currentUsers) == 0 {
|
||||
isAdmin = true
|
||||
}
|
||||
|
||||
// Create user
|
||||
authHash := fmt.Sprintf("%x", rawAuthHash)
|
||||
if rows, err := api.db.Queries.CreateUser(api.db.Ctx, database.CreateUserParams{
|
||||
ID: rUser.Username,
|
||||
Pass: &hashedPassword,
|
||||
AuthHash: &authHash,
|
||||
Admin: isAdmin,
|
||||
}); err != nil {
|
||||
log.Error("CreateUser DB Error:", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Invalid User Data")
|
||||
return
|
||||
} else if rows == 0 {
|
||||
log.Error("User Already Exists:", rUser.Username)
|
||||
apiErrorPage(c, http.StatusBadRequest, "User Already Exists")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"username": rUser.Username,
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) demoModeJSONError(c *gin.Context) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Not Allowed in Demo Mode"})
|
||||
}
|
||||
|
||||
func getSession(session sessions.Session) (user string, ok bool) {
|
||||
// Check Session
|
||||
func (api *API) getSession(session sessions.Session) (auth authData, ok bool) {
|
||||
// Get Session
|
||||
authorizedUser := session.Get("authorizedUser")
|
||||
if authorizedUser == nil {
|
||||
return "", false
|
||||
isAdmin := session.Get("isAdmin")
|
||||
expiresAt := session.Get("expiresAt")
|
||||
authHash := session.Get("authHash")
|
||||
if authorizedUser == nil || isAdmin == nil || expiresAt == nil || authHash == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Create Auth Object
|
||||
auth = authData{
|
||||
UserName: authorizedUser.(string),
|
||||
IsAdmin: isAdmin.(bool),
|
||||
AuthHash: authHash.(string),
|
||||
}
|
||||
|
||||
// Validate Auth Hash
|
||||
correctAuthHash, err := api.getUserAuthHash(auth.UserName)
|
||||
if err != nil || correctAuthHash != auth.AuthHash {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh
|
||||
expiresAt := session.Get("expiresAt")
|
||||
if expiresAt != nil && expiresAt.(int64)-time.Now().Unix() < 60*60*24 {
|
||||
log.Info("[getSession] Refreshing Session")
|
||||
setSession(session, authorizedUser.(string))
|
||||
if expiresAt.(int64)-time.Now().Unix() < 60*60*24 {
|
||||
log.Info("Refreshing Session")
|
||||
if err := api.setSession(session, auth); err != nil {
|
||||
log.Error("unable to get session")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return authorizedUser.(string), true
|
||||
// Authorized
|
||||
return auth, true
|
||||
}
|
||||
|
||||
func setSession(session sessions.Session, user string) error {
|
||||
func (api *API) setSession(session sessions.Session, auth authData) error {
|
||||
// Set Session Cookie
|
||||
session.Set("authorizedUser", user)
|
||||
session.Set("authorizedUser", auth.UserName)
|
||||
session.Set("isAdmin", auth.IsAdmin)
|
||||
session.Set("expiresAt", time.Now().Unix()+(60*60*24*7))
|
||||
session.Set("authHash", auth.AuthHash)
|
||||
|
||||
return session.Save()
|
||||
}
|
||||
|
||||
func (api *API) getUserAuthHash(username string) (string, error) {
|
||||
// Return Cache
|
||||
if api.userAuthCache[username] != "" {
|
||||
return api.userAuthCache[username], nil
|
||||
}
|
||||
|
||||
// Get DB
|
||||
user, err := api.db.Queries.GetUser(api.db.Ctx, username)
|
||||
if err != nil {
|
||||
log.Error("GetUser DB Error:", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Update Cache
|
||||
api.userAuthCache[username] = *user.AuthHash
|
||||
|
||||
return api.userAuthCache[username], nil
|
||||
}
|
||||
|
||||
func (api *API) rotateAllAuthHashes() error {
|
||||
// Do Transaction
|
||||
tx, err := api.db.DB.Begin()
|
||||
if err != nil {
|
||||
log.Error("Transaction Begin DB Error: ", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Defer & Start Transaction
|
||||
defer func() {
|
||||
if err := tx.Rollback(); err != nil {
|
||||
log.Error("DB Rollback Error:", err)
|
||||
}
|
||||
}()
|
||||
qtx := api.db.Queries.WithTx(tx)
|
||||
|
||||
users, err := qtx.GetUsers(api.db.Ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update Users
|
||||
newAuthHashCache := make(map[string]string, 0)
|
||||
for _, user := range users {
|
||||
// Generate Auth Hash
|
||||
rawAuthHash, err := utils.GenerateToken(64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update User
|
||||
authHash := fmt.Sprintf("%x", rawAuthHash)
|
||||
if _, err = qtx.UpdateUser(api.db.Ctx, database.UpdateUserParams{
|
||||
UserID: user.ID,
|
||||
AuthHash: &authHash,
|
||||
Admin: user.Admin,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save New Hash Cache
|
||||
newAuthHashCache[user.ID] = fmt.Sprintf("%x", rawAuthHash)
|
||||
}
|
||||
|
||||
// Commit Transaction
|
||||
if err := tx.Commit(); err != nil {
|
||||
log.Error("Transaction Commit DB Error: ", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Transaction Succeeded -> Update Cache
|
||||
for user, hash := range newAuthHashCache {
|
||||
api.userAuthCache[user] = hash
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
151
api/common.go
Normal file
151
api/common.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"reichard.io/antholume/database"
|
||||
"reichard.io/antholume/metadata"
|
||||
)
|
||||
|
||||
func (api *API) createDownloadDocumentHandler(errorFunc func(*gin.Context, int, string)) func(*gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
var rDoc requestDocumentID
|
||||
if err := c.ShouldBindUri(&rDoc); err != nil {
|
||||
log.Error("Invalid URI Bind")
|
||||
errorFunc(c, http.StatusBadRequest, "Invalid Request")
|
||||
return
|
||||
}
|
||||
|
||||
// Get Document
|
||||
document, err := api.db.Queries.GetDocument(api.db.Ctx, rDoc.DocumentID)
|
||||
if err != nil {
|
||||
log.Error("GetDocument DB Error:", err)
|
||||
errorFunc(c, http.StatusBadRequest, "Unknown Document")
|
||||
return
|
||||
}
|
||||
|
||||
if document.Filepath == nil {
|
||||
log.Error("Document Doesn't Have File:", rDoc.DocumentID)
|
||||
errorFunc(c, http.StatusBadRequest, "Document Doesn't Exist")
|
||||
return
|
||||
}
|
||||
|
||||
// Derive Basepath
|
||||
basepath := filepath.Join(api.cfg.DataPath, "documents")
|
||||
if document.Basepath != nil && *document.Basepath != "" {
|
||||
basepath = *document.Basepath
|
||||
}
|
||||
|
||||
// Derive Storage Location
|
||||
filePath := filepath.Join(basepath, *document.Filepath)
|
||||
|
||||
// Validate File Exists
|
||||
_, err = os.Stat(filePath)
|
||||
if os.IsNotExist(err) {
|
||||
log.Error("File should but doesn't exist: ", err)
|
||||
errorFunc(c, http.StatusBadRequest, "Document Doesn't Exist")
|
||||
return
|
||||
}
|
||||
|
||||
// Force Download
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filepath.Base(*document.Filepath)))
|
||||
c.File(filePath)
|
||||
}
|
||||
}
|
||||
|
||||
func (api *API) createGetCoverHandler(errorFunc func(*gin.Context, int, string)) func(*gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
var rDoc requestDocumentID
|
||||
if err := c.ShouldBindUri(&rDoc); err != nil {
|
||||
log.Error("Invalid URI Bind")
|
||||
errorFunc(c, http.StatusNotFound, "Invalid cover.")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate Document Exists in DB
|
||||
document, err := api.db.Queries.GetDocument(api.db.Ctx, rDoc.DocumentID)
|
||||
if err != nil {
|
||||
log.Error("GetDocument DB Error:", err)
|
||||
errorFunc(c, http.StatusInternalServerError, fmt.Sprintf("GetDocument DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Handle Identified Document
|
||||
if document.Coverfile != nil {
|
||||
if *document.Coverfile == "UNKNOWN" {
|
||||
c.FileFromFS("assets/images/no-cover.jpg", http.FS(api.assets))
|
||||
return
|
||||
}
|
||||
|
||||
// Derive Path
|
||||
safePath := filepath.Join(api.cfg.DataPath, "covers", *document.Coverfile)
|
||||
|
||||
// Validate File Exists
|
||||
_, err = os.Stat(safePath)
|
||||
if err != nil {
|
||||
log.Error("File should but doesn't exist: ", err)
|
||||
c.FileFromFS("assets/images/no-cover.jpg", http.FS(api.assets))
|
||||
return
|
||||
}
|
||||
|
||||
c.File(safePath)
|
||||
return
|
||||
}
|
||||
|
||||
// Attempt Metadata
|
||||
var coverDir string = filepath.Join(api.cfg.DataPath, "covers")
|
||||
var coverFile string = "UNKNOWN"
|
||||
|
||||
// Identify Documents & Save Covers
|
||||
metadataResults, err := metadata.SearchMetadata(metadata.SOURCE_GBOOK, metadata.MetadataInfo{
|
||||
Title: document.Title,
|
||||
Author: document.Author,
|
||||
})
|
||||
|
||||
if err == nil && len(metadataResults) > 0 && metadataResults[0].ID != nil {
|
||||
firstResult := metadataResults[0]
|
||||
|
||||
// Save Cover
|
||||
fileName, err := metadata.CacheCover(*firstResult.ID, coverDir, document.ID, false)
|
||||
if err == nil {
|
||||
coverFile = *fileName
|
||||
}
|
||||
|
||||
// Store First Metadata Result
|
||||
if _, err = api.db.Queries.AddMetadata(api.db.Ctx, database.AddMetadataParams{
|
||||
DocumentID: document.ID,
|
||||
Title: firstResult.Title,
|
||||
Author: firstResult.Author,
|
||||
Description: firstResult.Description,
|
||||
Gbid: firstResult.ID,
|
||||
Olid: nil,
|
||||
Isbn10: firstResult.ISBN10,
|
||||
Isbn13: firstResult.ISBN13,
|
||||
}); err != nil {
|
||||
log.Error("AddMetadata DB Error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Upsert Document
|
||||
if _, err = api.db.Queries.UpsertDocument(api.db.Ctx, database.UpsertDocumentParams{
|
||||
ID: document.ID,
|
||||
Coverfile: &coverFile,
|
||||
}); err != nil {
|
||||
log.Warn("UpsertDocument DB Error:", err)
|
||||
}
|
||||
|
||||
// Return Unknown Cover
|
||||
if coverFile == "UNKNOWN" {
|
||||
c.FileFromFS("assets/images/no-cover.jpg", http.FS(api.assets))
|
||||
return
|
||||
}
|
||||
|
||||
coverFilePath := filepath.Join(coverDir, coverFile)
|
||||
c.File(coverFilePath)
|
||||
}
|
||||
}
|
||||
451
api/ko-routes.go
451
api/ko-routes.go
@@ -10,15 +10,12 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
argon2 "github.com/alexedwards/argon2id"
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/exp/slices"
|
||||
"reichard.io/bbank/database"
|
||||
"reichard.io/antholume/database"
|
||||
"reichard.io/antholume/metadata"
|
||||
)
|
||||
|
||||
type activityItem struct {
|
||||
@@ -74,97 +71,53 @@ type requestDocumentID struct {
|
||||
DocumentID string `uri:"document" binding:"required"`
|
||||
}
|
||||
|
||||
func (api *API) authorizeUser(c *gin.Context) {
|
||||
func (api *API) koAuthorizeUser(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"authorized": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) createUser(c *gin.Context) {
|
||||
if !api.Config.RegistrationEnabled {
|
||||
c.AbortWithStatus(http.StatusConflict)
|
||||
return
|
||||
func (api *API) koSetProgress(c *gin.Context) {
|
||||
var auth authData
|
||||
if data, _ := c.Get("Authorization"); data != nil {
|
||||
auth = data.(authData)
|
||||
}
|
||||
|
||||
var rUser requestUser
|
||||
if err := c.ShouldBindJSON(&rUser); err != nil {
|
||||
log.Error("[createUser] Invalid JSON Bind")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid User Data"})
|
||||
return
|
||||
}
|
||||
|
||||
if rUser.Username == "" || rUser.Password == "" {
|
||||
log.Error("[createUser] Invalid User - Empty Username or Password")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid User Data"})
|
||||
return
|
||||
}
|
||||
|
||||
hashedPassword, err := argon2.CreateHash(rUser.Password, argon2.DefaultParams)
|
||||
if err != nil {
|
||||
log.Error("[createUser] Argon2 Hash Failure:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Error"})
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := api.DB.Queries.CreateUser(api.DB.Ctx, database.CreateUserParams{
|
||||
ID: rUser.Username,
|
||||
Pass: &hashedPassword,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("[createUser] CreateUser DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid User Data"})
|
||||
return
|
||||
}
|
||||
|
||||
// User Exists
|
||||
if rows == 0 {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "User Already Exists"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"username": rUser.Username,
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) setProgress(c *gin.Context) {
|
||||
rUser, _ := c.Get("AuthorizedUser")
|
||||
|
||||
var rPosition requestPosition
|
||||
if err := c.ShouldBindJSON(&rPosition); err != nil {
|
||||
log.Error("[setProgress] Invalid JSON Bind")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Progress Data"})
|
||||
log.Error("Invalid JSON Bind")
|
||||
apiErrorPage(c, http.StatusBadRequest, "Invalid Progress Data")
|
||||
return
|
||||
}
|
||||
|
||||
// Upsert Device
|
||||
if _, err := api.DB.Queries.UpsertDevice(api.DB.Ctx, database.UpsertDeviceParams{
|
||||
if _, err := api.db.Queries.UpsertDevice(api.db.Ctx, database.UpsertDeviceParams{
|
||||
ID: rPosition.DeviceID,
|
||||
UserID: rUser.(string),
|
||||
UserID: auth.UserName,
|
||||
DeviceName: rPosition.Device,
|
||||
LastSynced: time.Now().UTC().Format(time.RFC3339),
|
||||
}); err != nil {
|
||||
log.Error("[setProgress] UpsertDevice DB Error:", err)
|
||||
log.Error("UpsertDevice DB Error:", err)
|
||||
}
|
||||
|
||||
// Upsert Document
|
||||
if _, err := api.DB.Queries.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{
|
||||
if _, err := api.db.Queries.UpsertDocument(api.db.Ctx, database.UpsertDocumentParams{
|
||||
ID: rPosition.DocumentID,
|
||||
}); err != nil {
|
||||
log.Error("[setProgress] UpsertDocument DB Error:", err)
|
||||
log.Error("UpsertDocument DB Error:", err)
|
||||
}
|
||||
|
||||
// Create or Replace Progress
|
||||
progress, err := api.DB.Queries.UpdateProgress(api.DB.Ctx, database.UpdateProgressParams{
|
||||
progress, err := api.db.Queries.UpdateProgress(api.db.Ctx, database.UpdateProgressParams{
|
||||
Percentage: rPosition.Percentage,
|
||||
DocumentID: rPosition.DocumentID,
|
||||
DeviceID: rPosition.DeviceID,
|
||||
UserID: rUser.(string),
|
||||
UserID: auth.UserName,
|
||||
Progress: rPosition.Progress,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("[setProgress] UpdateProgress DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
log.Error("UpdateProgress DB Error:", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Invalid Request")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -174,19 +127,22 @@ func (api *API) setProgress(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) getProgress(c *gin.Context) {
|
||||
rUser, _ := c.Get("AuthorizedUser")
|
||||
func (api *API) koGetProgress(c *gin.Context) {
|
||||
var auth authData
|
||||
if data, _ := c.Get("Authorization"); data != nil {
|
||||
auth = data.(authData)
|
||||
}
|
||||
|
||||
var rDocID requestDocumentID
|
||||
if err := c.ShouldBindUri(&rDocID); err != nil {
|
||||
log.Error("[getProgress] Invalid URI Bind")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
log.Error("Invalid URI Bind")
|
||||
apiErrorPage(c, http.StatusBadRequest, "Invalid Request")
|
||||
return
|
||||
}
|
||||
|
||||
progress, err := api.DB.Queries.GetProgress(api.DB.Ctx, database.GetProgressParams{
|
||||
progress, err := api.db.Queries.GetDocumentProgress(api.db.Ctx, database.GetDocumentProgressParams{
|
||||
DocumentID: rDocID.DocumentID,
|
||||
UserID: rUser.(string),
|
||||
UserID: auth.UserName,
|
||||
})
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
@@ -194,8 +150,8 @@ func (api *API) getProgress(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{})
|
||||
return
|
||||
} else if err != nil {
|
||||
log.Error("[getProgress] GetProgress DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Document"})
|
||||
log.Error("GetDocumentProgress DB Error:", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Invalid Document")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -208,21 +164,24 @@ func (api *API) getProgress(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) addActivities(c *gin.Context) {
|
||||
rUser, _ := c.Get("AuthorizedUser")
|
||||
func (api *API) koAddActivities(c *gin.Context) {
|
||||
var auth authData
|
||||
if data, _ := c.Get("Authorization"); data != nil {
|
||||
auth = data.(authData)
|
||||
}
|
||||
|
||||
var rActivity requestActivity
|
||||
if err := c.ShouldBindJSON(&rActivity); err != nil {
|
||||
log.Error("[addActivity] Invalid JSON Bind")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Activity"})
|
||||
log.Error("Invalid JSON Bind")
|
||||
apiErrorPage(c, http.StatusBadRequest, "Invalid Activity")
|
||||
return
|
||||
}
|
||||
|
||||
// Do Transaction
|
||||
tx, err := api.DB.DB.Begin()
|
||||
tx, err := api.db.DB.Begin()
|
||||
if err != nil {
|
||||
log.Error("[addActivities] Transaction Begin DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Error"})
|
||||
log.Error("Transaction Begin DB Error:", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Unknown Error")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -234,109 +193,108 @@ func (api *API) addActivities(c *gin.Context) {
|
||||
allDocuments := getKeys(allDocumentsMap)
|
||||
|
||||
// Defer & Start Transaction
|
||||
defer tx.Rollback()
|
||||
qtx := api.DB.Queries.WithTx(tx)
|
||||
defer func() {
|
||||
if err := tx.Rollback(); err != nil {
|
||||
log.Error("DB Rollback Error:", err)
|
||||
}
|
||||
}()
|
||||
qtx := api.db.Queries.WithTx(tx)
|
||||
|
||||
// Upsert Documents
|
||||
for _, doc := range allDocuments {
|
||||
if _, err := qtx.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{
|
||||
if _, err := qtx.UpsertDocument(api.db.Ctx, database.UpsertDocumentParams{
|
||||
ID: doc,
|
||||
}); err != nil {
|
||||
log.Error("[addActivities] UpsertDocument DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Document"})
|
||||
log.Error("UpsertDocument DB Error:", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Invalid Document")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Upsert Device
|
||||
if _, err = qtx.UpsertDevice(api.DB.Ctx, database.UpsertDeviceParams{
|
||||
if _, err = qtx.UpsertDevice(api.db.Ctx, database.UpsertDeviceParams{
|
||||
ID: rActivity.DeviceID,
|
||||
UserID: rUser.(string),
|
||||
UserID: auth.UserName,
|
||||
DeviceName: rActivity.Device,
|
||||
LastSynced: time.Now().UTC().Format(time.RFC3339),
|
||||
}); err != nil {
|
||||
log.Error("[addActivities] UpsertDevice DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Device"})
|
||||
log.Error("UpsertDevice DB Error:", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Invalid Device")
|
||||
return
|
||||
}
|
||||
|
||||
// Add All Activity
|
||||
for _, item := range rActivity.Activity {
|
||||
if _, err := qtx.AddActivity(api.DB.Ctx, database.AddActivityParams{
|
||||
UserID: rUser.(string),
|
||||
DocumentID: item.DocumentID,
|
||||
DeviceID: rActivity.DeviceID,
|
||||
StartTime: time.Unix(int64(item.StartTime), 0).UTC().Format(time.RFC3339),
|
||||
Duration: int64(item.Duration),
|
||||
Page: int64(item.Page),
|
||||
Pages: int64(item.Pages),
|
||||
if _, err := qtx.AddActivity(api.db.Ctx, database.AddActivityParams{
|
||||
UserID: auth.UserName,
|
||||
DocumentID: item.DocumentID,
|
||||
DeviceID: rActivity.DeviceID,
|
||||
StartTime: time.Unix(int64(item.StartTime), 0).UTC().Format(time.RFC3339),
|
||||
Duration: int64(item.Duration),
|
||||
StartPercentage: float64(item.Page) / float64(item.Pages),
|
||||
EndPercentage: float64(item.Page+1) / float64(item.Pages),
|
||||
}); err != nil {
|
||||
log.Error("[addActivities] AddActivity DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Activity"})
|
||||
log.Error("AddActivity DB Error:", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Invalid Activity")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Commit Transaction
|
||||
if err := tx.Commit(); err != nil {
|
||||
log.Error("[addActivities] Transaction Commit DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Error"})
|
||||
log.Error("Transaction Commit DB Error:", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Unknown Error")
|
||||
return
|
||||
}
|
||||
|
||||
// Update Temp Tables
|
||||
go func() {
|
||||
log.Info("[addActivities] Caching Temp Tables")
|
||||
if err := api.DB.CacheTempTables(); err != nil {
|
||||
log.Warn("[addActivities] CacheTempTables Failure: ", err)
|
||||
}
|
||||
}()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"added": len(rActivity.Activity),
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) checkActivitySync(c *gin.Context) {
|
||||
rUser, _ := c.Get("AuthorizedUser")
|
||||
func (api *API) koCheckActivitySync(c *gin.Context) {
|
||||
var auth authData
|
||||
if data, _ := c.Get("Authorization"); data != nil {
|
||||
auth = data.(authData)
|
||||
}
|
||||
|
||||
var rCheckActivity requestCheckActivitySync
|
||||
if err := c.ShouldBindJSON(&rCheckActivity); err != nil {
|
||||
log.Error("[checkActivitySync] Invalid JSON Bind")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
log.Error("Invalid JSON Bind")
|
||||
apiErrorPage(c, http.StatusBadRequest, "Invalid Request")
|
||||
return
|
||||
}
|
||||
|
||||
// Upsert Device
|
||||
if _, err := api.DB.Queries.UpsertDevice(api.DB.Ctx, database.UpsertDeviceParams{
|
||||
if _, err := api.db.Queries.UpsertDevice(api.db.Ctx, database.UpsertDeviceParams{
|
||||
ID: rCheckActivity.DeviceID,
|
||||
UserID: rUser.(string),
|
||||
UserID: auth.UserName,
|
||||
DeviceName: rCheckActivity.Device,
|
||||
LastSynced: time.Now().UTC().Format(time.RFC3339),
|
||||
}); err != nil {
|
||||
log.Error("[checkActivitySync] UpsertDevice DB Error", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Device"})
|
||||
log.Error("UpsertDevice DB Error", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Invalid Device")
|
||||
return
|
||||
}
|
||||
|
||||
// Get Last Device Activity
|
||||
lastActivity, err := api.DB.Queries.GetLastActivity(api.DB.Ctx, database.GetLastActivityParams{
|
||||
UserID: rUser.(string),
|
||||
lastActivity, err := api.db.Queries.GetLastActivity(api.db.Ctx, database.GetLastActivityParams{
|
||||
UserID: auth.UserName,
|
||||
DeviceID: rCheckActivity.DeviceID,
|
||||
})
|
||||
if err == sql.ErrNoRows {
|
||||
lastActivity = time.UnixMilli(0).Format(time.RFC3339)
|
||||
} else if err != nil {
|
||||
log.Error("[checkActivitySync] GetLastActivity DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Error"})
|
||||
log.Error("GetLastActivity DB Error:", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Unknown Error")
|
||||
return
|
||||
}
|
||||
|
||||
// Parse Time
|
||||
parsedTime, err := time.Parse(time.RFC3339, lastActivity)
|
||||
if err != nil {
|
||||
log.Error("[checkActivitySync] Time Parse Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Error"})
|
||||
log.Error("Time Parse Error:", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Unknown Error")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -345,29 +303,33 @@ func (api *API) checkActivitySync(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) addDocuments(c *gin.Context) {
|
||||
func (api *API) koAddDocuments(c *gin.Context) {
|
||||
var rNewDocs requestDocument
|
||||
if err := c.ShouldBindJSON(&rNewDocs); err != nil {
|
||||
log.Error("[addDocuments] Invalid JSON Bind")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Document(s)"})
|
||||
log.Error("Invalid JSON Bind")
|
||||
apiErrorPage(c, http.StatusBadRequest, "Invalid Document(s)")
|
||||
return
|
||||
}
|
||||
|
||||
// Do Transaction
|
||||
tx, err := api.DB.DB.Begin()
|
||||
tx, err := api.db.DB.Begin()
|
||||
if err != nil {
|
||||
log.Error("[addDocuments] Transaction Begin DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Error"})
|
||||
log.Error("Transaction Begin DB Error:", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Unknown Error")
|
||||
return
|
||||
}
|
||||
|
||||
// Defer & Start Transaction
|
||||
defer tx.Rollback()
|
||||
qtx := api.DB.Queries.WithTx(tx)
|
||||
defer func() {
|
||||
if err := tx.Rollback(); err != nil {
|
||||
log.Error("DB Rollback Error:", err)
|
||||
}
|
||||
}()
|
||||
qtx := api.db.Queries.WithTx(tx)
|
||||
|
||||
// Upsert Documents
|
||||
for _, doc := range rNewDocs.Documents {
|
||||
doc, err := qtx.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{
|
||||
_, err := qtx.UpsertDocument(api.db.Ctx, database.UpsertDocumentParams{
|
||||
ID: doc.ID,
|
||||
Title: api.sanitizeInput(doc.Title),
|
||||
Author: api.sanitizeInput(doc.Author),
|
||||
@@ -377,26 +339,16 @@ func (api *API) addDocuments(c *gin.Context) {
|
||||
Description: api.sanitizeInput(doc.Description),
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("[addDocuments] UpsertDocument DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Document"})
|
||||
log.Error("UpsertDocument DB Error:", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Invalid Document")
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = qtx.UpdateDocumentSync(api.DB.Ctx, database.UpdateDocumentSyncParams{
|
||||
ID: doc.ID,
|
||||
Synced: true,
|
||||
}); err != nil {
|
||||
log.Error("[addDocuments] UpdateDocumentSync DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Document"})
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Commit Transaction
|
||||
if err := tx.Commit(); err != nil {
|
||||
log.Error("[addDocuments] Transaction Commit DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Error"})
|
||||
log.Error("Transaction Commit DB Error:", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Unknown Error")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -405,62 +357,60 @@ func (api *API) addDocuments(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) checkDocumentsSync(c *gin.Context) {
|
||||
rUser, _ := c.Get("AuthorizedUser")
|
||||
func (api *API) koCheckDocumentsSync(c *gin.Context) {
|
||||
var auth authData
|
||||
if data, _ := c.Get("Authorization"); data != nil {
|
||||
auth = data.(authData)
|
||||
}
|
||||
|
||||
var rCheckDocs requestCheckDocumentSync
|
||||
if err := c.ShouldBindJSON(&rCheckDocs); err != nil {
|
||||
log.Error("[checkDocumentsSync] Invalid JSON Bind")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
log.Error("Invalid JSON Bind")
|
||||
apiErrorPage(c, http.StatusBadRequest, "Invalid Request")
|
||||
return
|
||||
}
|
||||
|
||||
// Upsert Device
|
||||
device, err := api.DB.Queries.UpsertDevice(api.DB.Ctx, database.UpsertDeviceParams{
|
||||
_, err := api.db.Queries.UpsertDevice(api.db.Ctx, database.UpsertDeviceParams{
|
||||
ID: rCheckDocs.DeviceID,
|
||||
UserID: rUser.(string),
|
||||
UserID: auth.UserName,
|
||||
DeviceName: rCheckDocs.Device,
|
||||
LastSynced: time.Now().UTC().Format(time.RFC3339),
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("[checkDocumentsSync] UpsertDevice DB Error", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Device"})
|
||||
log.Error("UpsertDevice DB Error", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Invalid Device")
|
||||
return
|
||||
}
|
||||
|
||||
missingDocs := []database.Document{}
|
||||
deletedDocIDs := []string{}
|
||||
// Get Missing Documents
|
||||
missingDocs, err := api.db.Queries.GetMissingDocuments(api.db.Ctx, rCheckDocs.Have)
|
||||
if err != nil {
|
||||
log.Error("GetMissingDocuments DB Error", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Invalid Request")
|
||||
return
|
||||
}
|
||||
|
||||
if device.Sync == true {
|
||||
// Get Missing Documents
|
||||
missingDocs, err = api.DB.Queries.GetMissingDocuments(api.DB.Ctx, rCheckDocs.Have)
|
||||
if err != nil {
|
||||
log.Error("[checkDocumentsSync] GetMissingDocuments DB Error", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get Deleted Documents
|
||||
deletedDocIDs, err = api.DB.Queries.GetDeletedDocuments(api.DB.Ctx, rCheckDocs.Have)
|
||||
if err != nil {
|
||||
log.Error("[checkDocumentsSync] GetDeletedDocuments DB Error", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
// Get Deleted Documents
|
||||
deletedDocIDs, err := api.db.Queries.GetDeletedDocuments(api.db.Ctx, rCheckDocs.Have)
|
||||
if err != nil {
|
||||
log.Error("GetDeletedDocuments DB Error", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Invalid Request")
|
||||
return
|
||||
}
|
||||
|
||||
// Get Wanted Documents
|
||||
jsonHaves, err := json.Marshal(rCheckDocs.Have)
|
||||
if err != nil {
|
||||
log.Error("[checkDocumentsSync] JSON Marshal Error", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
log.Error("JSON Marshal Error", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Invalid Request")
|
||||
return
|
||||
}
|
||||
|
||||
wantedDocs, err := api.DB.Queries.GetWantedDocuments(api.DB.Ctx, string(jsonHaves))
|
||||
wantedDocs, err := api.db.Queries.GetWantedDocuments(api.db.Ctx, string(jsonHaves))
|
||||
if err != nil {
|
||||
log.Error("[checkDocumentsSync] GetWantedDocuments DB Error", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
log.Error("GetWantedDocuments DB Error", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Invalid Request")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -500,100 +450,87 @@ func (api *API) checkDocumentsSync(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, rCheckDocSync)
|
||||
}
|
||||
|
||||
func (api *API) uploadExistingDocument(c *gin.Context) {
|
||||
func (api *API) koUploadExistingDocument(c *gin.Context) {
|
||||
var rDoc requestDocumentID
|
||||
if err := c.ShouldBindUri(&rDoc); err != nil {
|
||||
log.Error("[uploadExistingDocument] Invalid URI Bind")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
log.Error("Invalid URI Bind")
|
||||
apiErrorPage(c, http.StatusBadRequest, "Invalid Request")
|
||||
return
|
||||
}
|
||||
|
||||
// Open Form File
|
||||
fileData, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
log.Error("[uploadExistingDocument] File Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "File Error"})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate Type & Derive Extension on MIME
|
||||
uploadedFile, err := fileData.Open()
|
||||
fileMime, err := mimetype.DetectReader(uploadedFile)
|
||||
fileExtension := fileMime.Extension()
|
||||
|
||||
if !slices.Contains([]string{".epub", ".html"}, fileExtension) {
|
||||
log.Error("[uploadExistingDocument] Invalid FileType:", fileExtension)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Filetype"})
|
||||
log.Error("File Error:", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "File error")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate Document Exists in DB
|
||||
document, err := api.DB.Queries.GetDocument(api.DB.Ctx, rDoc.DocumentID)
|
||||
document, err := api.db.Queries.GetDocument(api.db.Ctx, rDoc.DocumentID)
|
||||
if err != nil {
|
||||
log.Error("[uploadExistingDocument] GetDocument DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Document"})
|
||||
log.Error("GetDocument DB Error:", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Unknown Document")
|
||||
return
|
||||
}
|
||||
|
||||
// Open File
|
||||
uploadedFile, err := fileData.Open()
|
||||
if err != nil {
|
||||
log.Error("Unable to open file")
|
||||
apiErrorPage(c, http.StatusBadRequest, "Unable to open file")
|
||||
return
|
||||
}
|
||||
|
||||
// Check Support
|
||||
docType, err := metadata.GetDocumentTypeReader(uploadedFile)
|
||||
if err != nil {
|
||||
log.Error("Unsupported file")
|
||||
apiErrorPage(c, http.StatusBadRequest, "Unsupported file")
|
||||
return
|
||||
}
|
||||
|
||||
// Derive Filename
|
||||
var fileName string
|
||||
if document.Author != nil {
|
||||
fileName = fileName + *document.Author
|
||||
} else {
|
||||
fileName = fileName + "Unknown"
|
||||
}
|
||||
|
||||
if document.Title != nil {
|
||||
fileName = fileName + " - " + *document.Title
|
||||
} else {
|
||||
fileName = fileName + " - Unknown"
|
||||
}
|
||||
|
||||
// Remove Slashes
|
||||
fileName = strings.ReplaceAll(fileName, "/", "")
|
||||
|
||||
// Derive & Sanitize File Name
|
||||
fileName = "." + filepath.Clean(fmt.Sprintf("/%s [%s]%s", fileName, document.ID, fileExtension))
|
||||
fileName := deriveBaseFileName(&metadata.MetadataInfo{
|
||||
Type: *docType,
|
||||
PartialMD5: &document.ID,
|
||||
Title: document.Title,
|
||||
Author: document.Author,
|
||||
})
|
||||
|
||||
// Generate Storage Path
|
||||
safePath := filepath.Join(api.Config.DataPath, "documents", fileName)
|
||||
basePath := filepath.Join(api.cfg.DataPath, "documents")
|
||||
safePath := filepath.Join(basePath, fileName)
|
||||
|
||||
// Save & Prevent Overwrites
|
||||
_, err = os.Stat(safePath)
|
||||
if os.IsNotExist(err) {
|
||||
err = c.SaveUploadedFile(fileData, safePath)
|
||||
if err != nil {
|
||||
log.Error("[uploadExistingDocument] Save Failure:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "File Error"})
|
||||
log.Error("Save Failure:", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "File Error")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Get MD5 Hash
|
||||
fileHash, err := getFileMD5(safePath)
|
||||
// Acquire Metadata
|
||||
metadataInfo, err := metadata.GetMetadata(safePath)
|
||||
if err != nil {
|
||||
log.Error("[uploadExistingDocument] Hash Failure:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "File Error"})
|
||||
log.Errorf("Unable to acquire metadata: %v", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Unable to acquire metadata")
|
||||
return
|
||||
}
|
||||
|
||||
// Upsert Document
|
||||
if _, err = api.DB.Queries.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{
|
||||
if _, err = api.db.Queries.UpsertDocument(api.db.Ctx, database.UpsertDocumentParams{
|
||||
ID: document.ID,
|
||||
Md5: fileHash,
|
||||
Md5: metadataInfo.MD5,
|
||||
Words: metadataInfo.WordCount,
|
||||
Filepath: &fileName,
|
||||
Basepath: &basePath,
|
||||
}); err != nil {
|
||||
log.Error("[uploadExistingDocument] UpsertDocument DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Document Error"})
|
||||
return
|
||||
}
|
||||
|
||||
// Update Document Sync Attribute
|
||||
if _, err = api.DB.Queries.UpdateDocumentSync(api.DB.Ctx, database.UpdateDocumentSyncParams{
|
||||
ID: document.ID,
|
||||
Synced: true,
|
||||
}); err != nil {
|
||||
log.Error("[uploadExistingDocument] UpdateDocumentSync DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Document"})
|
||||
log.Error("UpsertDocument DB Error:", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Document Error")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -602,54 +539,24 @@ func (api *API) uploadExistingDocument(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) downloadDocument(c *gin.Context) {
|
||||
var rDoc requestDocumentID
|
||||
if err := c.ShouldBindUri(&rDoc); err != nil {
|
||||
log.Error("[downloadDocument] Invalid URI Bind")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
func (api *API) koDemoModeJSONError(c *gin.Context) {
|
||||
apiErrorPage(c, http.StatusUnauthorized, "Not Allowed in Demo Mode")
|
||||
}
|
||||
|
||||
// Get Document
|
||||
document, err := api.DB.Queries.GetDocument(api.DB.Ctx, rDoc.DocumentID)
|
||||
if err != nil {
|
||||
log.Error("[downloadDocument] GetDocument DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Document"})
|
||||
return
|
||||
}
|
||||
|
||||
if document.Filepath == nil {
|
||||
log.Error("[downloadDocument] Document Doesn't Have File:", rDoc.DocumentID)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Document Doesn't Exist"})
|
||||
return
|
||||
}
|
||||
|
||||
// Derive Storage Location
|
||||
filePath := filepath.Join(api.Config.DataPath, "documents", *document.Filepath)
|
||||
|
||||
// Validate File Exists
|
||||
_, err = os.Stat(filePath)
|
||||
if os.IsNotExist(err) {
|
||||
log.Error("[downloadDocument] File Doesn't Exist:", rDoc.DocumentID)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Document Doesn't Exists"})
|
||||
return
|
||||
}
|
||||
|
||||
// Force Download (Security)
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filepath.Base(*document.Filepath)))
|
||||
c.File(filePath)
|
||||
func apiErrorPage(c *gin.Context, errorCode int, errorMessage string) {
|
||||
c.AbortWithStatusJSON(errorCode, gin.H{"error": errorMessage})
|
||||
}
|
||||
|
||||
func (api *API) sanitizeInput(val any) *string {
|
||||
switch v := val.(type) {
|
||||
case *string:
|
||||
if v != nil {
|
||||
newString := html.UnescapeString(api.HTMLPolicy.Sanitize(string(*v)))
|
||||
newString := html.UnescapeString(htmlPolicy.Sanitize(string(*v)))
|
||||
return &newString
|
||||
}
|
||||
case string:
|
||||
if v != "" {
|
||||
newString := html.UnescapeString(api.HTMLPolicy.Sanitize(string(v)))
|
||||
newString := html.UnescapeString(htmlPolicy.Sanitize(string(v)))
|
||||
return &newString
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"reichard.io/bbank/database"
|
||||
"reichard.io/bbank/opds"
|
||||
"reichard.io/antholume/database"
|
||||
"reichard.io/antholume/opds"
|
||||
)
|
||||
|
||||
var mimeMapping map[string]string = map[string]string{
|
||||
@@ -26,23 +26,65 @@ var mimeMapping map[string]string = map[string]string{
|
||||
"lit": "application/x-ms-reader",
|
||||
}
|
||||
|
||||
func (api *API) opdsDocuments(c *gin.Context) {
|
||||
var userID string
|
||||
if rUser, _ := c.Get("AuthorizedUser"); rUser != nil {
|
||||
userID = rUser.(string)
|
||||
func (api *API) opdsEntry(c *gin.Context) {
|
||||
// Build & Return XML
|
||||
mainFeed := &opds.Feed{
|
||||
Title: "AnthoLume OPDS Server",
|
||||
Updated: time.Now().UTC(),
|
||||
Links: []opds.Link{
|
||||
{
|
||||
Title: "Search AnthoLume",
|
||||
Rel: "search",
|
||||
TypeLink: "application/opensearchdescription+xml",
|
||||
Href: "/api/opds/search.xml",
|
||||
},
|
||||
},
|
||||
|
||||
Entries: []opds.Entry{
|
||||
{
|
||||
Title: "AnthoLume - All Documents",
|
||||
Content: &opds.Content{
|
||||
Content: "AnthoLume - All Documents",
|
||||
ContentType: "text",
|
||||
},
|
||||
Links: []opds.Link{
|
||||
{
|
||||
Href: "/api/opds/documents",
|
||||
TypeLink: "application/atom+xml;type=feed;profile=opds-catalog",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Potential URL Parameters
|
||||
qParams := bindQueryParams(c)
|
||||
c.XML(http.StatusOK, mainFeed)
|
||||
}
|
||||
|
||||
func (api *API) opdsDocuments(c *gin.Context) {
|
||||
var auth authData
|
||||
if data, _ := c.Get("Authorization"); data != nil {
|
||||
auth = data.(authData)
|
||||
}
|
||||
|
||||
// Potential URL Parameters (Default Pagination - 100)
|
||||
qParams := bindQueryParams(c, 100)
|
||||
|
||||
// Possible Query
|
||||
var query *string
|
||||
if qParams.Search != nil && *qParams.Search != "" {
|
||||
search := "%" + *qParams.Search + "%"
|
||||
query = &search
|
||||
}
|
||||
|
||||
// Get Documents
|
||||
documents, err := api.DB.Queries.GetDocumentsWithStats(api.DB.Ctx, database.GetDocumentsWithStatsParams{
|
||||
UserID: userID,
|
||||
documents, err := api.db.Queries.GetDocumentsWithStats(api.db.Ctx, database.GetDocumentsWithStatsParams{
|
||||
UserID: auth.UserName,
|
||||
Query: query,
|
||||
Offset: (*qParams.Page - 1) * *qParams.Limit,
|
||||
Limit: *qParams.Limit,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("[opdsDocuments] GetDocumentsWithStats DB Error:", err)
|
||||
log.Error("GetDocumentsWithStats DB Error:", err)
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@@ -55,26 +97,41 @@ func (api *API) opdsDocuments(c *gin.Context) {
|
||||
splitFilepath := strings.Split(*doc.Filepath, ".")
|
||||
fileType := splitFilepath[len(splitFilepath)-1]
|
||||
|
||||
title := "N/A"
|
||||
if doc.Title != nil {
|
||||
title = *doc.Title
|
||||
}
|
||||
|
||||
author := "N/A"
|
||||
if doc.Author != nil {
|
||||
author = *doc.Author
|
||||
}
|
||||
|
||||
description := "N/A"
|
||||
if doc.Description != nil {
|
||||
description = *doc.Description
|
||||
}
|
||||
|
||||
item := opds.Entry{
|
||||
Title: fmt.Sprintf("[%3d%%] %s", int(doc.Percentage.(float64)), *doc.Title),
|
||||
Title: title,
|
||||
Author: []opds.Author{
|
||||
{
|
||||
Name: *doc.Author,
|
||||
Name: author,
|
||||
},
|
||||
},
|
||||
Content: &opds.Content{
|
||||
Content: *doc.Description,
|
||||
Content: description,
|
||||
ContentType: "text",
|
||||
},
|
||||
Links: []opds.Link{
|
||||
{
|
||||
Rel: "http://opds-spec.org/acquisition",
|
||||
Href: fmt.Sprintf("./documents/%s/file", doc.ID),
|
||||
Href: fmt.Sprintf("/api/opds/documents/%s/file", doc.ID),
|
||||
TypeLink: mimeMapping[fileType],
|
||||
},
|
||||
{
|
||||
Rel: "http://opds-spec.org/image",
|
||||
Href: fmt.Sprintf("./documents/%s/cover", doc.ID),
|
||||
Href: fmt.Sprintf("/api/opds/documents/%s/cover", doc.ID),
|
||||
TypeLink: "image/jpeg",
|
||||
},
|
||||
},
|
||||
@@ -84,19 +141,15 @@ func (api *API) opdsDocuments(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
feedTitle := "All Documents"
|
||||
if query != nil {
|
||||
feedTitle = "Search Results"
|
||||
}
|
||||
|
||||
// Build & Return XML
|
||||
searchFeed := &opds.Feed{
|
||||
Title: "All Documents",
|
||||
Title: feedTitle,
|
||||
Updated: time.Now().UTC(),
|
||||
// TODO
|
||||
// Links: []opds.Link{
|
||||
// {
|
||||
// Title: "Search Book Manager",
|
||||
// Rel: "search",
|
||||
// TypeLink: "application/opensearchdescription+xml",
|
||||
// Href: "search.xml",
|
||||
// },
|
||||
// },
|
||||
Entries: allEntries,
|
||||
}
|
||||
|
||||
@@ -105,9 +158,9 @@ func (api *API) opdsDocuments(c *gin.Context) {
|
||||
|
||||
func (api *API) opdsSearchDescription(c *gin.Context) {
|
||||
rawXML := `<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
|
||||
<ShortName>Search Book Manager</ShortName>
|
||||
<Description>Search Book Manager</Description>
|
||||
<Url type="application/atom+xml;profile=opds-catalog;kind=acquisition" template="./search?query={searchTerms}"/>
|
||||
<ShortName>Search AnthoLume</ShortName>
|
||||
<Description>Search AnthoLume</Description>
|
||||
<Url type="application/atom+xml;profile=opds-catalog;kind=acquisition" template="/api/opds/documents?search={searchTerms}"/>
|
||||
</OpenSearchDescription>`
|
||||
c.Data(http.StatusOK, "application/xml", []byte(rawXML))
|
||||
}
|
||||
|
||||
59
api/renderer/renderer.go
Normal file
59
api/renderer/renderer.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin/render"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
)
|
||||
|
||||
var Default = &HTMLTemplRenderer{}
|
||||
|
||||
type HTMLTemplRenderer struct {
|
||||
FallbackHtmlRenderer render.HTMLRender
|
||||
}
|
||||
|
||||
func (r *HTMLTemplRenderer) Instance(s string, d any) render.Render {
|
||||
templData, ok := d.(templ.Component)
|
||||
if !ok {
|
||||
if r.FallbackHtmlRenderer != nil {
|
||||
return r.FallbackHtmlRenderer.Instance(s, d)
|
||||
}
|
||||
}
|
||||
return &Renderer{
|
||||
Ctx: context.Background(),
|
||||
Status: -1,
|
||||
Component: templData,
|
||||
}
|
||||
}
|
||||
|
||||
func New(ctx context.Context, status int, component templ.Component) *Renderer {
|
||||
return &Renderer{
|
||||
Ctx: ctx,
|
||||
Status: status,
|
||||
Component: component,
|
||||
}
|
||||
}
|
||||
|
||||
type Renderer struct {
|
||||
Ctx context.Context
|
||||
Status int
|
||||
Component templ.Component
|
||||
}
|
||||
|
||||
func (t Renderer) Render(w http.ResponseWriter) error {
|
||||
t.WriteContentType(w)
|
||||
if t.Status != -1 {
|
||||
w.WriteHeader(t.Status)
|
||||
}
|
||||
if t.Component != nil {
|
||||
return t.Component.Render(t.Ctx, w)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t Renderer) WriteContentType(w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
}
|
||||
76
api/streamer.go
Normal file
76
api/streamer.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type streamer struct {
|
||||
templates map[string]*template.Template
|
||||
writer gin.ResponseWriter
|
||||
mutex sync.Mutex
|
||||
completeCh chan struct{}
|
||||
}
|
||||
|
||||
func (api *API) newStreamer(c *gin.Context, data string) *streamer {
|
||||
stream := &streamer{
|
||||
writer: c.Writer,
|
||||
templates: api.templates,
|
||||
completeCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
// Set Headers
|
||||
header := stream.writer.Header()
|
||||
header.Set("Transfer-Encoding", "chunked")
|
||||
header.Set("Content-Type", "text/html; charset=utf-8")
|
||||
header.Set("X-Content-Type-Options", "nosniff")
|
||||
stream.writer.WriteHeader(http.StatusOK)
|
||||
|
||||
// Send Open Element Tags
|
||||
stream.write(data)
|
||||
|
||||
// Keep Alive
|
||||
go func() {
|
||||
closeCh := stream.writer.CloseNotify()
|
||||
for {
|
||||
select {
|
||||
case <-stream.completeCh:
|
||||
return
|
||||
case <-closeCh:
|
||||
return
|
||||
default:
|
||||
stream.write("<!-- ping -->")
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
func (stream *streamer) write(str string) {
|
||||
stream.mutex.Lock()
|
||||
stream.writer.WriteString(str)
|
||||
stream.writer.(http.Flusher).Flush()
|
||||
stream.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (stream *streamer) send(templateName string, templateVars gin.H) {
|
||||
t := stream.templates[templateName]
|
||||
buf := &bytes.Buffer{}
|
||||
_ = t.ExecuteTemplate(buf, templateName, templateVars)
|
||||
stream.write(buf.String())
|
||||
}
|
||||
|
||||
func (stream *streamer) close(data string) {
|
||||
// Send Close Element Tags
|
||||
stream.write(data)
|
||||
|
||||
// Close
|
||||
close(stream.completeCh)
|
||||
}
|
||||
198
api/utils.go
198
api/utils.go
@@ -1,63 +1,60 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"reichard.io/bbank/database"
|
||||
"reichard.io/bbank/graph"
|
||||
"reichard.io/antholume/database"
|
||||
"reichard.io/antholume/graph"
|
||||
"reichard.io/antholume/metadata"
|
||||
)
|
||||
|
||||
type UTCOffset struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
var UTC_OFFSETS = []UTCOffset{
|
||||
{Value: "-12 hours", Name: "UTC−12:00"},
|
||||
{Value: "-11 hours", Name: "UTC−11:00"},
|
||||
{Value: "-10 hours", Name: "UTC−10:00"},
|
||||
{Value: "-9.5 hours", Name: "UTC−09:30"},
|
||||
{Value: "-9 hours", Name: "UTC−09:00"},
|
||||
{Value: "-8 hours", Name: "UTC−08:00"},
|
||||
{Value: "-7 hours", Name: "UTC−07:00"},
|
||||
{Value: "-6 hours", Name: "UTC−06:00"},
|
||||
{Value: "-5 hours", Name: "UTC−05:00"},
|
||||
{Value: "-4 hours", Name: "UTC−04:00"},
|
||||
{Value: "-3.5 hours", Name: "UTC−03:30"},
|
||||
{Value: "-3 hours", Name: "UTC−03:00"},
|
||||
{Value: "-2 hours", Name: "UTC−02:00"},
|
||||
{Value: "-1 hours", Name: "UTC−01:00"},
|
||||
{Value: "0 hours", Name: "UTC±00:00"},
|
||||
{Value: "+1 hours", Name: "UTC+01:00"},
|
||||
{Value: "+2 hours", Name: "UTC+02:00"},
|
||||
{Value: "+3 hours", Name: "UTC+03:00"},
|
||||
{Value: "+3.5 hours", Name: "UTC+03:30"},
|
||||
{Value: "+4 hours", Name: "UTC+04:00"},
|
||||
{Value: "+4.5 hours", Name: "UTC+04:30"},
|
||||
{Value: "+5 hours", Name: "UTC+05:00"},
|
||||
{Value: "+5.5 hours", Name: "UTC+05:30"},
|
||||
{Value: "+5.75 hours", Name: "UTC+05:45"},
|
||||
{Value: "+6 hours", Name: "UTC+06:00"},
|
||||
{Value: "+6.5 hours", Name: "UTC+06:30"},
|
||||
{Value: "+7 hours", Name: "UTC+07:00"},
|
||||
{Value: "+8 hours", Name: "UTC+08:00"},
|
||||
{Value: "+8.75 hours", Name: "UTC+08:45"},
|
||||
{Value: "+9 hours", Name: "UTC+09:00"},
|
||||
{Value: "+9.5 hours", Name: "UTC+09:30"},
|
||||
{Value: "+10 hours", Name: "UTC+10:00"},
|
||||
{Value: "+10.5 hours", Name: "UTC+10:30"},
|
||||
{Value: "+11 hours", Name: "UTC+11:00"},
|
||||
{Value: "+12 hours", Name: "UTC+12:00"},
|
||||
{Value: "+12.75 hours", Name: "UTC+12:45"},
|
||||
{Value: "+13 hours", Name: "UTC+13:00"},
|
||||
{Value: "+14 hours", Name: "UTC+14:00"},
|
||||
}
|
||||
|
||||
func getUTCOffsets() []UTCOffset {
|
||||
return UTC_OFFSETS
|
||||
// getTimeZones returns a string slice of IANA timezones.
|
||||
func getTimeZones() []string {
|
||||
return []string{
|
||||
"Africa/Cairo",
|
||||
"Africa/Johannesburg",
|
||||
"Africa/Lagos",
|
||||
"Africa/Nairobi",
|
||||
"America/Adak",
|
||||
"America/Anchorage",
|
||||
"America/Buenos_Aires",
|
||||
"America/Chicago",
|
||||
"America/Denver",
|
||||
"America/Los_Angeles",
|
||||
"America/Mexico_City",
|
||||
"America/New_York",
|
||||
"America/Nuuk",
|
||||
"America/Phoenix",
|
||||
"America/Puerto_Rico",
|
||||
"America/Sao_Paulo",
|
||||
"America/St_Johns",
|
||||
"America/Toronto",
|
||||
"Asia/Dubai",
|
||||
"Asia/Hong_Kong",
|
||||
"Asia/Kolkata",
|
||||
"Asia/Seoul",
|
||||
"Asia/Shanghai",
|
||||
"Asia/Singapore",
|
||||
"Asia/Tokyo",
|
||||
"Atlantic/Azores",
|
||||
"Australia/Melbourne",
|
||||
"Australia/Sydney",
|
||||
"Europe/Berlin",
|
||||
"Europe/London",
|
||||
"Europe/Moscow",
|
||||
"Europe/Paris",
|
||||
"Pacific/Auckland",
|
||||
"Pacific/Honolulu",
|
||||
}
|
||||
}
|
||||
|
||||
// niceSeconds takes in an int (in seconds) and returns a string readable
|
||||
// representation. For example 1928371 -> "22d 7h 39m 31s".
|
||||
func niceSeconds(input int64) (result string) {
|
||||
if input == 0 {
|
||||
return "N/A"
|
||||
@@ -86,12 +83,107 @@ func niceSeconds(input int64) (result string) {
|
||||
return
|
||||
}
|
||||
|
||||
// Convert Database Array -> Int64 Array
|
||||
// niceNumbers takes in an int and returns a string representation. For example
|
||||
// 19823 -> "19.8k".
|
||||
func niceNumbers(input int64) string {
|
||||
if input == 0 {
|
||||
return "0"
|
||||
}
|
||||
|
||||
abbreviations := []string{"", "k", "M", "B", "T"}
|
||||
abbrevIndex := int(math.Log10(float64(input)) / 3)
|
||||
scaledNumber := float64(input) / math.Pow(10, float64(abbrevIndex*3))
|
||||
|
||||
if scaledNumber >= 100 {
|
||||
return fmt.Sprintf("%.0f%s", scaledNumber, abbreviations[abbrevIndex])
|
||||
} else if scaledNumber >= 10 {
|
||||
return fmt.Sprintf("%.1f%s", scaledNumber, abbreviations[abbrevIndex])
|
||||
} else {
|
||||
return fmt.Sprintf("%.2f%s", scaledNumber, abbreviations[abbrevIndex])
|
||||
}
|
||||
}
|
||||
|
||||
// getSVGGraphData builds SVGGraphData from the provided stats, width and height.
|
||||
// It is used exclusively in templates to generate the daily read stats graph.
|
||||
func getSVGGraphData(inputData []database.GetDailyReadStatsRow, svgWidth int, svgHeight int) graph.SVGGraphData {
|
||||
var intData []int64
|
||||
var intData []graph.SVGRawData
|
||||
for _, item := range inputData {
|
||||
intData = append(intData, item.MinutesRead)
|
||||
intData = append(intData, graph.SVGRawData{
|
||||
Value: int(item.MinutesRead),
|
||||
Label: item.Date,
|
||||
})
|
||||
}
|
||||
|
||||
return graph.GetSVGGraphData(intData, svgWidth, svgHeight)
|
||||
}
|
||||
|
||||
// dict returns a map[string]any dict. Each pair of two is a key & value
|
||||
// respectively. It's primarily utilized in templates.
|
||||
func dict(values ...any) (map[string]any, error) {
|
||||
if len(values)%2 != 0 {
|
||||
return nil, errors.New("invalid dict call")
|
||||
}
|
||||
dict := make(map[string]any, len(values)/2)
|
||||
for i := 0; i < len(values); i += 2 {
|
||||
key, ok := values[i].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("dict keys must be strings")
|
||||
}
|
||||
dict[key] = values[i+1]
|
||||
}
|
||||
return dict, nil
|
||||
}
|
||||
|
||||
// fields returns a map[string]any of the provided struct. It's primarily
|
||||
// utilized in templates.
|
||||
func fields(value any) (map[string]any, error) {
|
||||
v := reflect.Indirect(reflect.ValueOf(value))
|
||||
if v.Kind() != reflect.Struct {
|
||||
return nil, fmt.Errorf("%T is not a struct", value)
|
||||
}
|
||||
m := make(map[string]any)
|
||||
t := v.Type()
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
sv := t.Field(i)
|
||||
m[sv.Name] = v.Field(i).Interface()
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// slice returns a slice of the provided arguments. It's primarily utilized in
|
||||
// templates.
|
||||
func slice(elements ...any) []any {
|
||||
return elements
|
||||
}
|
||||
|
||||
// deriveBaseFileName builds the base filename for a given MetadataInfo object.
|
||||
func deriveBaseFileName(metadataInfo *metadata.MetadataInfo) string {
|
||||
// Derive New FileName
|
||||
var newFileName string
|
||||
if *metadataInfo.Author != "" {
|
||||
newFileName = newFileName + *metadataInfo.Author
|
||||
} else {
|
||||
newFileName = newFileName + "Unknown"
|
||||
}
|
||||
if *metadataInfo.Title != "" {
|
||||
newFileName = newFileName + " - " + *metadataInfo.Title
|
||||
} else {
|
||||
newFileName = newFileName + " - Unknown"
|
||||
}
|
||||
|
||||
// Remove Slashes
|
||||
fileName := strings.ReplaceAll(newFileName, "/", "")
|
||||
return "." + filepath.Clean(fmt.Sprintf("/%s [%s]%s", fileName, *metadataInfo.PartialMD5, metadataInfo.Type))
|
||||
}
|
||||
|
||||
// importStatusPriority returns the order priority for import status in the UI.
|
||||
func importStatusPriority(status importStatus) int {
|
||||
switch status {
|
||||
case importFailed:
|
||||
return 1
|
||||
case importExists:
|
||||
return 2
|
||||
default:
|
||||
return 3
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,35 @@
|
||||
package api
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNiceSeconds(t *testing.T) {
|
||||
want := "22d 7h 39m 31s"
|
||||
nice := niceSeconds(1928371)
|
||||
wantOne := "22d 7h 39m 31s"
|
||||
wantNA := "N/A"
|
||||
|
||||
if nice != want {
|
||||
t.Fatalf(`Expected: %v, Got: %v`, want, nice)
|
||||
}
|
||||
niceOne := niceSeconds(1928371)
|
||||
niceNA := niceSeconds(0)
|
||||
|
||||
assert.Equal(t, wantOne, niceOne, "should be nice seconds")
|
||||
assert.Equal(t, wantNA, niceNA, "should be nice NA")
|
||||
}
|
||||
|
||||
func TestNiceNumbers(t *testing.T) {
|
||||
wantMillions := "198M"
|
||||
wantThousands := "19.8k"
|
||||
wantThousandsTwo := "1.98k"
|
||||
wantZero := "0"
|
||||
|
||||
niceMillions := niceNumbers(198236461)
|
||||
niceThousands := niceNumbers(19823)
|
||||
niceThousandsTwo := niceNumbers(1984)
|
||||
niceZero := niceNumbers(0)
|
||||
|
||||
assert.Equal(t, wantMillions, niceMillions, "should be nice millions")
|
||||
assert.Equal(t, wantThousands, niceThousands, "should be nice thousands")
|
||||
assert.Equal(t, wantThousandsTwo, niceThousandsTwo, "should be nice thousands")
|
||||
assert.Equal(t, wantZero, niceZero, "should be nice zero")
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Custom Service Worker Convenience Functions Wrapper
|
||||
**/
|
||||
const SW = (function () {
|
||||
// Helper Function
|
||||
function randomID() {
|
||||
@@ -63,3 +66,57 @@ const SW = (function () {
|
||||
|
||||
return { install, send };
|
||||
})();
|
||||
|
||||
/**
|
||||
* Custom IndexedDB Convenience Functions Wrapper
|
||||
**/
|
||||
const IDB = (function () {
|
||||
if (!idbKeyval)
|
||||
return console.error(
|
||||
"[IDB] idbKeyval not found - Did you load idb-keyval?"
|
||||
);
|
||||
|
||||
let { get, del, entries, update, keys } = idbKeyval;
|
||||
|
||||
return {
|
||||
async set(key, newValue) {
|
||||
let changeObj = {};
|
||||
await update(key, (oldValue) => {
|
||||
if (oldValue != null) changeObj.oldValue = oldValue;
|
||||
changeObj.newValue = newValue;
|
||||
return newValue;
|
||||
});
|
||||
return changeObj;
|
||||
},
|
||||
|
||||
get(key, defaultValue) {
|
||||
return get(key).then((resp) => {
|
||||
return defaultValue && resp == null ? defaultValue : resp;
|
||||
});
|
||||
},
|
||||
|
||||
del(key) {
|
||||
return del(key);
|
||||
},
|
||||
|
||||
find(keyRegExp, includeValues = false) {
|
||||
if (!(keyRegExp instanceof RegExp)) throw new Error("Invalid RegExp");
|
||||
|
||||
if (!includeValues)
|
||||
return keys().then((allKeys) =>
|
||||
allKeys.filter((key) => keyRegExp.test(key))
|
||||
);
|
||||
|
||||
return entries().then((allItems) => {
|
||||
const matchingKeys = allItems.filter((keyVal) =>
|
||||
keyRegExp.test(keyVal[0])
|
||||
);
|
||||
return matchingKeys.reduce((obj, keyVal) => {
|
||||
const [key, val] = keyVal;
|
||||
obj[key] = val;
|
||||
return obj;
|
||||
}, {});
|
||||
});
|
||||
},
|
||||
};
|
||||
})();
|
||||
BIN
assets/icons/favicon.ico
Normal file
BIN
assets/icons/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
assets/icons/icon512.png
Normal file
BIN
assets/icons/icon512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
2
assets/lib/epub.min.js
vendored
2
assets/lib/epub.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,50 +1 @@
|
||||
function _slicedToArray(t,n){return _arrayWithHoles(t)||_iterableToArrayLimit(t,n)||_unsupportedIterableToArray(t,n)||_nonIterableRest()}function _nonIterableRest(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function _unsupportedIterableToArray(t,n){if(t){if("string"==typeof t)return _arrayLikeToArray(t,n);var r=Object.prototype.toString.call(t).slice(8,-1);return"Object"===r&&t.constructor&&(r=t.constructor.name),"Map"===r||"Set"===r?Array.from(t):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?_arrayLikeToArray(t,n):void 0}}function _arrayLikeToArray(t,n){(null==n||n>t.length)&&(n=t.length);for(var r=0,e=new Array(n);r<n;r++)e[r]=t[r];return e}function _iterableToArrayLimit(t,n){var r=null==t?null:"undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(null!=r){var e,o,u=[],i=!0,a=!1;try{for(r=r.call(t);!(i=(e=r.next()).done)&&(u.push(e.value),!n||u.length!==n);i=!0);}catch(t){a=!0,o=t}finally{try{i||null==r.return||r.return()}finally{if(a)throw o}}return u}}function _arrayWithHoles(t){if(Array.isArray(t))return t}function _typeof(t){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},_typeof(t)}!function(t,n){"object"===("undefined"==typeof exports?"undefined":_typeof(exports))&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((t="undefined"!=typeof globalThis?globalThis:t||self).idbKeyval={})}(this,(function(t){"use strict";function n(t){return new Promise((function(n,r){t.oncomplete=t.onsuccess=function(){return n(t.result)},t.onabort=t.onerror=function(){return r(t.error)}}))}function r(t,r){var e=indexedDB.open(t);e.onupgradeneeded=function(){return e.result.createObjectStore(r)};var o=n(e);return function(t,n){return o.then((function(e){return n(e.transaction(r,t).objectStore(r))}))}}var e;function o(){return e||(e=r("keyval-store","keyval")),e}function u(t,r){return t.openCursor().onsuccess=function(){this.result&&(r(this.result),this.result.continue())},n(t.transaction)}t.clear=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:o();return t("readwrite",(function(t){return t.clear(),n(t.transaction)}))},t.createStore=r,t.del=function(t){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:o();return r("readwrite",(function(r){return r.delete(t),n(r.transaction)}))},t.delMany=function(t){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:o();return r("readwrite",(function(r){return t.forEach((function(t){return r.delete(t)})),n(r.transaction)}))},t.entries=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:o();return t("readonly",(function(r){if(r.getAll&&r.getAllKeys)return Promise.all([n(r.getAllKeys()),n(r.getAll())]).then((function(t){var n=_slicedToArray(t,2),r=n[0],e=n[1];return r.map((function(t,n){return[t,e[n]]}))}));var e=[];return t("readonly",(function(t){return u(t,(function(t){return e.push([t.key,t.value])})).then((function(){return e}))}))}))},t.get=function(t){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:o();return r("readonly",(function(r){return n(r.get(t))}))},t.getMany=function(t){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:o();return r("readonly",(function(r){return Promise.all(t.map((function(t){return n(r.get(t))})))}))},t.keys=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:o();return t("readonly",(function(t){if(t.getAllKeys)return n(t.getAllKeys());var r=[];return u(t,(function(t){return r.push(t.key)})).then((function(){return r}))}))},t.promisifyRequest=n,t.set=function(t,r){var e=arguments.length>2&&void 0!==arguments[2]?arguments[2]:o();return e("readwrite",(function(e){return e.put(r,t),n(e.transaction)}))},t.setMany=function(t){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:o();return r("readwrite",(function(r){return t.forEach((function(t){return r.put(t[1],t[0])})),n(r.transaction)}))},t.update=function(t,r){var e=arguments.length>2&&void 0!==arguments[2]?arguments[2]:o();return e("readwrite",(function(e){return new Promise((function(o,u){e.get(t).onsuccess=function(){try{e.put(r(this.result),t),o(n(e.transaction))}catch(t){u(t)}}}))}))},t.values=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:o();return t("readonly",(function(t){if(t.getAll)return n(t.getAll());var r=[];return u(t,(function(t){return r.push(t.value)})).then((function(){return r}))}))},Object.defineProperty(t,"__esModule",{value:!0})}));
|
||||
|
||||
/**
|
||||
* Custom IDB Convenience Functions Wrapper
|
||||
**/
|
||||
const IDB = (function () {
|
||||
let { get, del, entries, update, keys } = idbKeyval;
|
||||
|
||||
return {
|
||||
async set(key, newValue) {
|
||||
let changeObj = {};
|
||||
await update(key, (oldValue) => {
|
||||
if (oldValue != null) changeObj.oldValue = oldValue;
|
||||
changeObj.newValue = newValue;
|
||||
return newValue;
|
||||
});
|
||||
return changeObj;
|
||||
},
|
||||
|
||||
get(key, defaultValue) {
|
||||
return get(key).then((resp) => {
|
||||
return defaultValue && resp == null ? defaultValue : resp;
|
||||
});
|
||||
},
|
||||
|
||||
del(key) {
|
||||
return del(key);
|
||||
},
|
||||
|
||||
find(keyRegExp, includeValues = false) {
|
||||
if (!(keyRegExp instanceof RegExp)) throw new Error("Invalid RegExp");
|
||||
|
||||
if (!includeValues)
|
||||
return keys().then((allKeys) =>
|
||||
allKeys.filter((key) => keyRegExp.test(key))
|
||||
);
|
||||
|
||||
return entries().then((allItems) => {
|
||||
const matchingKeys = allItems.filter((keyVal) =>
|
||||
keyRegExp.test(keyVal[0])
|
||||
);
|
||||
return matchingKeys.reduce((obj, keyVal) => {
|
||||
const [key, val] = keyVal;
|
||||
obj[key] = val;
|
||||
return obj;
|
||||
}, {});
|
||||
});
|
||||
},
|
||||
};
|
||||
})();
|
||||
File diff suppressed because one or more lines are too long
2
assets/lib/no-sleep.min.js
vendored
Normal file
2
assets/lib/no-sleep.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -22,15 +22,18 @@
|
||||
media="(prefers-color-scheme: dark)"
|
||||
/>
|
||||
|
||||
<title>Book Manager - Local</title>
|
||||
<title>AnthoLume - Local</title>
|
||||
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="stylesheet" href="/assets/style.css" />
|
||||
|
||||
<!-- Libraries -->
|
||||
<script src="/assets/lib/jszip.min.js"></script>
|
||||
<script src="/assets/lib/epub.min.js"></script>
|
||||
<script src="/assets/lib/idb-keyval.js"></script>
|
||||
<script src="/assets/lib/sw-helper.js"></script>
|
||||
<script src="/assets/lib/idb-keyval.min.js"></script>
|
||||
|
||||
<!-- Local -->
|
||||
<script src="/assets/common.js"></script>
|
||||
<script src="/assets/index.js"></script>
|
||||
<script src="/assets/local/index.js"></script>
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
{
|
||||
"short_name": "Book Manager",
|
||||
"name": "Book Manager",
|
||||
"name": "AnthoLume",
|
||||
"short_name": "AnthoLume",
|
||||
"lang": "en-US",
|
||||
"theme_color": "#1F2937",
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"start_url": "/"
|
||||
"start_url": "/",
|
||||
"icons": [
|
||||
{
|
||||
"purpose": "any",
|
||||
"sizes": "512x512",
|
||||
"src": "/assets/icons/icon512.png",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
119
assets/reader/fonts.css
Normal file
119
assets/reader/fonts.css
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Lato
|
||||
* - Charsets: [latin,latin-ext]
|
||||
* - Styles: [100,700,100italic,regular,italic,700italic]
|
||||
**/
|
||||
|
||||
/* lato-100 - latin_latin-ext */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "Lato";
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
src: url("./fonts/lato-v24-latin_latin-ext-100.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* lato-100italic - latin_latin-ext */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "Lato";
|
||||
font-style: italic;
|
||||
font-weight: 100;
|
||||
src: url("./fonts/lato-v24-latin_latin-ext-100italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* lato-regular - latin_latin-ext */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "Lato";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url("./fonts/lato-v24-latin_latin-ext-regular.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* lato-italic - latin_latin-ext */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "Lato";
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url("./fonts/lato-v24-latin_latin-ext-italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* lato-700 - latin_latin-ext */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "Lato";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url("./fonts/lato-v24-latin_latin-ext-700.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* lato-700italic - latin_latin-ext */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "Lato";
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url("./fonts/lato-v24-latin_latin-ext-700italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/**
|
||||
* Open Sans
|
||||
* - Charsets: [latin,latin-ext]
|
||||
* - Styles: [700,regular,italic,700italic]
|
||||
**/
|
||||
|
||||
/* open-sans-regular - latin_latin-ext */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "Open Sans";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url("./fonts/open-sans-v36-latin_latin-ext-regular.woff2")
|
||||
format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* open-sans-italic - latin_latin-ext */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "Open Sans";
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url("./fonts/open-sans-v36-latin_latin-ext-italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* open-sans-700 - latin_latin-ext */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "Open Sans";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url("./fonts/open-sans-v36-latin_latin-ext-700.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* open-sans-700italic - latin_latin-ext */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "Open Sans";
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url("./fonts/open-sans-v36-latin_latin-ext-700italic.woff2")
|
||||
format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/**
|
||||
* Arbutus Slab
|
||||
* - Charsets: [latin,latin-ext]
|
||||
* - Styles: [regular]
|
||||
**/
|
||||
|
||||
/* arbutus-slab-regular - latin_latin-ext */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: "Arbutus Slab";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url("./fonts/arbutus-slab-v16-latin_latin-ext-regular.woff2")
|
||||
format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
Binary file not shown.
BIN
assets/reader/fonts/lato-v24-latin_latin-ext-100.woff2
Normal file
BIN
assets/reader/fonts/lato-v24-latin_latin-ext-100.woff2
Normal file
Binary file not shown.
BIN
assets/reader/fonts/lato-v24-latin_latin-ext-100italic.woff2
Normal file
BIN
assets/reader/fonts/lato-v24-latin_latin-ext-100italic.woff2
Normal file
Binary file not shown.
BIN
assets/reader/fonts/lato-v24-latin_latin-ext-700.woff2
Normal file
BIN
assets/reader/fonts/lato-v24-latin_latin-ext-700.woff2
Normal file
Binary file not shown.
BIN
assets/reader/fonts/lato-v24-latin_latin-ext-700italic.woff2
Normal file
BIN
assets/reader/fonts/lato-v24-latin_latin-ext-700italic.woff2
Normal file
Binary file not shown.
BIN
assets/reader/fonts/lato-v24-latin_latin-ext-italic.woff2
Normal file
BIN
assets/reader/fonts/lato-v24-latin_latin-ext-italic.woff2
Normal file
Binary file not shown.
BIN
assets/reader/fonts/lato-v24-latin_latin-ext-regular.woff2
Normal file
BIN
assets/reader/fonts/lato-v24-latin_latin-ext-regular.woff2
Normal file
Binary file not shown.
BIN
assets/reader/fonts/open-sans-v36-latin_latin-ext-700.woff2
Normal file
BIN
assets/reader/fonts/open-sans-v36-latin_latin-ext-700.woff2
Normal file
Binary file not shown.
Binary file not shown.
BIN
assets/reader/fonts/open-sans-v36-latin_latin-ext-italic.woff2
Normal file
BIN
assets/reader/fonts/open-sans-v36-latin_latin-ext-italic.woff2
Normal file
Binary file not shown.
BIN
assets/reader/fonts/open-sans-v36-latin_latin-ext-regular.woff2
Normal file
BIN
assets/reader/fonts/open-sans-v36-latin_latin-ext-regular.woff2
Normal file
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
@@ -14,24 +14,26 @@
|
||||
/>
|
||||
<meta name="theme-color" content="#D2B48C" />
|
||||
|
||||
<title>Book Manager - Reader</title>
|
||||
<title>AnthoLume - Reader</title>
|
||||
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="stylesheet" href="/assets/style.css" />
|
||||
|
||||
<!-- Libraries -->
|
||||
<script src="/assets/lib/platform.js"></script>
|
||||
<script src="/assets/lib/jszip.min.js"></script>
|
||||
<script src="/assets/lib/epub.min.js"></script>
|
||||
<script src="/assets/lib/no-sleep.js"></script>
|
||||
<script src="/assets/lib/idb-keyval.js"></script>
|
||||
<script src="/assets/lib/sw-helper.js"></script>
|
||||
<script src="/assets/lib/no-sleep.min.js"></script>
|
||||
<script src="/assets/lib/idb-keyval.min.js"></script>
|
||||
|
||||
<!-- Reader -->
|
||||
<script src="/assets/common.js"></script>
|
||||
<script src="/assets/index.js"></script>
|
||||
<script src="/assets/reader/index.js"></script>
|
||||
|
||||
<style>
|
||||
/* ----------------------------- */
|
||||
/* -------- PWA Styling -------- */
|
||||
/* ----------------------------- */
|
||||
html,
|
||||
body {
|
||||
overscroll-behavior-y: none;
|
||||
@@ -68,6 +70,10 @@
|
||||
#top-bar:not(.top-0) {
|
||||
top: calc((8em + env(safe-area-inset-top)) * -1);
|
||||
}
|
||||
|
||||
select:invalid {
|
||||
color: gray;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-100 dark:bg-gray-800">
|
||||
@@ -257,5 +263,120 @@
|
||||
</div>
|
||||
<div id="viewer" class="w-full h-full"></div>
|
||||
</main>
|
||||
|
||||
<!-- Device Selector -->
|
||||
<div
|
||||
id="device-selector"
|
||||
class="hidden absolute top-0 left-0 w-full h-full z-50"
|
||||
>
|
||||
<div
|
||||
class="fixed top-0 left-0 bg-black opacity-50 w-screen h-screen"
|
||||
></div>
|
||||
<div
|
||||
class="relative flex flex-col gap-4 p-4 max-h-[95%] w-5/6 md:w-1/2 bg-white dark:bg-gray-800 overflow-scroll -translate-x-2/4 -translate-y-2/4 top-1/2 left-1/2 overflow-hidden shadow rounded"
|
||||
>
|
||||
<div class="text-center flex flex-col gap-2">
|
||||
<h3 class="text-lg font-bold leading-6 dark:text-gray-300">
|
||||
Select Device
|
||||
</h3>
|
||||
|
||||
<p class="text-xs text-gray-500 text-center">
|
||||
This device appears to be new! Please either assume an existing
|
||||
device, or create a new one.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col gap-2 grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
||||
>
|
||||
<div class="flex gap-4 flex-col">
|
||||
<div class="flex relative min-w-[12em]">
|
||||
<span
|
||||
class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
||||
>
|
||||
<svg
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5.65517 2.22732C5.2225 2.34037 4.9438 2.50021 4.72718 2.71244C4.42179 3.01165 4.22268 3.43172 4.11382 4.225C4.00176 5.04159 4 6.12387 4 7.67568V16.2442C4.38867 15.9781 4.82674 15.7756 5.29899 15.6517C5.41296 15.6217 5.53103 15.5983 5.65517 15.5799V2.22732Z"
|
||||
/>
|
||||
<path
|
||||
d="M7.31034 15.5135C7.32206 15.5135 7.33382 15.5135 7.34563 15.5135L20 15.5135V7.67568C20 6.12387 19.9982 5.04159 19.8862 4.22499C19.7773 3.43172 19.5782 3.01165 19.2728 2.71244C18.9674 2.41324 18.5387 2.21816 17.729 2.11151C16.8955 2.00172 15.7908 2 14.2069 2H9.7931C8.79138 2 7.98133 2.00069 7.31034 2.02897V15.5135Z"
|
||||
/>
|
||||
<path
|
||||
d="M7.47341 17.1351C6.39395 17.1351 6.01657 17.1421 5.72738 17.218C4.93365 17.4264 4.30088 18.0044 4.02952 18.7558C4.0463 19.1382 4.07259 19.4746 4.11382 19.775C4.22268 20.5683 4.42179 20.9884 4.72718 21.2876C5.03258 21.5868 5.46135 21.7818 6.27103 21.8885C7.10452 21.9983 8.2092 22 9.7931 22H14.2069C15.7908 22 16.8955 21.9983 17.729 21.8885C18.5387 21.7818 18.9674 21.5868 19.2728 21.2876C19.5782 20.9884 19.7773 20.5683 19.8862 19.775C19.9776 19.1088 19.9956 18.2657 19.9991 17.1351H7.47341Z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<select
|
||||
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
||||
id="source"
|
||||
name="source"
|
||||
required
|
||||
>
|
||||
<option value="" disabled selected hidden>
|
||||
Select Existing Device
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2"
|
||||
>
|
||||
<span class="w-full">Assume Device</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col gap-2 grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
||||
>
|
||||
<div class="flex gap-4 flex-col">
|
||||
<div class="flex flex-col w-full grow">
|
||||
<div class="flex relative">
|
||||
<span
|
||||
class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
||||
>
|
||||
<svg
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5.65517 2.22732C5.2225 2.34037 4.9438 2.50021 4.72718 2.71244C4.42179 3.01165 4.22268 3.43172 4.11382 4.225C4.00176 5.04159 4 6.12387 4 7.67568V16.2442C4.38867 15.9781 4.82674 15.7756 5.29899 15.6517C5.41296 15.6217 5.53103 15.5983 5.65517 15.5799V2.22732Z"
|
||||
/>
|
||||
<path
|
||||
d="M7.31034 15.5135C7.32206 15.5135 7.33382 15.5135 7.34563 15.5135L20 15.5135V7.67568C20 6.12387 19.9982 5.04159 19.8862 4.22499C19.7773 3.43172 19.5782 3.01165 19.2728 2.71244C18.9674 2.41324 18.5387 2.21816 17.729 2.11151C16.8955 2.00172 15.7908 2 14.2069 2H9.7931C8.79138 2 7.98133 2.00069 7.31034 2.02897V15.5135Z"
|
||||
/>
|
||||
<path
|
||||
d="M7.47341 17.1351C6.39395 17.1351 6.01657 17.1421 5.72738 17.218C4.93365 17.4264 4.30088 18.0044 4.02952 18.7558C4.0463 19.1382 4.07259 19.4746 4.11382 19.775C4.22268 20.5683 4.42179 20.9884 4.72718 21.2876C5.03258 21.5868 5.46135 21.7818 6.27103 21.8885C7.10452 21.9983 8.2092 22 9.7931 22H14.2069C15.7908 22 16.8955 21.9983 17.729 21.8885C18.5387 21.7818 18.9674 21.5868 19.2728 21.2876C19.5782 20.9884 19.7773 20.5683 19.8862 19.775C19.9776 19.1088 19.9956 18.2657 19.9991 17.1351H7.47341Z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
||||
placeholder="New Device Name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2"
|
||||
>
|
||||
<span class="w-full">Create Device</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,5 +1,5 @@
|
||||
const THEMES = ["light", "tan", "blue", "gray", "black"];
|
||||
const THEME_FILE = "/assets/reader/readerThemes.css";
|
||||
const THEME_FILE = "/assets/reader/themes.css";
|
||||
|
||||
/**
|
||||
* Initial load handler. Gets called on DOMContentLoaded. Responsible for
|
||||
@@ -17,7 +17,7 @@ async function initReader() {
|
||||
|
||||
if (documentType == "REMOTE") {
|
||||
// Get Server / Cached Document
|
||||
let progressResp = await fetch("/documents/" + documentID + "/progress");
|
||||
let progressResp = await fetch("/reader/progress/" + documentID);
|
||||
documentData = await progressResp.json();
|
||||
|
||||
// Update With Local Cache
|
||||
@@ -73,7 +73,6 @@ function populateMetadata(data) {
|
||||
**/
|
||||
class EBookReader {
|
||||
bookState = {
|
||||
currentWord: 0,
|
||||
pages: 0,
|
||||
percentage: 0,
|
||||
progress: "",
|
||||
@@ -98,16 +97,18 @@ class EBookReader {
|
||||
flow: "paginated",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
allowScriptedContent: true,
|
||||
});
|
||||
|
||||
// Setup Reader
|
||||
this.book.ready.then(this.setupReader.bind(this));
|
||||
|
||||
// Initialize
|
||||
this.initCSP();
|
||||
this.initDevice();
|
||||
this.initWakeLock();
|
||||
this.initThemes();
|
||||
this.initRenditionListeners();
|
||||
this.initViewerListeners();
|
||||
this.initDocumentListeners();
|
||||
}
|
||||
|
||||
@@ -115,27 +116,11 @@ class EBookReader {
|
||||
* Load progress and generate locations
|
||||
**/
|
||||
async setupReader() {
|
||||
// Get Word Count (If Needed)
|
||||
if (this.bookState.words == 0)
|
||||
this.bookState.words = await this.countWords();
|
||||
// Get Word Count
|
||||
this.bookState.words = await this.countWords();
|
||||
|
||||
// Load Progress
|
||||
let { cfi } = await this.getCFIFromXPath(this.bookState.progress);
|
||||
this.bookState.currentWord = cfi
|
||||
? this.bookState.percentage * (this.bookState.words / 100)
|
||||
: 0;
|
||||
|
||||
let getStats = function () {
|
||||
// Start Timer
|
||||
this.bookState.pageStart = Date.now();
|
||||
|
||||
// Get Stats
|
||||
let stats = this.getBookStats();
|
||||
this.updateBookStatElements(stats);
|
||||
}.bind(this);
|
||||
|
||||
// Register Content Hook
|
||||
this.rendition.hooks.content.register(getStats);
|
||||
|
||||
// Update Position
|
||||
await this.setPosition(cfi);
|
||||
@@ -143,8 +128,14 @@ class EBookReader {
|
||||
// Highlight Element - DOM Has Element
|
||||
let { element } = await this.getCFIFromXPath(this.bookState.progress);
|
||||
|
||||
// Set Progress Element & Highlight
|
||||
this.bookState.progressElement = element;
|
||||
this.highlightPositionMarker();
|
||||
|
||||
// Update Stats & Page Start
|
||||
let stats = await this.getBookStats();
|
||||
this.updateBookStatElements(stats);
|
||||
this.bookState.pageStart = Date.now();
|
||||
}
|
||||
|
||||
initDevice() {
|
||||
@@ -152,18 +143,64 @@ class EBookReader {
|
||||
return "00000000000000000000000000000000".replace(/[018]/g, (c) =>
|
||||
(c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4))))
|
||||
.toString(16)
|
||||
.toUpperCase()
|
||||
.toUpperCase(),
|
||||
);
|
||||
}
|
||||
|
||||
this.readerSettings.deviceName =
|
||||
this.readerSettings.deviceName ||
|
||||
platform.os.toString() + " - " + platform.name;
|
||||
// Device Already Set
|
||||
if (this.readerSettings.deviceID) return;
|
||||
|
||||
this.readerSettings.deviceID = this.readerSettings.deviceID || randomID();
|
||||
// Get Elements
|
||||
let devicePopup = document.querySelector("#device-selector");
|
||||
let devSelector = devicePopup.querySelector("select");
|
||||
let devInput = devicePopup.querySelector("input");
|
||||
let [assumeButton, createButton] = devicePopup.querySelectorAll("button");
|
||||
|
||||
// Save Settings (Device ID)
|
||||
this.saveSettings();
|
||||
// Set Visible
|
||||
devicePopup.classList.remove("hidden");
|
||||
|
||||
// Add Devices
|
||||
fetch("/reader/devices").then(async (r) => {
|
||||
let data = await r.json();
|
||||
|
||||
data.forEach((item) => {
|
||||
let optionEl = document.createElement("option");
|
||||
optionEl.value = item.id;
|
||||
optionEl.textContent = item.device_name;
|
||||
devSelector.appendChild(optionEl);
|
||||
});
|
||||
});
|
||||
|
||||
assumeButton.addEventListener("click", () => {
|
||||
let deviceID = devSelector.value;
|
||||
|
||||
if (deviceID == "") {
|
||||
// TODO - Error Message
|
||||
return;
|
||||
}
|
||||
|
||||
let selectedOption = devSelector.children[devSelector.selectedIndex];
|
||||
let deviceName = selectedOption.textContent;
|
||||
|
||||
this.readerSettings.deviceID = deviceID;
|
||||
this.readerSettings.deviceName = deviceName;
|
||||
this.saveSettings();
|
||||
devicePopup.classList.add("hidden");
|
||||
});
|
||||
|
||||
createButton.addEventListener("click", () => {
|
||||
let deviceName = devInput.value.trim();
|
||||
|
||||
if (deviceName == "") {
|
||||
// TODO - Error Message
|
||||
return;
|
||||
}
|
||||
|
||||
this.readerSettings.deviceID = randomID();
|
||||
this.readerSettings.deviceName = deviceName;
|
||||
this.saveSettings();
|
||||
devicePopup.classList.add("hidden");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,7 +246,7 @@ class EBookReader {
|
||||
initThemes() {
|
||||
// Register Themes
|
||||
THEMES.forEach((theme) =>
|
||||
this.rendition.themes.register(theme, THEME_FILE)
|
||||
this.rendition.themes.register(theme, THEME_FILE),
|
||||
);
|
||||
|
||||
let themeLinkEl = document.createElement("link");
|
||||
@@ -232,25 +269,48 @@ class EBookReader {
|
||||
// Restore Theme
|
||||
this.setTheme();
|
||||
|
||||
// Set Fonts - TODO: Local
|
||||
// https://gwfh.mranftl.com/fonts
|
||||
// Set Fonts
|
||||
this.rendition.getContents().forEach((c) => {
|
||||
[
|
||||
"https://fonts.googleapis.com/css?family=Arbutus+Slab",
|
||||
"https://fonts.googleapis.com/css?family=Open+Sans",
|
||||
"https://fonts.googleapis.com/css?family=Lato:400,400i,700,700i",
|
||||
].forEach((url) => {
|
||||
let el = c.document.head.appendChild(
|
||||
c.document.createElement("link")
|
||||
);
|
||||
el.setAttribute("rel", "stylesheet");
|
||||
el.setAttribute("href", url);
|
||||
});
|
||||
let el = c.document.head.appendChild(
|
||||
c.document.createElement("link"),
|
||||
);
|
||||
el.setAttribute("rel", "stylesheet");
|
||||
el.setAttribute("href", "/assets/reader/fonts.css");
|
||||
});
|
||||
}.bind(this)
|
||||
}.bind(this),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* EpubJS will set iframe sandbox when settings "allowScriptedContent: false".
|
||||
* However, Safari completely blocks us from attaching listeners to the iframe
|
||||
* document. So instead we just inject a restrictive CSP rule.
|
||||
*
|
||||
* This effectively blocks all script content within the iframe while still
|
||||
* allowing us to attach listeners to the iframe document.
|
||||
**/
|
||||
initCSP() {
|
||||
// Derive CSP Host
|
||||
var protocol = document.location.protocol;
|
||||
var host = document.location.host;
|
||||
var cspURL = `${protocol}//${host}`;
|
||||
|
||||
// Add CSP Policy
|
||||
this.book.spine.hooks.content.register((output, section) => {
|
||||
let cspWrapper = document.createElement("div");
|
||||
cspWrapper.innerHTML = `
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="require-trusted-types-for 'script';
|
||||
style-src 'self' blob: 'unsafe-inline' ${cspURL};
|
||||
object-src 'none';
|
||||
script-src 'none';"
|
||||
>`;
|
||||
let cspMeta = cspWrapper.children[0];
|
||||
output.head.append(cspMeta);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set theme & meta theme color
|
||||
**/
|
||||
@@ -276,7 +336,7 @@ class EBookReader {
|
||||
let themeColorEl = document.querySelector("[name='theme-color']");
|
||||
let themeStyleSheet = document.querySelector("#themes").sheet;
|
||||
let themeStyleRule = Array.from(themeStyleSheet.cssRules).find(
|
||||
(item) => item.selectorText == "." + colorScheme
|
||||
(item) => item.selectorText == "." + colorScheme,
|
||||
);
|
||||
|
||||
// Match Reader Theme
|
||||
@@ -290,13 +350,13 @@ class EBookReader {
|
||||
// Set Font Family
|
||||
item.document.documentElement.style.setProperty(
|
||||
"--editor-font-family",
|
||||
fontFamily
|
||||
fontFamily,
|
||||
);
|
||||
|
||||
// Set Font Size
|
||||
item.document.documentElement.style.setProperty(
|
||||
"--editor-font-size",
|
||||
fontSize + "em"
|
||||
fontSize + "em",
|
||||
);
|
||||
|
||||
// Set Highlight Style
|
||||
@@ -329,7 +389,7 @@ class EBookReader {
|
||||
|
||||
// Compute Style
|
||||
let backgroundColor = getComputedStyle(
|
||||
this.bookState.progressElement.ownerDocument.body
|
||||
this.bookState.progressElement.ownerDocument.body,
|
||||
).backgroundColor;
|
||||
|
||||
// Set Style
|
||||
@@ -343,9 +403,9 @@ class EBookReader {
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendition hooks
|
||||
* Viewer Listeners
|
||||
**/
|
||||
initRenditionListeners() {
|
||||
initViewerListeners() {
|
||||
/**
|
||||
* Initiate the debounce when the given function returns true.
|
||||
* Don't run it again until the timeout lapses.
|
||||
@@ -373,15 +433,52 @@ class EBookReader {
|
||||
let bottomBar = document.querySelector("#bottom-bar");
|
||||
|
||||
// Local Functions
|
||||
let getCFIFromXPath = this.getCFIFromXPath.bind(this);
|
||||
let setPosition = this.setPosition.bind(this);
|
||||
let nextPage = this.nextPage.bind(this);
|
||||
let prevPage = this.prevPage.bind(this);
|
||||
let saveSettings = this.saveSettings.bind(this);
|
||||
|
||||
// Local Vars
|
||||
let readerSettings = this.readerSettings;
|
||||
let bookState = this.bookState;
|
||||
// ------------------------------------------------ //
|
||||
// ----------------- Swipe Helpers ---------------- //
|
||||
// ------------------------------------------------ //
|
||||
let touchStartX,
|
||||
touchStartY,
|
||||
touchEndX,
|
||||
touchEndY = undefined;
|
||||
|
||||
function handleGesture(event) {
|
||||
let drasticity = 75;
|
||||
|
||||
// Swipe Down
|
||||
if (touchEndY - drasticity > touchStartY) {
|
||||
return handleSwipeDown();
|
||||
}
|
||||
|
||||
// Swipe Up
|
||||
if (touchEndY + drasticity < touchStartY) {
|
||||
// Prioritize Down & Up Swipes
|
||||
return handleSwipeUp();
|
||||
}
|
||||
|
||||
// Swipe Left
|
||||
if (touchEndX + drasticity < touchStartX) {
|
||||
nextPage();
|
||||
}
|
||||
|
||||
// Swipe Right
|
||||
if (touchEndX - drasticity > touchStartX) {
|
||||
prevPage();
|
||||
}
|
||||
}
|
||||
|
||||
function handleSwipeDown() {
|
||||
if (bottomBar.classList.contains("bottom-0"))
|
||||
bottomBar.classList.remove("bottom-0");
|
||||
else topBar.classList.add("top-0");
|
||||
}
|
||||
|
||||
function handleSwipeUp() {
|
||||
if (topBar.classList.contains("top-0")) topBar.classList.remove("top-0");
|
||||
else bottomBar.classList.add("bottom-0");
|
||||
}
|
||||
|
||||
this.rendition.hooks.render.register(function (doc, data) {
|
||||
let renderDoc = doc.document;
|
||||
@@ -390,66 +487,14 @@ class EBookReader {
|
||||
// ---------------- Wake Lock Hack ---------------- //
|
||||
// ------------------------------------------------ //
|
||||
let wakeLockListener = function () {
|
||||
doc.window.parent.document.dispatchEvent(new CustomEvent("wakelock"));
|
||||
renderDoc.dispatchEvent(new CustomEvent("wakelock"));
|
||||
};
|
||||
renderDoc.addEventListener("click", wakeLockListener);
|
||||
renderDoc.addEventListener("gesturechange", wakeLockListener);
|
||||
renderDoc.addEventListener("touchstart", wakeLockListener);
|
||||
|
||||
// ------------------------------------------------ //
|
||||
// --------------- Swipe Pagination --------------- //
|
||||
// ------------------------------------------------ //
|
||||
let touchStartX,
|
||||
touchStartY,
|
||||
touchEndX,
|
||||
touchEndY = undefined;
|
||||
|
||||
renderDoc.addEventListener(
|
||||
"touchstart",
|
||||
function (event) {
|
||||
touchStartX = event.changedTouches[0].screenX;
|
||||
touchStartY = event.changedTouches[0].screenY;
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
renderDoc.addEventListener(
|
||||
"touchend",
|
||||
function (event) {
|
||||
touchEndX = event.changedTouches[0].screenX;
|
||||
touchEndY = event.changedTouches[0].screenY;
|
||||
handleGesture(event);
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
function handleGesture(event) {
|
||||
let drasticity = 75;
|
||||
|
||||
// Swipe Down
|
||||
if (touchEndY - drasticity > touchStartY) {
|
||||
return handleSwipeDown();
|
||||
}
|
||||
|
||||
// Swipe Up
|
||||
if (touchEndY + drasticity < touchStartY) {
|
||||
// Prioritize Down & Up Swipes
|
||||
return handleSwipeUp();
|
||||
}
|
||||
|
||||
// Swipe Left
|
||||
if (touchEndX + drasticity < touchStartX) {
|
||||
nextPage();
|
||||
}
|
||||
|
||||
// Swipe Right
|
||||
if (touchEndX - drasticity > touchStartX) {
|
||||
prevPage();
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------ //
|
||||
// --------------- Bottom & Top Bar --------------- //
|
||||
// --------------- Bars & Page Turn --------------- //
|
||||
// ------------------------------------------------ //
|
||||
renderDoc.addEventListener(
|
||||
"click",
|
||||
@@ -484,7 +529,7 @@ class EBookReader {
|
||||
bottomBar.classList.remove("bottom-0");
|
||||
topBar.classList.remove("top-0");
|
||||
}
|
||||
}.bind(this)
|
||||
}.bind(this),
|
||||
);
|
||||
|
||||
renderDoc.addEventListener(
|
||||
@@ -498,50 +543,30 @@ class EBookReader {
|
||||
handleSwipeDown();
|
||||
return true;
|
||||
}
|
||||
}, 400)
|
||||
}, 400),
|
||||
);
|
||||
|
||||
function handleSwipeDown() {
|
||||
if (bottomBar.classList.contains("bottom-0"))
|
||||
bottomBar.classList.remove("bottom-0");
|
||||
else topBar.classList.add("top-0");
|
||||
}
|
||||
|
||||
function handleSwipeUp() {
|
||||
if (topBar.classList.contains("top-0"))
|
||||
topBar.classList.remove("top-0");
|
||||
else bottomBar.classList.add("bottom-0");
|
||||
}
|
||||
|
||||
// ------------------------------------------------ //
|
||||
// -------------- Keyboard Shortcuts -------------- //
|
||||
// ------------------- Gestures ------------------- //
|
||||
// ------------------------------------------------ //
|
||||
|
||||
renderDoc.addEventListener(
|
||||
"keyup",
|
||||
function (e) {
|
||||
// Left Key (Previous Page)
|
||||
if ((e.keyCode || e.which) == 37) {
|
||||
prevPage();
|
||||
}
|
||||
|
||||
// Right Key (Next Page)
|
||||
if ((e.keyCode || e.which) == 39) {
|
||||
nextPage();
|
||||
}
|
||||
|
||||
// "t" Key (Theme Cycle)
|
||||
if ((e.keyCode || e.which) == 84) {
|
||||
let currentThemeIdx = THEMES.indexOf(
|
||||
readerSettings.theme.colorScheme
|
||||
);
|
||||
let colorScheme =
|
||||
THEMES.length == currentThemeIdx + 1
|
||||
? THEMES[0]
|
||||
: THEMES[currentThemeIdx + 1];
|
||||
setTheme({ colorScheme });
|
||||
}
|
||||
"touchstart",
|
||||
function (event) {
|
||||
touchStartX = event.changedTouches[0].screenX;
|
||||
touchStartY = event.changedTouches[0].screenY;
|
||||
},
|
||||
false
|
||||
false,
|
||||
);
|
||||
|
||||
renderDoc.addEventListener(
|
||||
"touchend",
|
||||
function (event) {
|
||||
touchEndX = event.changedTouches[0].screenX;
|
||||
touchEndY = event.changedTouches[0].screenY;
|
||||
handleGesture(event);
|
||||
},
|
||||
false,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -556,7 +581,9 @@ class EBookReader {
|
||||
let nextPage = this.nextPage.bind(this);
|
||||
let prevPage = this.prevPage.bind(this);
|
||||
|
||||
// Keyboard Shortcuts
|
||||
// ------------------------------------------------ //
|
||||
// -------------- Keyboard Shortcuts -------------- //
|
||||
// ------------------------------------------------ //
|
||||
document.addEventListener(
|
||||
"keyup",
|
||||
function (e) {
|
||||
@@ -573,7 +600,7 @@ class EBookReader {
|
||||
// "t" Key (Theme Cycle)
|
||||
if ((e.keyCode || e.which) == 84) {
|
||||
let currentThemeIdx = THEMES.indexOf(
|
||||
this.readerSettings.theme.colorScheme
|
||||
this.readerSettings.theme.colorScheme,
|
||||
);
|
||||
let colorScheme =
|
||||
THEMES.length == currentThemeIdx + 1
|
||||
@@ -582,7 +609,7 @@ class EBookReader {
|
||||
this.setTheme({ colorScheme });
|
||||
}
|
||||
}.bind(this),
|
||||
false
|
||||
false,
|
||||
);
|
||||
|
||||
// Color Scheme Switcher
|
||||
@@ -593,9 +620,9 @@ class EBookReader {
|
||||
function (event) {
|
||||
let colorScheme = event.target.innerText;
|
||||
this.setTheme({ colorScheme });
|
||||
}.bind(this)
|
||||
}.bind(this),
|
||||
);
|
||||
}.bind(this)
|
||||
}.bind(this),
|
||||
);
|
||||
|
||||
// Font Switcher
|
||||
@@ -610,9 +637,9 @@ class EBookReader {
|
||||
this.setTheme({ fontFamily });
|
||||
|
||||
this.setPosition(cfi);
|
||||
}.bind(this)
|
||||
}.bind(this),
|
||||
);
|
||||
}.bind(this)
|
||||
}.bind(this),
|
||||
);
|
||||
|
||||
// Font Size
|
||||
@@ -635,9 +662,9 @@ class EBookReader {
|
||||
|
||||
// Restore CFI
|
||||
this.setPosition(cfi);
|
||||
}.bind(this)
|
||||
}.bind(this),
|
||||
);
|
||||
}.bind(this)
|
||||
}.bind(this),
|
||||
);
|
||||
|
||||
// Close Top Bar
|
||||
@@ -660,7 +687,7 @@ class EBookReader {
|
||||
this.bookState.pageStart = Date.now();
|
||||
|
||||
// Update Stats
|
||||
let stats = this.getBookStats();
|
||||
let stats = await this.getBookStats();
|
||||
this.updateBookStatElements(stats);
|
||||
|
||||
// Create Progress
|
||||
@@ -674,16 +701,11 @@ class EBookReader {
|
||||
// Render Previous Page
|
||||
await this.rendition.prev();
|
||||
|
||||
// Update Current Word
|
||||
let pageWords = await this.getVisibleWordCount();
|
||||
this.bookState.currentWord -= pageWords;
|
||||
if (this.bookState.currentWord < 0) this.bookState.currentWord = 0;
|
||||
|
||||
// Reset Read Timer
|
||||
this.bookState.pageStart = Date.now();
|
||||
|
||||
// Update Stats
|
||||
let stats = this.getBookStats();
|
||||
let stats = await this.getBookStats();
|
||||
this.updateBookStatElements(stats);
|
||||
|
||||
// Create Progress
|
||||
@@ -719,9 +741,8 @@ class EBookReader {
|
||||
|
||||
// Update Current Word
|
||||
let pageWords = await this.getVisibleWordCount();
|
||||
let startingWord = this.bookState.currentWord;
|
||||
let currentWord = await this.getBookWordPosition();
|
||||
let percentRead = pageWords / this.bookState.words;
|
||||
this.bookState.currentWord += pageWords;
|
||||
|
||||
let pageWPM = pageWords / (elapsedTime / 60000);
|
||||
console.log("[createActivity] Page WPM:", pageWPM);
|
||||
@@ -730,7 +751,7 @@ class EBookReader {
|
||||
if (pageWPM >= WPM_MAX)
|
||||
return console.log(
|
||||
"[createActivity] Page WPM Exceeds Max (2000):",
|
||||
pageWPM
|
||||
pageWPM,
|
||||
);
|
||||
|
||||
// Ensure WPM Minimum
|
||||
@@ -740,10 +761,10 @@ class EBookReader {
|
||||
|
||||
// Exclude 0 Pages
|
||||
if (totalPages == 0)
|
||||
return console.log("[createActivity] Invalid Total Pages (0)");
|
||||
return console.warn("[createActivity] Invalid Total Pages (0)");
|
||||
|
||||
let currentPage = Math.round(
|
||||
(startingWord * totalPages) / this.bookState.words
|
||||
(currentWord * totalPages) / this.bookState.words,
|
||||
);
|
||||
|
||||
// Create Activity Event
|
||||
@@ -797,7 +818,7 @@ class EBookReader {
|
||||
response: r,
|
||||
json: await r.json(),
|
||||
data: activityEvent,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -805,6 +826,8 @@ class EBookReader {
|
||||
// Update Pointers
|
||||
let currentCFI = await this.rendition.currentLocation();
|
||||
let { element, xpath } = await this.getXPathFromCFI(currentCFI.start.cfi);
|
||||
let currentWord = await this.getBookWordPosition();
|
||||
console.log("[createProgress] Current Word:", currentWord);
|
||||
this.bookState.progress = xpath;
|
||||
this.bookState.progressElement = element;
|
||||
|
||||
@@ -814,9 +837,7 @@ class EBookReader {
|
||||
device_id: this.readerSettings.deviceID,
|
||||
device: this.readerSettings.deviceName,
|
||||
percentage:
|
||||
Math.round(
|
||||
(this.bookState.currentWord / this.bookState.words) * 100000
|
||||
) / 100000,
|
||||
Math.round((currentWord / this.bookState.words) * 100000) / 100000,
|
||||
progress: this.bookState.progress,
|
||||
};
|
||||
|
||||
@@ -858,7 +879,7 @@ class EBookReader {
|
||||
response: r,
|
||||
json: await r.json(),
|
||||
data: progressEvent,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -885,15 +906,16 @@ class EBookReader {
|
||||
/**
|
||||
* Get chapter pages, name and progress percentage
|
||||
**/
|
||||
getBookStats() {
|
||||
async getBookStats() {
|
||||
let currentProgress = this.sectionProgress();
|
||||
if (!currentProgress) return;
|
||||
let { sectionPages, sectionCurrentPage } = currentProgress;
|
||||
|
||||
let currentLocation = this.rendition.currentLocation();
|
||||
let currentWord = await this.getBookWordPosition();
|
||||
|
||||
let currentTOC = this.book.navigation.toc.find(
|
||||
(item) => item.href == currentLocation.start.href
|
||||
(item) => item.href == currentLocation.start.href,
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -901,9 +923,7 @@ class EBookReader {
|
||||
sectionTotalPages: sectionPages,
|
||||
chapterName: currentTOC ? currentTOC.label.trim() : "N/A",
|
||||
percentage:
|
||||
Math.round(
|
||||
(this.bookState.currentWord / this.bookState.words) * 10000
|
||||
) / 100,
|
||||
Math.round((currentWord / this.bookState.words) * 10000) / 100,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -928,66 +948,70 @@ class EBookReader {
|
||||
* Get XPath from current location
|
||||
**/
|
||||
async getXPathFromCFI(cfi) {
|
||||
// Get DocFragment (current book spline index)
|
||||
// Get DocFragment (Spine Index)
|
||||
let startCFI = cfi.replace("epubcfi(", "");
|
||||
let docFragmentIndex =
|
||||
this.book.spine.spineItems.find((item) =>
|
||||
startCFI.startsWith(item.cfiBase)
|
||||
startCFI.startsWith(item.cfiBase),
|
||||
).index + 1;
|
||||
|
||||
// Base Progress
|
||||
let newPos = "/body/DocFragment[" + docFragmentIndex + "]/body";
|
||||
let basePos = "/body/DocFragment[" + docFragmentIndex + "]/body";
|
||||
|
||||
// Get first visible node
|
||||
// Get First Node & Element Reference
|
||||
let contents = this.rendition.getContents()[0];
|
||||
let node = contents.range(cfi).startContainer;
|
||||
let element = null;
|
||||
let currentNode = contents.range(cfi).startContainer;
|
||||
let element =
|
||||
currentNode.nodeType == Node.ELEMENT_NODE
|
||||
? currentNode
|
||||
: currentNode.parentElement;
|
||||
|
||||
// Walk upwards and build progress until body
|
||||
let childPos = "";
|
||||
while (node.nodeName != "BODY") {
|
||||
let ownValue;
|
||||
// XPath Reference
|
||||
let allPos = "";
|
||||
|
||||
switch (node.nodeType) {
|
||||
case Node.ELEMENT_NODE:
|
||||
// Store First Element Node
|
||||
if (!element) element = node;
|
||||
let relativeIndex =
|
||||
Array.from(node.parentNode.children)
|
||||
.filter((item) => item.nodeName == node.nodeName)
|
||||
.indexOf(node) + 1;
|
||||
// Walk Upwards
|
||||
while (currentNode.nodeName != "BODY") {
|
||||
// Get Parent
|
||||
let parentElement = currentNode.parentElement;
|
||||
|
||||
ownValue = node.nodeName.toLowerCase() + "[" + relativeIndex + "]";
|
||||
break;
|
||||
case Node.ATTRIBUTE_NODE:
|
||||
ownValue = "@" + node.nodeName;
|
||||
break;
|
||||
case Node.TEXT_NODE:
|
||||
case Node.CDATA_SECTION_NODE:
|
||||
ownValue = "text()";
|
||||
break;
|
||||
case Node.PROCESSING_INSTRUCTION_NODE:
|
||||
ownValue = "processing-instruction()";
|
||||
break;
|
||||
case Node.COMMENT_NODE:
|
||||
ownValue = "comment()";
|
||||
break;
|
||||
case Node.DOCUMENT_NODE:
|
||||
ownValue = "";
|
||||
break;
|
||||
default:
|
||||
ownValue = "";
|
||||
break;
|
||||
// Unknown Node -> Update Reference
|
||||
if (currentNode.nodeType != Node.ELEMENT_NODE) {
|
||||
console.log("[getXPathFromCFI] Unknown Node Type:", currentNode);
|
||||
currentNode = parentElement;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Prepend childPos & Update node reference
|
||||
childPos = "/" + ownValue + childPos;
|
||||
node = node.parentNode;
|
||||
/**
|
||||
* Exclude A tags. This could potentially be all inline elements:
|
||||
* https://github.com/koreader/crengine/blob/master/cr3gui/data/epub.css#L149
|
||||
**/
|
||||
while (parentElement.nodeName == "A") {
|
||||
parentElement = parentElement.parentElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: This is depth / document order first, which means that this
|
||||
* _could_ return incorrect results when dealing with nested "A" tags
|
||||
* (dependent on how KOReader deals with nested "A" tags)
|
||||
**/
|
||||
let allDescendents = parentElement.querySelectorAll(currentNode.nodeName);
|
||||
let relativeIndex = Array.from(allDescendents).indexOf(currentNode) + 1;
|
||||
|
||||
// Get Node Position
|
||||
let nodePos =
|
||||
currentNode.nodeName.toLowerCase() + "[" + relativeIndex + "]";
|
||||
|
||||
// Update Reference
|
||||
currentNode = parentElement;
|
||||
|
||||
// Update Position
|
||||
allPos = "/" + nodePos + allPos;
|
||||
}
|
||||
|
||||
let xpath = newPos + childPos;
|
||||
// Combine XPath
|
||||
let xpath = basePos + allPos;
|
||||
|
||||
// Return derived progress
|
||||
// Return Derived Progress
|
||||
return { xpath, element };
|
||||
}
|
||||
|
||||
@@ -995,19 +1019,13 @@ class EBookReader {
|
||||
* Get CFI from current location
|
||||
**/
|
||||
async getCFIFromXPath(xpath) {
|
||||
// XPath Reference - Example: /body/DocFragment[15]/body/div[10]/text().184
|
||||
//
|
||||
// - /body/DocFragment[15] = 15th item in book spline
|
||||
// - [...]/body/div[10] = 10th child div under body (direct descendents only)
|
||||
// - [...]/text().184 = text node of parent, character offset @ 184 chars?
|
||||
|
||||
// No XPath
|
||||
if (!xpath || xpath == "") return {};
|
||||
|
||||
// Match Document Fragment Index
|
||||
let fragMatch = xpath.match(/^\/body\/DocFragment\[(\d+)\]/);
|
||||
if (!fragMatch) {
|
||||
console.warn("No XPath Match");
|
||||
console.warn("[getCFIFromXPath] No XPath Match");
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -1039,7 +1057,7 @@ class EBookReader {
|
||||
.find((item) => item.sectionIndex == spinePosition)?.document ||
|
||||
sectionItem.document;
|
||||
|
||||
// Derive XPath & Namespace
|
||||
// Derive Namespace & XPath
|
||||
let namespaceURI = docItem.documentElement.namespaceURI;
|
||||
let remainingXPath = xpath
|
||||
// Replace with new base
|
||||
@@ -1049,6 +1067,26 @@ class EBookReader {
|
||||
// Remove potential trailing `text()`
|
||||
.replace(/\/text\(\)(\[\d+\])?$/, "");
|
||||
|
||||
// XPath to Element
|
||||
let derivedSelectorElement = remainingXPath
|
||||
.replace(/^\/html\/body/, "body")
|
||||
.split("/")
|
||||
.reduce((el, item) => {
|
||||
// No Match
|
||||
if (!el) return null;
|
||||
|
||||
// Non Index
|
||||
let indexMatch = item.match(/(\w+)\[(\d+)\]$/);
|
||||
if (!indexMatch) return el.querySelector(item);
|
||||
|
||||
// Get @ Index
|
||||
let tag = indexMatch[1];
|
||||
let index = parseInt(indexMatch[2]) - 1;
|
||||
return el.querySelectorAll(tag)[index];
|
||||
}, docItem);
|
||||
|
||||
console.log("[getCFIFromXPath] Selector Element:", derivedSelectorElement);
|
||||
|
||||
// Validate Namespace
|
||||
if (namespaceURI) remainingXPath = remainingXPath.replaceAll("/", "/ns:");
|
||||
|
||||
@@ -1062,11 +1100,28 @@ class EBookReader {
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Get Element & CFI
|
||||
let element = docSearch.iterateNext();
|
||||
/**
|
||||
* There are two ways to do this. One via XPath, and the other via derived
|
||||
* CSS selectors. Unfortunately it seems like KOReaders XPath implementation
|
||||
* is a little wonky, requiring the need for CSS Selectors.
|
||||
*
|
||||
* For example the following XPath was generated by KOReader:
|
||||
* "/body/DocFragment[19]/body/h1/img.0"
|
||||
*
|
||||
* In reality, the XPath should have been (note the 'a'):
|
||||
* "/body/DocFragment[19]/body/h1/a/img.0"
|
||||
*
|
||||
* Unfortunately due to the above, `docItem.evaluate` will not find the
|
||||
* element. So as an alternative I thought it would be possible to derive
|
||||
* a CSS selector. I think this should be fully comprehensive; AFAICT
|
||||
* KOReader only creates XPaths referencing HTML tag names and indexes.
|
||||
**/
|
||||
|
||||
// Get Element & CFI (XPath -> CSS Selector Fallback)
|
||||
let element = docSearch.iterateNext() || derivedSelectorElement;
|
||||
let cfi = sectionItem.cfiFromElement(element);
|
||||
|
||||
return { cfi, element };
|
||||
@@ -1076,6 +1131,43 @@ class EBookReader {
|
||||
* Get visible word count - used for reading stats
|
||||
**/
|
||||
async getVisibleWordCount() {
|
||||
let visibleText = await this.getVisibleText();
|
||||
return visibleText.trim().split(/\s+/).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the word number of the whole book for the first visible word.
|
||||
**/
|
||||
async getBookWordPosition() {
|
||||
// Get Contents & Spine
|
||||
let contents = this.rendition.getContents()[0];
|
||||
let spineItem = this.book.spine.get(contents.sectionIndex);
|
||||
|
||||
// Get CFI Range
|
||||
let firstCFI = spineItem.cfiFromElement(
|
||||
spineItem.document.body.children[0],
|
||||
);
|
||||
let currentLocation = await this.rendition.currentLocation();
|
||||
let cfiRange = this.getCFIRange(firstCFI, currentLocation.start.cfi);
|
||||
|
||||
// Get Chapter Text (Before Current Position)
|
||||
let textRange = await this.book.getRange(cfiRange);
|
||||
let chapterText = textRange.toString();
|
||||
|
||||
// Get Chapter & Book Positions
|
||||
let chapterWordPosition = chapterText.trim().split(/\s+/).length;
|
||||
let preChapterWordPosition = this.book.spine.spineItems
|
||||
.slice(0, contents.sectionIndex)
|
||||
.reduce((totalCount, item) => totalCount + item.wordCount, 0);
|
||||
|
||||
// Return Current Word Pointer
|
||||
return chapterWordPosition + preChapterWordPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get visible text - used for word counts
|
||||
**/
|
||||
async getVisibleText() {
|
||||
// Force Expand & Resize (Race Condition Issue)
|
||||
this.rendition.manager.visible().forEach((item) => item.expand());
|
||||
|
||||
@@ -1092,7 +1184,7 @@ class EBookReader {
|
||||
let visibleText = textRange.toString();
|
||||
|
||||
// Split on Whitespace
|
||||
return visibleText.trim().split(/\s+/).length;
|
||||
return visibleText;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1147,14 +1239,17 @@ class EBookReader {
|
||||
* of progress percentage. Implementation returns the same number as the
|
||||
* server side implementation.
|
||||
**/
|
||||
countWords() {
|
||||
// Iterate over each item in the spine, render, and count words.
|
||||
return this.book.spine.spineItems.reduce(async (totalCount, item) => {
|
||||
let currentCount = await totalCount;
|
||||
let newDoc = await item.load(this.book.load.bind(this.book));
|
||||
let itemCount = newDoc.innerText.trim().split(/\s+/).length;
|
||||
return currentCount + itemCount;
|
||||
}, 0);
|
||||
async countWords() {
|
||||
let spineWC = await Promise.all(
|
||||
this.book.spine.spineItems.map(async (item) => {
|
||||
let newDoc = await item.load(this.book.load.bind(this.book));
|
||||
let spineWords = newDoc.innerText.trim().split(/\s+/).length;
|
||||
item.wordCount = spineWords;
|
||||
return spineWords;
|
||||
}),
|
||||
);
|
||||
|
||||
return spineWC.reduce((totalCount, itemCount) => totalCount + itemCount, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1170,9 +1265,20 @@ class EBookReader {
|
||||
**/
|
||||
loadSettings() {
|
||||
this.readerSettings = JSON.parse(
|
||||
localStorage.getItem("readerSettings") || "{}"
|
||||
localStorage.getItem("readerSettings") || "{}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", initReader);
|
||||
|
||||
// WIP
|
||||
async function getTOC() {
|
||||
let toc = currentReader.book.navigation.toc;
|
||||
|
||||
// Alternatively:
|
||||
// let nav = await currentReader.book.loaded.navigation;
|
||||
// let toc = nav.toc;
|
||||
|
||||
currentReader.rendition.display(nav.toc[10].href);
|
||||
}
|
||||
|
||||
2140
assets/style.css
2140
assets/style.css
File diff suppressed because one or more lines are too long
77
assets/sw.js
77
assets/sw.js
@@ -38,13 +38,14 @@ const ROUTES = [
|
||||
{ route: "/local", type: CACHE_UPDATE_ASYNC },
|
||||
{ route: "/reader", type: CACHE_UPDATE_ASYNC },
|
||||
{ route: "/manifest.json", type: CACHE_UPDATE_ASYNC },
|
||||
{ route: /^\/assets\/reader\/fonts\//, type: CACHE_ONLY },
|
||||
{ route: /^\/assets\//, type: CACHE_UPDATE_ASYNC },
|
||||
{
|
||||
route: /^\/documents\/[a-zA-Z0-9]{32}\/(cover|file)$/,
|
||||
type: CACHE_UPDATE_ASYNC,
|
||||
},
|
||||
{
|
||||
route: /^\/documents\/[a-zA-Z0-9]{32}\/progress$/,
|
||||
route: /^\/reader\/progress\/[a-zA-Z0-9]{32}$/,
|
||||
type: CACHE_UPDATE_SYNC,
|
||||
},
|
||||
{
|
||||
@@ -63,28 +64,42 @@ const PRECACHE_ASSETS = [
|
||||
"/reader",
|
||||
"/assets/local/index.js",
|
||||
"/assets/reader/index.js",
|
||||
"/assets/reader/fonts.css",
|
||||
"/assets/reader/themes.css",
|
||||
"/assets/icons/icon512.png",
|
||||
"/assets/images/no-cover.jpg",
|
||||
"/assets/reader/readerThemes.css",
|
||||
|
||||
// Main App Assets
|
||||
"/manifest.json",
|
||||
"/assets/index.js",
|
||||
"/assets/style.css",
|
||||
"/assets/common.js",
|
||||
|
||||
// Library Assets
|
||||
"/assets/lib/platform.js",
|
||||
"/assets/lib/jszip.min.js",
|
||||
"/assets/lib/epub.min.js",
|
||||
"/assets/lib/no-sleep.js",
|
||||
"/assets/lib/idb-keyval.js",
|
||||
"/assets/lib/sw-helper.js",
|
||||
"/assets/lib/no-sleep.min.js",
|
||||
"/assets/lib/idb-keyval.min.js",
|
||||
|
||||
// Fonts
|
||||
"/assets/reader/fonts/arbutus-slab-v16-latin_latin-ext-regular.woff2",
|
||||
"/assets/reader/fonts/lato-v24-latin_latin-ext-100.woff2",
|
||||
"/assets/reader/fonts/lato-v24-latin_latin-ext-100italic.woff2",
|
||||
"/assets/reader/fonts/lato-v24-latin_latin-ext-700.woff2",
|
||||
"/assets/reader/fonts/lato-v24-latin_latin-ext-700italic.woff2",
|
||||
"/assets/reader/fonts/lato-v24-latin_latin-ext-italic.woff2",
|
||||
"/assets/reader/fonts/lato-v24-latin_latin-ext-regular.woff2",
|
||||
"/assets/reader/fonts/open-sans-v36-latin_latin-ext-700.woff2",
|
||||
"/assets/reader/fonts/open-sans-v36-latin_latin-ext-700italic.woff2",
|
||||
"/assets/reader/fonts/open-sans-v36-latin_latin-ext-italic.woff2",
|
||||
"/assets/reader/fonts/open-sans-v36-latin_latin-ext-regular.woff2",
|
||||
];
|
||||
|
||||
// ------------------------------------------------------- //
|
||||
// ----------------------- Helpers ----------------------- //
|
||||
// ------------------------------------------------------- //
|
||||
|
||||
function purgeCache() {
|
||||
async function purgeCache() {
|
||||
console.log("[purgeCache] Purging Cache");
|
||||
return caches.keys().then(function (names) {
|
||||
for (let name of names) caches.delete(name);
|
||||
@@ -119,7 +134,9 @@ async function handleFetch(event) {
|
||||
|
||||
// Find Directive
|
||||
const directive = ROUTES.find(
|
||||
(item) => url.match(item.route) || url == item.route
|
||||
(item) =>
|
||||
(item.route instanceof RegExp && url.match(item.route)) ||
|
||||
url == item.route,
|
||||
) || { type: CACHE_NEVER };
|
||||
|
||||
// Get Fallback
|
||||
@@ -144,11 +161,11 @@ async function handleFetch(event) {
|
||||
);
|
||||
case CACHE_UPDATE_SYNC:
|
||||
return updateCache(event.request).catch(
|
||||
(e) => currentCache || fallbackFunc(event)
|
||||
(e) => currentCache || fallbackFunc(event),
|
||||
);
|
||||
case CACHE_UPDATE_ASYNC:
|
||||
let newResponse = updateCache(event.request).catch((e) =>
|
||||
fallbackFunc(event)
|
||||
fallbackFunc(event),
|
||||
);
|
||||
|
||||
return currentCache || newResponse;
|
||||
@@ -169,12 +186,22 @@ function handleMessage(event) {
|
||||
caches.open(SW_CACHE_NAME).then(async (cache) => {
|
||||
let allKeys = await cache.keys();
|
||||
|
||||
// Get Cached Resources
|
||||
let docResources = allKeys
|
||||
.map((item) => new URL(item.url).pathname)
|
||||
.filter((item) => item.startsWith("/documents/"));
|
||||
.filter(
|
||||
(item) =>
|
||||
item.startsWith("/documents/") ||
|
||||
item.startsWith("/reader/progress/"),
|
||||
);
|
||||
|
||||
// Derive Unique IDs
|
||||
let documentIDs = Array.from(
|
||||
new Set(docResources.map((item) => item.split("/")[2]))
|
||||
new Set(
|
||||
docResources
|
||||
.filter((item) => item.startsWith("/documents/"))
|
||||
.map((item) => item.split("/")[2]),
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -187,27 +214,26 @@ function handleMessage(event) {
|
||||
.filter(
|
||||
(id) =>
|
||||
docResources.includes("/documents/" + id + "/file") &&
|
||||
docResources.includes("/documents/" + id + "/progress")
|
||||
docResources.includes("/reader/progress/" + id),
|
||||
)
|
||||
.map(async (id) => {
|
||||
let url = "/documents/" + id + "/progress";
|
||||
let url = "/reader/progress/" + id;
|
||||
let currentCache = await caches.match(url);
|
||||
let resp = await updateCache(url).catch((e) => currentCache);
|
||||
return resp.json();
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
event.source.postMessage({ id, data: cachedDocuments });
|
||||
});
|
||||
} else if (data.type === DEL_SW_CACHE) {
|
||||
let basePath = "/documents/" + data.id;
|
||||
caches
|
||||
.open(SW_CACHE_NAME)
|
||||
.then((cache) =>
|
||||
Promise.all([
|
||||
cache.delete(basePath + "/file"),
|
||||
cache.delete(basePath + "/progress"),
|
||||
])
|
||||
cache.delete("/documents/" + data.id + "/file"),
|
||||
cache.delete("/reader/progress/" + data.id),
|
||||
]),
|
||||
)
|
||||
.then(() => event.source.postMessage({ id, data: "SUCCESS" }))
|
||||
.catch(() => event.source.postMessage({ id, data: "FAILURE" }));
|
||||
@@ -227,6 +253,13 @@ self.addEventListener("install", function (event) {
|
||||
event.waitUntil(handleInstall(event));
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", (event) =>
|
||||
event.respondWith(handleFetch(event))
|
||||
);
|
||||
self.addEventListener("fetch", (event) => {
|
||||
/**
|
||||
* Weird things happen when a service worker attempts to handle a request
|
||||
* when the server responds with chunked transfer encoding. Right now we only
|
||||
* use chunked encoding on POSTs. So this is to avoid processing those.
|
||||
**/
|
||||
|
||||
if (event.request.method != "GET") return;
|
||||
return event.respondWith(handleFetch(event));
|
||||
});
|
||||
|
||||
BIN
banner.png
Normal file
BIN
banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
BIN
banner.xcf
Normal file
BIN
banner.xcf
Normal file
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
# Book Manager - SyncNinja KOReader Plugin
|
||||
# AnthoLume - SyncNinja KOReader Plugin
|
||||
|
||||
This is BookManagers KOReader Plugin called `syncninja.koplugin`. Features include:
|
||||
This is AnthoLume's KOReader Plugin called `syncninja.koplugin`. Features include:
|
||||
|
||||
- Syncing read activity
|
||||
- Uploading documents
|
||||
@@ -12,10 +12,10 @@ Copy the `syncninja.koplugin` directory to the `plugins` directory for your KORe
|
||||
|
||||
## Configuration
|
||||
|
||||
You must configure the BookManager server and credentials in SyncNinja. Afterwhich you'll have the ability to configure the sync cadence as well as whether you'd like the plugin to sync your activity, document metadata, and/or documents themselves.
|
||||
You must configure the AnthoLume server and credentials in SyncNinja. Afterwhich you'll have the ability to configure the sync cadence as well as whether you'd like the plugin to sync your activity, document metadata, and/or documents themselves.
|
||||
|
||||
## KOSync Compatibility
|
||||
|
||||
BookManager implements API's compatible with the KOSync plugin. This means that you can utilize this server for KOSync (and it's recommended!). SyncNinja provides an easy way to merge configurations between both KOSync and itself in the menu.
|
||||
AnthoLume implements API's compatible with the KOSync plugin. This means that you can utilize this server for KOSync (and it's recommended!). SyncNinja provides an easy way to merge configurations between both KOSync and itself in the menu.
|
||||
|
||||
The KOSync compatible API endpoint is located at: `http(s)://<SERVER>/api/ko`. You can either use the previous mentioned merge feature to automatically configure KOSync once SyncNinja is configured, or you can manually set KOSync's server to the above.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
115
config/config.go
115
config/config.go
@@ -1,8 +1,14 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@@ -22,28 +28,100 @@ type Config struct {
|
||||
RegistrationEnabled bool
|
||||
SearchEnabled bool
|
||||
DemoMode bool
|
||||
LogLevel string
|
||||
|
||||
// Cookie Settings
|
||||
CookieSessionKey string
|
||||
CookieSecure bool
|
||||
CookieHTTPOnly bool
|
||||
CookieAuthKey string
|
||||
CookieEncKey string
|
||||
CookieSecure bool
|
||||
CookieHTTPOnly bool
|
||||
}
|
||||
|
||||
type customFormatter struct {
|
||||
log.Formatter
|
||||
}
|
||||
|
||||
// Force UTC & Set type (app)
|
||||
func (cf customFormatter) Format(e *log.Entry) ([]byte, error) {
|
||||
if e.Data["type"] == nil {
|
||||
e.Data["type"] = "app"
|
||||
}
|
||||
e.Time = e.Time.UTC()
|
||||
return cf.Formatter.Format(e)
|
||||
}
|
||||
|
||||
// Set at runtime
|
||||
var version string = "develop"
|
||||
|
||||
func Load() *Config {
|
||||
return &Config{
|
||||
Version: "0.0.2",
|
||||
DBType: trimLowerString(getEnv("DATABASE_TYPE", "SQLite")),
|
||||
DBName: trimLowerString(getEnv("DATABASE_NAME", "book_manager")),
|
||||
c := &Config{
|
||||
Version: version,
|
||||
ConfigPath: getEnv("CONFIG_PATH", "/config"),
|
||||
DataPath: getEnv("DATA_PATH", "/data"),
|
||||
ListenPort: getEnv("LISTEN_PORT", "8585"),
|
||||
DBType: trimLowerString(getEnv("DATABASE_TYPE", "SQLite")),
|
||||
DBName: trimLowerString(getEnv("DATABASE_NAME", "antholume")),
|
||||
RegistrationEnabled: trimLowerString(getEnv("REGISTRATION_ENABLED", "false")) == "true",
|
||||
DemoMode: trimLowerString(getEnv("DEMO_MODE", "false")) == "true",
|
||||
SearchEnabled: trimLowerString(getEnv("SEARCH_ENABLED", "false")) == "true",
|
||||
CookieSessionKey: trimLowerString(getEnv("COOKIE_SESSION_KEY", "")),
|
||||
CookieAuthKey: trimLowerString(getEnv("COOKIE_AUTH_KEY", "")),
|
||||
CookieEncKey: trimLowerString(getEnv("COOKIE_ENC_KEY", "")),
|
||||
LogLevel: trimLowerString(getEnv("LOG_LEVEL", "info")),
|
||||
CookieSecure: trimLowerString(getEnv("COOKIE_SECURE", "true")) == "true",
|
||||
CookieHTTPOnly: trimLowerString(getEnv("COOKIE_HTTP_ONLY", "true")) == "true",
|
||||
}
|
||||
|
||||
// Parse log level
|
||||
logLevel, err := log.ParseLevel(c.LogLevel)
|
||||
if err != nil {
|
||||
logLevel = log.InfoLevel
|
||||
}
|
||||
|
||||
// Create custom formatter
|
||||
logFormatter := &customFormatter{&log.JSONFormatter{
|
||||
CallerPrettyfier: prettyCaller,
|
||||
}}
|
||||
|
||||
// Create log rotator
|
||||
rotateFileHook, err := NewRotateFileHook(RotateFileConfig{
|
||||
Filename: path.Join(c.ConfigPath, "logs/antholume.log"),
|
||||
MaxSize: 50,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 30,
|
||||
Level: logLevel,
|
||||
Formatter: logFormatter,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal("Unable to initialize file rotate hook")
|
||||
}
|
||||
|
||||
// Rotate now
|
||||
rotateFileHook.Rotate()
|
||||
|
||||
// Set logger settings
|
||||
log.SetLevel(logLevel)
|
||||
log.SetFormatter(logFormatter)
|
||||
log.SetReportCaller(true)
|
||||
log.AddHook(rotateFileHook)
|
||||
|
||||
// Ensure directories exist
|
||||
c.EnsureDirectories()
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Ensures needed directories exist
|
||||
func (c *Config) EnsureDirectories() {
|
||||
os.Mkdir(c.ConfigPath, 0755)
|
||||
os.Mkdir(c.DataPath, 0755)
|
||||
|
||||
docDir := filepath.Join(c.DataPath, "documents")
|
||||
coversDir := filepath.Join(c.DataPath, "covers")
|
||||
backupDir := filepath.Join(c.DataPath, "backups")
|
||||
|
||||
os.Mkdir(docDir, 0755)
|
||||
os.Mkdir(coversDir, 0755)
|
||||
os.Mkdir(backupDir, 0755)
|
||||
}
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
@@ -56,3 +134,24 @@ func getEnv(key, fallback string) string {
|
||||
func trimLowerString(val string) string {
|
||||
return strings.ToLower(strings.TrimSpace(val))
|
||||
}
|
||||
|
||||
func prettyCaller(f *runtime.Frame) (function string, file string) {
|
||||
purgePrefix := "reichard.io/antholume/"
|
||||
|
||||
pathName := strings.Replace(f.Func.Name(), purgePrefix, "", 1)
|
||||
parts := strings.Split(pathName, ".")
|
||||
|
||||
filepath, line := f.Func.FileLine(f.PC)
|
||||
splitFilePath := strings.Split(filepath, "/")
|
||||
|
||||
fileName := fmt.Sprintf("%s/%s@%d", parts[0], splitFilePath[len(splitFilePath)-1], line)
|
||||
functionName := strings.Replace(pathName, parts[0]+".", "", 1)
|
||||
|
||||
// Exclude GIN Logger
|
||||
if functionName == "NewApi.apiLogger.func1" {
|
||||
fileName = ""
|
||||
functionName = ""
|
||||
}
|
||||
|
||||
return functionName, fileName
|
||||
}
|
||||
|
||||
@@ -1,35 +1,37 @@
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLoadConfig(t *testing.T) {
|
||||
conf := Load()
|
||||
want := "sqlite"
|
||||
if conf.DBType != want {
|
||||
t.Fatalf(`Load().DBType = %q, want match for %#q, nil`, conf.DBType, want)
|
||||
}
|
||||
assert.Equal(t, "sqlite", conf.DBType)
|
||||
}
|
||||
|
||||
func TestGetEnvDefault(t *testing.T) {
|
||||
want := "def_val"
|
||||
envDefault := getEnv("DEFAULT_TEST", want)
|
||||
if envDefault != want {
|
||||
t.Fatalf(`getEnv("DEFAULT_TEST", "def_val") = %q, want match for %#q, nil`, envDefault, want)
|
||||
}
|
||||
}
|
||||
desiredValue := "def_val"
|
||||
envDefault := getEnv("DEFAULT_TEST", desiredValue)
|
||||
|
||||
func TestGetEnvSet(t *testing.T) {
|
||||
envDefault := getEnv("SET_TEST", "not_this")
|
||||
want := "set_val"
|
||||
if envDefault != want {
|
||||
t.Fatalf(`getEnv("SET_TEST", "not_this") = %q, want match for %#q, nil`, envDefault, want)
|
||||
}
|
||||
assert.Equal(t, desiredValue, envDefault)
|
||||
}
|
||||
|
||||
func TestTrimLowerString(t *testing.T) {
|
||||
want := "trimtest"
|
||||
output := trimLowerString(" trimTest ")
|
||||
if output != want {
|
||||
t.Fatalf(`trimLowerString(" trimTest ") = %q, want match for %#q, nil`, output, want)
|
||||
}
|
||||
desiredValue := "trimtest"
|
||||
outputValue := trimLowerString(" trimTest ")
|
||||
|
||||
assert.Equal(t, desiredValue, outputValue)
|
||||
}
|
||||
|
||||
func TestPrettyCaller(t *testing.T) {
|
||||
p, _, _, _ := runtime.Caller(0)
|
||||
result := runtime.CallersFrames([]uintptr{p})
|
||||
f, _ := result.Next()
|
||||
functionName, fileName := prettyCaller(&f)
|
||||
|
||||
assert.Equal(t, "TestPrettyCaller", functionName, "should have current function name")
|
||||
assert.Equal(t, "config/config_test.go@30", fileName, "should have current file path and line number")
|
||||
}
|
||||
|
||||
54
config/logger.go
Normal file
54
config/logger.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
// Modified "snowzach/rotatefilehook" to support manual rotation
|
||||
|
||||
type RotateFileConfig struct {
|
||||
Filename string
|
||||
MaxSize int
|
||||
MaxBackups int
|
||||
MaxAge int
|
||||
Compress bool
|
||||
Level logrus.Level
|
||||
Formatter logrus.Formatter
|
||||
}
|
||||
|
||||
type RotateFileHook struct {
|
||||
Config RotateFileConfig
|
||||
logWriter *lumberjack.Logger
|
||||
}
|
||||
|
||||
func NewRotateFileHook(config RotateFileConfig) (*RotateFileHook, error) {
|
||||
hook := RotateFileHook{
|
||||
Config: config,
|
||||
}
|
||||
hook.logWriter = &lumberjack.Logger{
|
||||
Filename: config.Filename,
|
||||
MaxSize: config.MaxSize,
|
||||
MaxBackups: config.MaxBackups,
|
||||
MaxAge: config.MaxAge,
|
||||
Compress: config.Compress,
|
||||
}
|
||||
return &hook, nil
|
||||
}
|
||||
|
||||
func (hook *RotateFileHook) Rotate() error {
|
||||
return hook.logWriter.Rotate()
|
||||
}
|
||||
|
||||
func (hook *RotateFileHook) Levels() []logrus.Level {
|
||||
return logrus.AllLevels[:hook.Config.Level+1]
|
||||
}
|
||||
|
||||
func (hook *RotateFileHook) Fire(entry *logrus.Entry) (err error) {
|
||||
b, err := hook.Config.Formatter.Format(entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hook.logWriter.Write(b)
|
||||
return nil
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.21.0
|
||||
// sqlc v1.27.0
|
||||
|
||||
package database
|
||||
|
||||
|
||||
151
database/document_user_statistics.sql
Normal file
151
database/document_user_statistics.sql
Normal file
@@ -0,0 +1,151 @@
|
||||
WITH grouped_activity AS (
|
||||
SELECT
|
||||
ga.user_id,
|
||||
ga.document_id,
|
||||
MAX(ga.created_at) AS created_at,
|
||||
MAX(ga.start_time) AS start_time,
|
||||
MIN(ga.start_percentage) AS start_percentage,
|
||||
MAX(ga.end_percentage) AS end_percentage,
|
||||
|
||||
-- Total Duration & Percentage
|
||||
SUM(ga.duration) AS total_time_seconds,
|
||||
SUM(ga.end_percentage - ga.start_percentage) AS total_read_percentage,
|
||||
|
||||
-- Yearly Duration
|
||||
SUM(
|
||||
CASE
|
||||
WHEN
|
||||
ga.start_time >= DATE('now', '-1 year')
|
||||
THEN ga.duration
|
||||
ELSE 0
|
||||
END
|
||||
)
|
||||
AS yearly_time_seconds,
|
||||
|
||||
-- Yearly Percentage
|
||||
SUM(
|
||||
CASE
|
||||
WHEN
|
||||
ga.start_time >= DATE('now', '-1 year')
|
||||
THEN ga.end_percentage - ga.start_percentage
|
||||
ELSE 0
|
||||
END
|
||||
)
|
||||
AS yearly_read_percentage,
|
||||
|
||||
-- Monthly Duration
|
||||
SUM(
|
||||
CASE
|
||||
WHEN
|
||||
ga.start_time >= DATE('now', '-1 month')
|
||||
THEN ga.duration
|
||||
ELSE 0
|
||||
END
|
||||
)
|
||||
AS monthly_time_seconds,
|
||||
|
||||
-- Monthly Percentage
|
||||
SUM(
|
||||
CASE
|
||||
WHEN
|
||||
ga.start_time >= DATE('now', '-1 month')
|
||||
THEN ga.end_percentage - ga.start_percentage
|
||||
ELSE 0
|
||||
END
|
||||
)
|
||||
AS monthly_read_percentage,
|
||||
|
||||
-- Weekly Duration
|
||||
SUM(
|
||||
CASE
|
||||
WHEN
|
||||
ga.start_time >= DATE('now', '-7 days')
|
||||
THEN ga.duration
|
||||
ELSE 0
|
||||
END
|
||||
)
|
||||
AS weekly_time_seconds,
|
||||
|
||||
-- Weekly Percentage
|
||||
SUM(
|
||||
CASE
|
||||
WHEN
|
||||
ga.start_time >= DATE('now', '-7 days')
|
||||
THEN ga.end_percentage - ga.start_percentage
|
||||
ELSE 0
|
||||
END
|
||||
)
|
||||
AS weekly_read_percentage
|
||||
|
||||
FROM activity AS ga
|
||||
GROUP BY ga.user_id, ga.document_id
|
||||
),
|
||||
|
||||
current_progress AS (
|
||||
SELECT
|
||||
user_id,
|
||||
document_id,
|
||||
COALESCE((
|
||||
SELECT dp.percentage
|
||||
FROM document_progress AS dp
|
||||
WHERE
|
||||
dp.user_id = iga.user_id
|
||||
AND dp.document_id = iga.document_id
|
||||
ORDER BY dp.created_at DESC
|
||||
LIMIT 1
|
||||
), end_percentage) AS percentage
|
||||
FROM grouped_activity AS iga
|
||||
)
|
||||
|
||||
INSERT INTO document_user_statistics
|
||||
SELECT
|
||||
ga.document_id,
|
||||
ga.user_id,
|
||||
cp.percentage,
|
||||
MAX(ga.start_time) AS last_read,
|
||||
MAX(ga.created_at) AS last_seen,
|
||||
SUM(ga.total_read_percentage) AS read_percentage,
|
||||
|
||||
-- All Time WPM
|
||||
SUM(ga.total_time_seconds) AS total_time_seconds,
|
||||
(CAST(COALESCE(d.words, 0.0) AS REAL) * SUM(ga.total_read_percentage))
|
||||
AS total_words_read,
|
||||
(CAST(COALESCE(d.words, 0.0) AS REAL) * SUM(ga.total_read_percentage))
|
||||
/ (SUM(ga.total_time_seconds) / 60.0) AS total_wpm,
|
||||
|
||||
-- Yearly WPM
|
||||
ga.yearly_time_seconds,
|
||||
CAST(COALESCE(d.words, 0.0) AS REAL) * ga.yearly_read_percentage
|
||||
AS yearly_words_read,
|
||||
COALESCE(
|
||||
(CAST(COALESCE(d.words, 0.0) AS REAL) * ga.yearly_read_percentage)
|
||||
/ (ga.yearly_time_seconds / 60), 0.0)
|
||||
AS yearly_wpm,
|
||||
|
||||
-- Monthly WPM
|
||||
ga.monthly_time_seconds,
|
||||
CAST(COALESCE(d.words, 0.0) AS REAL) * ga.monthly_read_percentage
|
||||
AS monthly_words_read,
|
||||
COALESCE(
|
||||
(CAST(COALESCE(d.words, 0.0) AS REAL) * ga.monthly_read_percentage)
|
||||
/ (ga.monthly_time_seconds / 60), 0.0)
|
||||
AS monthly_wpm,
|
||||
|
||||
-- Weekly WPM
|
||||
ga.weekly_time_seconds,
|
||||
CAST(COALESCE(d.words, 0.0) AS REAL) * ga.weekly_read_percentage
|
||||
AS weekly_words_read,
|
||||
COALESCE(
|
||||
(CAST(COALESCE(d.words, 0.0) AS REAL) * ga.weekly_read_percentage)
|
||||
/ (ga.weekly_time_seconds / 60), 0.0)
|
||||
AS weekly_wpm
|
||||
|
||||
FROM grouped_activity AS ga
|
||||
INNER JOIN
|
||||
current_progress AS cp
|
||||
ON ga.user_id = cp.user_id AND ga.document_id = cp.document_id
|
||||
INNER JOIN
|
||||
documents AS d
|
||||
ON ga.document_id = d.id
|
||||
GROUP BY ga.document_id, ga.user_id
|
||||
ORDER BY total_wpm DESC;
|
||||
114
database/documents_test.go
Normal file
114
database/documents_test.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"reichard.io/antholume/config"
|
||||
)
|
||||
|
||||
type DocumentsTestSuite struct {
|
||||
suite.Suite
|
||||
dbm *DBManager
|
||||
}
|
||||
|
||||
func TestDocuments(t *testing.T) {
|
||||
suite.Run(t, new(DocumentsTestSuite))
|
||||
}
|
||||
|
||||
func (suite *DocumentsTestSuite) SetupTest() {
|
||||
cfg := config.Config{
|
||||
DBType: "memory",
|
||||
}
|
||||
|
||||
suite.dbm = NewMgr(&cfg)
|
||||
|
||||
// Create Document
|
||||
_, err := suite.dbm.Queries.UpsertDocument(suite.dbm.Ctx, UpsertDocumentParams{
|
||||
ID: documentID,
|
||||
Title: &documentTitle,
|
||||
Author: &documentAuthor,
|
||||
Words: &documentWords,
|
||||
})
|
||||
suite.NoError(err)
|
||||
}
|
||||
|
||||
// DOCUMENT - TODO:
|
||||
// - (q *Queries) GetDocumentProgress
|
||||
// - (q *Queries) GetDocumentWithStats
|
||||
// - (q *Queries) GetDocumentsSize
|
||||
// - (q *Queries) GetDocumentsWithStats
|
||||
// - (q *Queries) GetMissingDocuments
|
||||
func (suite *DocumentsTestSuite) TestGetDocument() {
|
||||
doc, err := suite.dbm.Queries.GetDocument(suite.dbm.Ctx, documentID)
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Equal(documentID, doc.ID, "should have changed the document")
|
||||
}
|
||||
|
||||
func (suite *DocumentsTestSuite) TestUpsertDocument() {
|
||||
testDocID := "docid1"
|
||||
|
||||
doc, err := suite.dbm.Queries.UpsertDocument(suite.dbm.Ctx, UpsertDocumentParams{
|
||||
ID: testDocID,
|
||||
Title: &documentTitle,
|
||||
Author: &documentAuthor,
|
||||
})
|
||||
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Equal(testDocID, doc.ID, "should have document id")
|
||||
suite.Equal(documentTitle, *doc.Title, "should have document title")
|
||||
suite.Equal(documentAuthor, *doc.Author, "should have document author")
|
||||
}
|
||||
|
||||
func (suite *DocumentsTestSuite) TestDeleteDocument() {
|
||||
changed, err := suite.dbm.Queries.DeleteDocument(suite.dbm.Ctx, documentID)
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Equal(int64(1), changed, "should have changed the document")
|
||||
|
||||
doc, err := suite.dbm.Queries.GetDocument(suite.dbm.Ctx, documentID)
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.True(doc.Deleted, "should have deleted the document")
|
||||
}
|
||||
|
||||
func (suite *DocumentsTestSuite) TestGetDeletedDocuments() {
|
||||
changed, err := suite.dbm.Queries.DeleteDocument(suite.dbm.Ctx, documentID)
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Equal(int64(1), changed, "should have changed the document")
|
||||
|
||||
deletedDocs, err := suite.dbm.Queries.GetDeletedDocuments(suite.dbm.Ctx, []string{documentID})
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Len(deletedDocs, 1, "should have one deleted document")
|
||||
}
|
||||
|
||||
// TODO - Convert GetWantedDocuments -> (sqlc.slice('document_ids'));
|
||||
func (suite *DocumentsTestSuite) TestGetWantedDocuments() {
|
||||
wantedDocs, err := suite.dbm.Queries.GetWantedDocuments(suite.dbm.Ctx, fmt.Sprintf("[\"%s\"]", documentID))
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Len(wantedDocs, 1, "should have one wanted document")
|
||||
}
|
||||
|
||||
func (suite *DocumentsTestSuite) TestGetMissingDocuments() {
|
||||
// Create Document
|
||||
_, err := suite.dbm.Queries.UpsertDocument(suite.dbm.Ctx, UpsertDocumentParams{
|
||||
ID: documentID,
|
||||
Filepath: &documentFilepath,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
missingDocs, err := suite.dbm.Queries.GetMissingDocuments(suite.dbm.Ctx, []string{documentID})
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Len(missingDocs, 0, "should have no wanted document")
|
||||
|
||||
missingDocs, err = suite.dbm.Queries.GetMissingDocuments(suite.dbm.Ctx, []string{"other"})
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Len(missingDocs, 1, "should have one missing document")
|
||||
suite.Equal(documentID, missingDocs[0].ID, "should have missing doc")
|
||||
|
||||
// TODO - https://github.com/sqlc-dev/sqlc/issues/3451
|
||||
// missingDocs, err = suite.dbm.Queries.GetMissingDocuments(suite.dbm.Ctx, []string{})
|
||||
// suite.Nil(err, "should have nil err")
|
||||
// suite.Len(missingDocs, 1, "should have one missing document")
|
||||
// suite.Equal(documentID, missingDocs[0].ID, "should have missing doc")
|
||||
}
|
||||
@@ -3,62 +3,261 @@ package database
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"embed"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/pressly/goose/v3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
_ "modernc.org/sqlite"
|
||||
"path"
|
||||
"reichard.io/bbank/config"
|
||||
sqlite "modernc.org/sqlite"
|
||||
"reichard.io/antholume/config"
|
||||
_ "reichard.io/antholume/database/migrations"
|
||||
)
|
||||
|
||||
type DBManager struct {
|
||||
DB *sql.DB
|
||||
Ctx context.Context
|
||||
Queries *Queries
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
//go:embed schema.sql
|
||||
var ddl string
|
||||
|
||||
//go:embed update_temp_tables.sql
|
||||
var tsql string
|
||||
//go:embed user_streaks.sql
|
||||
var user_streaks string
|
||||
|
||||
//go:embed document_user_statistics.sql
|
||||
var document_user_statistics string
|
||||
|
||||
//go:embed migrations/*
|
||||
var migrations embed.FS
|
||||
|
||||
// Register scalar sqlite function on init
|
||||
func init() {
|
||||
sqlite.MustRegisterFunction("LOCAL_TIME", &sqlite.FunctionImpl{
|
||||
NArgs: 2,
|
||||
Deterministic: true,
|
||||
Scalar: localTime,
|
||||
})
|
||||
sqlite.MustRegisterFunction("LOCAL_DATE", &sqlite.FunctionImpl{
|
||||
NArgs: 2,
|
||||
Deterministic: true,
|
||||
Scalar: localDate,
|
||||
})
|
||||
}
|
||||
|
||||
// NewMgr Returns an initialized manager
|
||||
func NewMgr(c *config.Config) *DBManager {
|
||||
// Create Manager
|
||||
dbm := &DBManager{
|
||||
Ctx: context.Background(),
|
||||
cfg: c,
|
||||
}
|
||||
|
||||
// Create Database
|
||||
if c.DBType == "sqlite" || c.DBType == "memory" {
|
||||
var dbLocation string = ":memory:"
|
||||
if c.DBType == "sqlite" {
|
||||
dbLocation = path.Join(c.ConfigPath, fmt.Sprintf("%s.db", c.DBName))
|
||||
}
|
||||
|
||||
var err error
|
||||
dbm.DB, err = sql.Open("sqlite", dbLocation)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Single Open Connection
|
||||
dbm.DB.SetMaxOpenConns(1)
|
||||
if _, err := dbm.DB.Exec(ddl, nil); err != nil {
|
||||
log.Info("Exec Error:", err)
|
||||
}
|
||||
} else {
|
||||
log.Fatal("Unsupported Database")
|
||||
if err := dbm.init(); err != nil {
|
||||
log.Panic("Unable to init DB")
|
||||
}
|
||||
|
||||
dbm.Queries = New(dbm.DB)
|
||||
|
||||
return dbm
|
||||
}
|
||||
|
||||
func (dbm *DBManager) CacheTempTables() error {
|
||||
if _, err := dbm.DB.ExecContext(dbm.Ctx, tsql); err != nil {
|
||||
// init loads the DB manager
|
||||
func (dbm *DBManager) init() error {
|
||||
// Build DB Location
|
||||
var dbLocation string
|
||||
switch dbm.cfg.DBType {
|
||||
case "sqlite":
|
||||
dbLocation = filepath.Join(dbm.cfg.ConfigPath, fmt.Sprintf("%s.db", dbm.cfg.DBName))
|
||||
case "memory":
|
||||
dbLocation = ":memory:"
|
||||
default:
|
||||
return fmt.Errorf("unsupported database")
|
||||
}
|
||||
|
||||
var err error
|
||||
dbm.DB, err = sql.Open("sqlite", dbLocation)
|
||||
if err != nil {
|
||||
log.Panicf("Unable to open DB: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Single open connection
|
||||
dbm.DB.SetMaxOpenConns(1)
|
||||
|
||||
// Check if DB is new
|
||||
isNew, err := isEmpty(dbm.DB)
|
||||
if err != nil {
|
||||
log.Panicf("Unable to determine db info: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Init SQLc
|
||||
dbm.Queries = New(dbm.DB)
|
||||
|
||||
// Execute schema
|
||||
if _, err := dbm.DB.Exec(ddl, nil); err != nil {
|
||||
log.Panicf("Error executing schema: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Perform migrations
|
||||
err = dbm.performMigrations(isNew)
|
||||
if err != nil && err != goose.ErrNoMigrationFiles {
|
||||
log.Panicf("Error running DB migrations: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Update settings
|
||||
err = dbm.updateSettings()
|
||||
if err != nil {
|
||||
log.Panicf("Error running DB settings update: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Cache tables
|
||||
if err := dbm.CacheTempTables(); err != nil {
|
||||
log.Warn("Refreshing temp table cache failed: ", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reload closes the DB & reinits
|
||||
func (dbm *DBManager) Reload() error {
|
||||
// Close handle
|
||||
err := dbm.DB.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Reinit DB
|
||||
if err := dbm.init(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CacheTempTables clears existing statistics and recalculates
|
||||
func (dbm *DBManager) CacheTempTables() error {
|
||||
start := time.Now()
|
||||
if _, err := dbm.DB.ExecContext(dbm.Ctx, user_streaks); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debug("Cached 'user_streaks' in: ", time.Since(start))
|
||||
|
||||
start = time.Now()
|
||||
if _, err := dbm.DB.ExecContext(dbm.Ctx, document_user_statistics); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debug("Cached 'document_user_statistics' in: ", time.Since(start))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateSettings ensures that we're enforcing foreign keys and enable journal
|
||||
// mode.
|
||||
func (dbm *DBManager) updateSettings() error {
|
||||
// Set SQLite PRAGMA Settings
|
||||
pragmaQuery := `
|
||||
PRAGMA foreign_keys = ON;
|
||||
PRAGMA journal_mode = WAL;
|
||||
`
|
||||
if _, err := dbm.DB.Exec(pragmaQuery, nil); err != nil {
|
||||
log.Errorf("Error executing pragma: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Update Antholume Version in DB
|
||||
if _, err := dbm.Queries.UpdateSettings(dbm.Ctx, UpdateSettingsParams{
|
||||
Name: "version",
|
||||
Value: dbm.cfg.Version,
|
||||
}); err != nil {
|
||||
log.Errorf("Error updating DB settings: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// performMigrations runs all migrations
|
||||
func (dbm *DBManager) performMigrations(isNew bool) error {
|
||||
// Create context
|
||||
ctx := context.WithValue(context.Background(), "isNew", isNew) // nolint
|
||||
|
||||
// Set DB migration
|
||||
goose.SetBaseFS(migrations)
|
||||
|
||||
// Run migrations
|
||||
goose.SetLogger(log.StandardLogger())
|
||||
if err := goose.SetDialect("sqlite"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return goose.UpContext(ctx, dbm.DB, "migrations")
|
||||
}
|
||||
|
||||
// isEmpty determines whether the database is empty
|
||||
func isEmpty(db *sql.DB) (bool, error) {
|
||||
var tableCount int
|
||||
err := db.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type='table';").Scan(&tableCount)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return tableCount == 0, nil
|
||||
}
|
||||
|
||||
// localTime is a custom SQL function that is registered as LOCAL_TIME in the init function
|
||||
func localTime(ctx *sqlite.FunctionContext, args []driver.Value) (driver.Value, error) {
|
||||
timeStr, ok := args[0].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("both arguments to TZTime must be strings")
|
||||
}
|
||||
|
||||
timeZoneStr, ok := args[1].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("both arguments to TZTime must be strings")
|
||||
}
|
||||
|
||||
timeZone, err := time.LoadLocation(timeZoneStr)
|
||||
if err != nil {
|
||||
return nil, errors.New("unable to parse timezone")
|
||||
}
|
||||
|
||||
formattedTime, err := time.ParseInLocation(time.RFC3339, timeStr, time.UTC)
|
||||
if err != nil {
|
||||
return nil, errors.New("unable to parse time")
|
||||
}
|
||||
|
||||
return formattedTime.In(timeZone).Format(time.RFC3339), nil
|
||||
}
|
||||
|
||||
// localDate is a custom SQL function that is registered as LOCAL_DATE in the init function
|
||||
func localDate(ctx *sqlite.FunctionContext, args []driver.Value) (driver.Value, error) {
|
||||
timeStr, ok := args[0].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("both arguments to TZTime must be strings")
|
||||
}
|
||||
|
||||
timeZoneStr, ok := args[1].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("both arguments to TZTime must be strings")
|
||||
}
|
||||
|
||||
timeZone, err := time.LoadLocation(timeZoneStr)
|
||||
if err != nil {
|
||||
return nil, errors.New("unable to parse timezone")
|
||||
}
|
||||
|
||||
formattedTime, err := time.ParseInLocation(time.RFC3339, timeStr, time.UTC)
|
||||
if err != nil {
|
||||
return nil, errors.New("unable to parse time")
|
||||
}
|
||||
|
||||
return formattedTime.In(timeZone).Format("2006-01-02"), nil
|
||||
}
|
||||
|
||||
@@ -1,213 +1,170 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reichard.io/bbank/config"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"reichard.io/antholume/config"
|
||||
"reichard.io/antholume/utils"
|
||||
)
|
||||
|
||||
type databaseTest struct {
|
||||
*testing.T
|
||||
var (
|
||||
userID string = "testUser"
|
||||
userPass string = "testPass"
|
||||
deviceID string = "testDevice"
|
||||
deviceName string = "testDeviceName"
|
||||
documentID string = "testDocument"
|
||||
documentTitle string = "testTitle"
|
||||
documentAuthor string = "testAuthor"
|
||||
documentFilepath string = "./testPath.epub"
|
||||
documentWords int64 = 5000
|
||||
)
|
||||
|
||||
type DatabaseTestSuite struct {
|
||||
suite.Suite
|
||||
dbm *DBManager
|
||||
}
|
||||
|
||||
var userID string = "testUser"
|
||||
var userPass string = "testPass"
|
||||
var deviceID string = "testDevice"
|
||||
var deviceName string = "testDeviceName"
|
||||
var documentID string = "testDocument"
|
||||
var documentTitle string = "testTitle"
|
||||
var documentAuthor string = "testAuthor"
|
||||
func TestDatabase(t *testing.T) {
|
||||
suite.Run(t, new(DatabaseTestSuite))
|
||||
}
|
||||
|
||||
func TestNewMgr(t *testing.T) {
|
||||
// PROGRESS - TODO:
|
||||
// - (q *Queries) GetProgress
|
||||
// - (q *Queries) UpdateProgress
|
||||
|
||||
func (suite *DatabaseTestSuite) SetupTest() {
|
||||
cfg := config.Config{
|
||||
DBType: "memory",
|
||||
}
|
||||
|
||||
dbm := NewMgr(&cfg)
|
||||
if dbm == nil {
|
||||
t.Fatalf(`Expected: *DBManager, Got: nil`)
|
||||
suite.dbm = NewMgr(&cfg)
|
||||
|
||||
// Create User
|
||||
rawAuthHash, _ := utils.GenerateToken(64)
|
||||
authHash := fmt.Sprintf("%x", rawAuthHash)
|
||||
_, err := suite.dbm.Queries.CreateUser(suite.dbm.Ctx, CreateUserParams{
|
||||
ID: userID,
|
||||
Pass: &userPass,
|
||||
AuthHash: &authHash,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
// Create Document
|
||||
_, err = suite.dbm.Queries.UpsertDocument(suite.dbm.Ctx, UpsertDocumentParams{
|
||||
ID: documentID,
|
||||
Title: &documentTitle,
|
||||
Author: &documentAuthor,
|
||||
Filepath: &documentFilepath,
|
||||
Words: &documentWords,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
// Create Device
|
||||
_, err = suite.dbm.Queries.UpsertDevice(suite.dbm.Ctx, UpsertDeviceParams{
|
||||
ID: deviceID,
|
||||
UserID: userID,
|
||||
DeviceName: deviceName,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
// Create Activity
|
||||
end := time.Now()
|
||||
start := end.AddDate(0, 0, -9)
|
||||
var counter int64 = 0
|
||||
|
||||
for d := start; d.After(end) == false; d = d.AddDate(0, 0, 1) {
|
||||
counter += 1
|
||||
|
||||
// Add Item
|
||||
activity, err := suite.dbm.Queries.AddActivity(suite.dbm.Ctx, AddActivityParams{
|
||||
DocumentID: documentID,
|
||||
DeviceID: deviceID,
|
||||
UserID: userID,
|
||||
StartTime: d.UTC().Format(time.RFC3339),
|
||||
Duration: 60,
|
||||
StartPercentage: float64(counter) / 100.0,
|
||||
EndPercentage: float64(counter+1) / 100.0,
|
||||
})
|
||||
|
||||
suite.Nil(err, fmt.Sprintf("[%d] should have nil err for add activity", counter))
|
||||
suite.Equal(counter, activity.ID, fmt.Sprintf("[%d] should have correct id for add activity", counter))
|
||||
}
|
||||
|
||||
t.Run("Database", func(t *testing.T) {
|
||||
dt := databaseTest{t, dbm}
|
||||
dt.TestUser()
|
||||
dt.TestDocument()
|
||||
dt.TestDevice()
|
||||
dt.TestActivity()
|
||||
dt.TestDailyReadStats()
|
||||
})
|
||||
// Initiate Cache
|
||||
err = suite.dbm.CacheTempTables()
|
||||
suite.NoError(err)
|
||||
}
|
||||
|
||||
func (dt *databaseTest) TestUser() {
|
||||
dt.Run("User", func(t *testing.T) {
|
||||
changed, err := dt.dbm.Queries.CreateUser(dt.dbm.Ctx, CreateUserParams{
|
||||
ID: userID,
|
||||
Pass: &userPass,
|
||||
})
|
||||
|
||||
if err != nil || changed != 1 {
|
||||
t.Fatalf(`Expected: %v, Got: %v, Error: %v`, 1, changed, err)
|
||||
}
|
||||
|
||||
user, err := dt.dbm.Queries.GetUser(dt.dbm.Ctx, userID)
|
||||
if err != nil || *user.Pass != userPass {
|
||||
t.Fatalf(`Expected: %v, Got: %v, Error: %v`, userPass, *user.Pass, err)
|
||||
}
|
||||
// DEVICES - TODO:
|
||||
// - (q *Queries) GetDevice
|
||||
// - (q *Queries) GetDevices
|
||||
// - (q *Queries) UpsertDevice
|
||||
func (suite *DatabaseTestSuite) TestDevice() {
|
||||
testDevice := "dev123"
|
||||
device, err := suite.dbm.Queries.UpsertDevice(suite.dbm.Ctx, UpsertDeviceParams{
|
||||
ID: testDevice,
|
||||
UserID: userID,
|
||||
DeviceName: deviceName,
|
||||
})
|
||||
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Equal(testDevice, device.ID, "should have device id")
|
||||
suite.Equal(userID, device.UserID, "should have user id")
|
||||
suite.Equal(deviceName, device.DeviceName, "should have device name")
|
||||
}
|
||||
|
||||
func (dt *databaseTest) TestDocument() {
|
||||
dt.Run("Document", func(t *testing.T) {
|
||||
doc, err := dt.dbm.Queries.UpsertDocument(dt.dbm.Ctx, UpsertDocumentParams{
|
||||
ID: documentID,
|
||||
Title: &documentTitle,
|
||||
Author: &documentAuthor,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf(`Expected: Document, Got: %v, Error: %v`, doc, err)
|
||||
}
|
||||
|
||||
if doc.ID != documentID {
|
||||
t.Fatalf(`Expected: %v, Got: %v`, documentID, doc.ID)
|
||||
}
|
||||
|
||||
if *doc.Title != documentTitle {
|
||||
t.Fatalf(`Expected: %v, Got: %v`, documentTitle, *doc.Title)
|
||||
}
|
||||
|
||||
if *doc.Author != documentAuthor {
|
||||
t.Fatalf(`Expected: %v, Got: %v`, documentAuthor, *doc.Author)
|
||||
}
|
||||
// ACTIVITY - TODO:
|
||||
// - (q *Queries) AddActivity
|
||||
// - (q *Queries) GetActivity
|
||||
// - (q *Queries) GetLastActivity
|
||||
func (suite *DatabaseTestSuite) TestActivity() {
|
||||
// Validate Exists
|
||||
existsRows, err := suite.dbm.Queries.GetActivity(suite.dbm.Ctx, GetActivityParams{
|
||||
UserID: userID,
|
||||
Offset: 0,
|
||||
Limit: 50,
|
||||
})
|
||||
|
||||
suite.Nil(err, "should have nil err for get activity")
|
||||
suite.Len(existsRows, 10, "should have correct number of rows get activity")
|
||||
|
||||
// Validate Doesn't Exist
|
||||
doesntExistsRows, err := suite.dbm.Queries.GetActivity(suite.dbm.Ctx, GetActivityParams{
|
||||
UserID: userID,
|
||||
DocumentID: "unknownDoc",
|
||||
DocFilter: true,
|
||||
Offset: 0,
|
||||
Limit: 50,
|
||||
})
|
||||
|
||||
suite.Nil(err, "should have nil err for get activity")
|
||||
suite.Len(doesntExistsRows, 0, "should have no rows")
|
||||
}
|
||||
|
||||
func (dt *databaseTest) TestDevice() {
|
||||
dt.Run("Device", func(t *testing.T) {
|
||||
device, err := dt.dbm.Queries.UpsertDevice(dt.dbm.Ctx, UpsertDeviceParams{
|
||||
ID: deviceID,
|
||||
UserID: userID,
|
||||
DeviceName: deviceName,
|
||||
})
|
||||
// MISC - TODO:
|
||||
// - (q *Queries) AddMetadata
|
||||
// - (q *Queries) GetDailyReadStats
|
||||
// - (q *Queries) GetDatabaseInfo
|
||||
// - (q *Queries) UpdateSettings
|
||||
func (suite *DatabaseTestSuite) TestGetDailyReadStats() {
|
||||
readStats, err := suite.dbm.Queries.GetDailyReadStats(suite.dbm.Ctx, userID)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf(`Expected: Device, Got: %v, Error: %v`, device, err)
|
||||
}
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Len(readStats, 30, "should have length of 30")
|
||||
|
||||
if device.ID != deviceID {
|
||||
t.Fatalf(`Expected: %v, Got: %v`, deviceID, device.ID)
|
||||
}
|
||||
// Validate 1 Minute / Day - Last 10 Days
|
||||
for i := 0; i < 10; i++ {
|
||||
stat := readStats[i]
|
||||
suite.Equal(int64(1), stat.MinutesRead, "should have one minute read")
|
||||
}
|
||||
|
||||
if device.UserID != userID {
|
||||
t.Fatalf(`Expected: %v, Got: %v`, userID, device.UserID)
|
||||
}
|
||||
|
||||
if device.DeviceName != deviceName {
|
||||
t.Fatalf(`Expected: %v, Got: %v`, deviceName, device.DeviceName)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (dt *databaseTest) TestActivity() {
|
||||
dt.Run("Progress", func(t *testing.T) {
|
||||
// 10 Activities, 10 Days
|
||||
end := time.Now()
|
||||
start := end.AddDate(0, 0, -9)
|
||||
var counter int64 = 0
|
||||
|
||||
for d := start; d.After(end) == false; d = d.AddDate(0, 0, 1) {
|
||||
counter += 1
|
||||
|
||||
// Add Item
|
||||
activity, err := dt.dbm.Queries.AddActivity(dt.dbm.Ctx, AddActivityParams{
|
||||
DocumentID: documentID,
|
||||
DeviceID: deviceID,
|
||||
UserID: userID,
|
||||
StartTime: d.UTC().Format(time.RFC3339),
|
||||
Duration: 60,
|
||||
Page: counter,
|
||||
Pages: 100,
|
||||
})
|
||||
|
||||
// Validate No Error
|
||||
if err != nil {
|
||||
t.Fatalf(`expected: rawactivity, got: %v, error: %v`, activity, err)
|
||||
}
|
||||
|
||||
// Validate Auto Increment Working
|
||||
if activity.ID != counter {
|
||||
t.Fatalf(`Expected: %v, Got: %v`, counter, activity.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// Initiate Cache
|
||||
if err := dt.dbm.CacheTempTables(); err != nil {
|
||||
t.Fatalf(`Error: %v`, err)
|
||||
}
|
||||
|
||||
// Validate Exists
|
||||
existsRows, err := dt.dbm.Queries.GetActivity(dt.dbm.Ctx, GetActivityParams{
|
||||
UserID: userID,
|
||||
Offset: 0,
|
||||
Limit: 50,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf(`Expected: []GetActivityRow, Got: %v, Error: %v`, existsRows, err)
|
||||
}
|
||||
|
||||
if len(existsRows) != 10 {
|
||||
t.Fatalf(`Expected: %v, Got: %v`, 10, len(existsRows))
|
||||
}
|
||||
|
||||
// Validate Doesn't Exist
|
||||
doesntExistsRows, err := dt.dbm.Queries.GetActivity(dt.dbm.Ctx, GetActivityParams{
|
||||
UserID: userID,
|
||||
DocumentID: "unknownDoc",
|
||||
DocFilter: true,
|
||||
Offset: 0,
|
||||
Limit: 50,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf(`Expected: []GetActivityRow, Got: %v, Error: %v`, doesntExistsRows, err)
|
||||
}
|
||||
|
||||
if len(doesntExistsRows) != 0 {
|
||||
t.Fatalf(`Expected: %v, Got: %v`, 0, len(doesntExistsRows))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (dt *databaseTest) TestDailyReadStats() {
|
||||
dt.Run("DailyReadStats", func(t *testing.T) {
|
||||
readStats, err := dt.dbm.Queries.GetDailyReadStats(dt.dbm.Ctx, userID)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf(`Expected: []GetDailyReadStatsRow, Got: %v, Error: %v`, readStats, err)
|
||||
}
|
||||
|
||||
// Validate 30 Days Stats
|
||||
if len(readStats) != 30 {
|
||||
t.Fatalf(`Expected: %v, Got: %v`, 30, len(readStats))
|
||||
}
|
||||
|
||||
// Validate 1 Minute / Day - Last 10 Days
|
||||
for i := 0; i < 10; i++ {
|
||||
stat := readStats[i]
|
||||
if stat.MinutesRead != 1 {
|
||||
t.Fatalf(`Day: %v, Expected: %v, Got: %v`, stat.Date, 1, stat.MinutesRead)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate 0 Minute / Day - Remaining 20 Days
|
||||
for i := 10; i < 30; i++ {
|
||||
stat := readStats[i]
|
||||
if stat.MinutesRead != 0 {
|
||||
t.Fatalf(`Day: %v, Expected: %v, Got: %v`, stat.Date, 0, stat.MinutesRead)
|
||||
}
|
||||
}
|
||||
})
|
||||
// Validate 0 Minute / Day - Remaining 20 Days
|
||||
for i := 10; i < 30; i++ {
|
||||
stat := readStats[i]
|
||||
suite.Equal(int64(0), stat.MinutesRead, "should have zero minutes read")
|
||||
}
|
||||
}
|
||||
|
||||
89
database/migrations/20240128012356_user_auth_hash.go
Normal file
89
database/migrations/20240128012356_user_auth_hash.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/pressly/goose/v3"
|
||||
"reichard.io/antholume/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
goose.AddMigrationContext(upUserAuthHash, downUserAuthHash)
|
||||
}
|
||||
|
||||
func upUserAuthHash(ctx context.Context, tx *sql.Tx) error {
|
||||
// Determine if we have a new DB or not
|
||||
isNew := ctx.Value("isNew").(bool)
|
||||
if isNew {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Copy table & create column
|
||||
_, err := tx.Exec(`
|
||||
-- Create Copy Table
|
||||
CREATE TABLE temp_users AS SELECT * FROM users;
|
||||
ALTER TABLE temp_users ADD COLUMN auth_hash TEXT;
|
||||
|
||||
-- Update Schema
|
||||
DELETE FROM users;
|
||||
ALTER TABLE users ADD COLUMN auth_hash TEXT NOT NULL;
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get current users
|
||||
rows, err := tx.Query("SELECT id FROM temp_users")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Query existing users
|
||||
var users []string
|
||||
for rows.Next() {
|
||||
var user string
|
||||
if err := rows.Scan(&user); err != nil {
|
||||
return err
|
||||
}
|
||||
users = append(users, user)
|
||||
}
|
||||
|
||||
// Create auth hash per user
|
||||
for _, user := range users {
|
||||
rawAuthHash, err := utils.GenerateToken(64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authHash := fmt.Sprintf("%x", rawAuthHash)
|
||||
_, err = tx.Exec("UPDATE temp_users SET auth_hash = ? WHERE id = ?", authHash, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Copy from temp to true table
|
||||
_, err = tx.Exec(`
|
||||
-- Copy Into New
|
||||
INSERT INTO users SELECT * FROM temp_users;
|
||||
|
||||
-- Drop Temp Table
|
||||
DROP TABLE temp_users;
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func downUserAuthHash(ctx context.Context, tx *sql.Tx) error {
|
||||
// Drop column
|
||||
_, err := tx.Exec("ALTER users DROP COLUMN auth_hash")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
58
database/migrations/20240311121111_user_timezone.go
Normal file
58
database/migrations/20240311121111_user_timezone.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/pressly/goose/v3"
|
||||
)
|
||||
|
||||
func init() {
|
||||
goose.AddMigrationContext(upUserTimezone, downUserTimezone)
|
||||
}
|
||||
|
||||
func upUserTimezone(ctx context.Context, tx *sql.Tx) error {
|
||||
// Determine if we have a new DB or not
|
||||
isNew := ctx.Value("isNew").(bool)
|
||||
if isNew {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Copy table & create column
|
||||
_, err := tx.Exec(`
|
||||
-- Copy Table
|
||||
CREATE TABLE temp_users AS SELECT * FROM users;
|
||||
ALTER TABLE temp_users DROP COLUMN time_offset;
|
||||
ALTER TABLE temp_users ADD COLUMN timezone TEXT;
|
||||
UPDATE temp_users SET timezone = 'Europe/London';
|
||||
|
||||
-- Clean Table
|
||||
DELETE FROM users;
|
||||
ALTER TABLE users DROP COLUMN time_offset;
|
||||
ALTER TABLE users ADD COLUMN timezone TEXT NOT NULL DEFAULT 'Europe/London';
|
||||
|
||||
-- Copy Temp Table -> Clean Table
|
||||
INSERT INTO users SELECT * FROM temp_users;
|
||||
|
||||
-- Drop Temp Table
|
||||
DROP TABLE temp_users;
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func downUserTimezone(ctx context.Context, tx *sql.Tx) error {
|
||||
// Update column name & value
|
||||
_, err := tx.Exec(`
|
||||
ALTER TABLE users RENAME COLUMN timezone TO time_offset;
|
||||
UPDATE users SET time_offset = '0 hours';
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
38
database/migrations/20240510123707_import_basepath.go
Normal file
38
database/migrations/20240510123707_import_basepath.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/pressly/goose/v3"
|
||||
)
|
||||
|
||||
func init() {
|
||||
goose.AddMigrationContext(upImportBasepath, downImportBasepath)
|
||||
}
|
||||
|
||||
func upImportBasepath(ctx context.Context, tx *sql.Tx) error {
|
||||
// Determine if we have a new DB or not
|
||||
isNew := ctx.Value("isNew").(bool)
|
||||
if isNew {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add basepath column
|
||||
_, err := tx.Exec(`ALTER TABLE documents ADD COLUMN basepath TEXT;`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// This code is executed when the migration is applied.
|
||||
return nil
|
||||
}
|
||||
|
||||
func downImportBasepath(ctx context.Context, tx *sql.Tx) error {
|
||||
// Drop basepath column
|
||||
_, err := tx.Exec("ALTER documents DROP COLUMN basepath;")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
9
database/migrations/README.md
Normal file
9
database/migrations/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# DB Migrations
|
||||
|
||||
```bash
|
||||
goose create migration_name
|
||||
```
|
||||
|
||||
## Note
|
||||
|
||||
Since we update both the `schema.sql`, as well as the migration files, when we create a new DB it will inherently be up-to-date. We don't want to run the migrations if it's already up-to-date. Instead each migration checks if we have a new DB (via a value passed into the context), and if we do we simply return.
|
||||
@@ -1,22 +1,19 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.21.0
|
||||
// sqlc v1.27.0
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
type Activity struct {
|
||||
UserID string `json:"user_id"`
|
||||
DocumentID string `json:"document_id"`
|
||||
DeviceID string `json:"device_id"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
StartTime string `json:"start_time"`
|
||||
Page int64 `json:"page"`
|
||||
Pages int64 `json:"pages"`
|
||||
Duration int64 `json:"duration"`
|
||||
ID int64 `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
DocumentID string `json:"document_id"`
|
||||
DeviceID string `json:"device_id"`
|
||||
StartTime string `json:"start_time"`
|
||||
StartPercentage float64 `json:"start_percentage"`
|
||||
EndPercentage float64 `json:"end_percentage"`
|
||||
Duration int64 `json:"duration"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
@@ -31,6 +28,7 @@ type Device struct {
|
||||
type Document struct {
|
||||
ID string `json:"id"`
|
||||
Md5 *string `json:"md5"`
|
||||
Basepath *string `json:"basepath"`
|
||||
Filepath *string `json:"filepath"`
|
||||
Coverfile *string `json:"coverfile"`
|
||||
Title *string `json:"title"`
|
||||
@@ -60,16 +58,24 @@ type DocumentProgress struct {
|
||||
}
|
||||
|
||||
type DocumentUserStatistic struct {
|
||||
DocumentID string `json:"document_id"`
|
||||
UserID string `json:"user_id"`
|
||||
LastRead string `json:"last_read"`
|
||||
Page int64 `json:"page"`
|
||||
Pages int64 `json:"pages"`
|
||||
TotalTimeSeconds int64 `json:"total_time_seconds"`
|
||||
ReadPages int64 `json:"read_pages"`
|
||||
Percentage float64 `json:"percentage"`
|
||||
WordsRead int64 `json:"words_read"`
|
||||
Wpm float64 `json:"wpm"`
|
||||
DocumentID string `json:"document_id"`
|
||||
UserID string `json:"user_id"`
|
||||
Percentage float64 `json:"percentage"`
|
||||
LastRead string `json:"last_read"`
|
||||
LastSeen string `json:"last_seen"`
|
||||
ReadPercentage float64 `json:"read_percentage"`
|
||||
TotalTimeSeconds int64 `json:"total_time_seconds"`
|
||||
TotalWordsRead int64 `json:"total_words_read"`
|
||||
TotalWpm float64 `json:"total_wpm"`
|
||||
YearlyTimeSeconds int64 `json:"yearly_time_seconds"`
|
||||
YearlyWordsRead int64 `json:"yearly_words_read"`
|
||||
YearlyWpm float64 `json:"yearly_wpm"`
|
||||
MonthlyTimeSeconds int64 `json:"monthly_time_seconds"`
|
||||
MonthlyWordsRead int64 `json:"monthly_words_read"`
|
||||
MonthlyWpm float64 `json:"monthly_wpm"`
|
||||
WeeklyTimeSeconds int64 `json:"weekly_time_seconds"`
|
||||
WeeklyWordsRead int64 `json:"weekly_words_read"`
|
||||
WeeklyWpm float64 `json:"weekly_wpm"`
|
||||
}
|
||||
|
||||
type Metadatum struct {
|
||||
@@ -85,24 +91,20 @@ type Metadatum struct {
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type RawActivity struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
DocumentID string `json:"document_id"`
|
||||
DeviceID string `json:"device_id"`
|
||||
StartTime string `json:"start_time"`
|
||||
Page int64 `json:"page"`
|
||||
Pages int64 `json:"pages"`
|
||||
Duration int64 `json:"duration"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
type Setting struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Pass *string `json:"-"`
|
||||
Admin bool `json:"-"`
|
||||
TimeOffset *string `json:"time_offset"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
ID string `json:"id"`
|
||||
Pass *string `json:"-"`
|
||||
AuthHash *string `json:"auth_hash"`
|
||||
Admin bool `json:"-"`
|
||||
Timezone *string `json:"timezone"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type UserStreak struct {
|
||||
@@ -114,39 +116,8 @@ type UserStreak struct {
|
||||
CurrentStreak int64 `json:"current_streak"`
|
||||
CurrentStreakStartDate string `json:"current_streak_start_date"`
|
||||
CurrentStreakEndDate string `json:"current_streak_end_date"`
|
||||
}
|
||||
|
||||
type ViewDocumentUserStatistic struct {
|
||||
DocumentID string `json:"document_id"`
|
||||
UserID string `json:"user_id"`
|
||||
LastRead string `json:"last_read"`
|
||||
Page int64 `json:"page"`
|
||||
Pages int64 `json:"pages"`
|
||||
TotalTimeSeconds sql.NullFloat64 `json:"total_time_seconds"`
|
||||
ReadPages int64 `json:"read_pages"`
|
||||
Percentage float64 `json:"percentage"`
|
||||
WordsRead interface{} `json:"words_read"`
|
||||
Wpm int64 `json:"wpm"`
|
||||
}
|
||||
|
||||
type ViewRescaledActivity struct {
|
||||
UserID string `json:"user_id"`
|
||||
DocumentID string `json:"document_id"`
|
||||
DeviceID string `json:"device_id"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
StartTime string `json:"start_time"`
|
||||
Page int64 `json:"page"`
|
||||
Pages int64 `json:"pages"`
|
||||
Duration int64 `json:"duration"`
|
||||
}
|
||||
|
||||
type ViewUserStreak struct {
|
||||
UserID string `json:"user_id"`
|
||||
Window string `json:"window"`
|
||||
MaxStreak interface{} `json:"max_streak"`
|
||||
MaxStreakStartDate interface{} `json:"max_streak_start_date"`
|
||||
MaxStreakEndDate interface{} `json:"max_streak_end_date"`
|
||||
CurrentStreak interface{} `json:"current_streak"`
|
||||
CurrentStreakStartDate interface{} `json:"current_streak_start_date"`
|
||||
CurrentStreakEndDate interface{} `json:"current_streak_end_date"`
|
||||
LastTimezone string `json:"last_timezone"`
|
||||
LastSeen string `json:"last_seen"`
|
||||
LastRecord string `json:"last_record"`
|
||||
LastCalculated string `json:"last_calculated"`
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
-- name: AddActivity :one
|
||||
INSERT INTO raw_activity (
|
||||
INSERT INTO activity (
|
||||
user_id,
|
||||
document_id,
|
||||
device_id,
|
||||
start_time,
|
||||
duration,
|
||||
page,
|
||||
pages
|
||||
start_percentage,
|
||||
end_percentage
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
RETURNING *;
|
||||
@@ -26,10 +26,13 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
RETURNING *;
|
||||
|
||||
-- name: CreateUser :execrows
|
||||
INSERT INTO users (id, pass)
|
||||
VALUES (?, ?)
|
||||
INSERT INTO users (id, pass, auth_hash, admin)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- name: DeleteUser :execrows
|
||||
DELETE FROM users WHERE id = $id;
|
||||
|
||||
-- name: DeleteDocument :execrows
|
||||
UPDATE documents
|
||||
SET
|
||||
@@ -40,11 +43,13 @@ WHERE id = $id;
|
||||
WITH filtered_activity AS (
|
||||
SELECT
|
||||
document_id,
|
||||
device_id,
|
||||
user_id,
|
||||
start_time,
|
||||
duration,
|
||||
page,
|
||||
pages
|
||||
ROUND(CAST(start_percentage AS REAL) * 100, 2) AS start_percentage,
|
||||
ROUND(CAST(end_percentage AS REAL) * 100, 2) AS end_percentage,
|
||||
ROUND(CAST(end_percentage - start_percentage AS REAL) * 100, 2) AS read_percentage
|
||||
FROM activity
|
||||
WHERE
|
||||
activity.user_id = $user_id
|
||||
@@ -61,19 +66,21 @@ WITH filtered_activity AS (
|
||||
|
||||
SELECT
|
||||
document_id,
|
||||
CAST(STRFTIME('%Y-%m-%d %H:%M:%S', activity.start_time, users.time_offset) AS TEXT) AS start_time,
|
||||
device_id,
|
||||
LOCAL_TIME(activity.start_time, users.timezone) AS start_time,
|
||||
title,
|
||||
author,
|
||||
duration,
|
||||
page,
|
||||
pages
|
||||
start_percentage,
|
||||
end_percentage,
|
||||
read_percentage
|
||||
FROM filtered_activity AS activity
|
||||
LEFT JOIN documents ON documents.id = activity.document_id
|
||||
LEFT JOIN users ON users.id = activity.user_id;
|
||||
|
||||
-- name: GetDailyReadStats :many
|
||||
WITH RECURSIVE last_30_days AS (
|
||||
SELECT DATE('now', time_offset) AS date
|
||||
SELECT LOCAL_DATE(STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'), timezone) AS date
|
||||
FROM users WHERE users.id = $user_id
|
||||
UNION ALL
|
||||
SELECT DATE(date, '-1 days')
|
||||
@@ -82,9 +89,9 @@ WITH RECURSIVE last_30_days AS (
|
||||
),
|
||||
filtered_activity AS (
|
||||
SELECT
|
||||
user_id,
|
||||
user_id,
|
||||
start_time,
|
||||
duration
|
||||
duration
|
||||
FROM activity
|
||||
WHERE start_time > DATE('now', '-31 days')
|
||||
AND activity.user_id = $user_id
|
||||
@@ -92,11 +99,10 @@ filtered_activity AS (
|
||||
activity_days AS (
|
||||
SELECT
|
||||
SUM(duration) AS seconds_read,
|
||||
DATE(start_time, time_offset) AS day
|
||||
LOCAL_DATE(start_time, timezone) AS day
|
||||
FROM filtered_activity AS activity
|
||||
LEFT JOIN users ON users.id = activity.user_id
|
||||
GROUP BY day
|
||||
LIMIT 30
|
||||
)
|
||||
SELECT
|
||||
CAST(date AS TEXT),
|
||||
@@ -130,9 +136,10 @@ WHERE id = $device_id LIMIT 1;
|
||||
|
||||
-- name: GetDevices :many
|
||||
SELECT
|
||||
devices.id,
|
||||
devices.device_name,
|
||||
CAST(STRFTIME('%Y-%m-%d %H:%M:%S', devices.created_at, users.time_offset) AS TEXT) AS created_at,
|
||||
CAST(STRFTIME('%Y-%m-%d %H:%M:%S', devices.last_synced, users.time_offset) AS TEXT) AS last_synced
|
||||
LOCAL_TIME(devices.created_at, users.timezone) AS created_at,
|
||||
LOCAL_TIME(devices.last_synced, users.timezone) AS last_synced
|
||||
FROM devices
|
||||
JOIN users ON users.id = devices.user_id
|
||||
WHERE users.id = $user_id
|
||||
@@ -142,40 +149,19 @@ ORDER BY devices.last_synced DESC;
|
||||
SELECT * FROM documents
|
||||
WHERE id = $document_id LIMIT 1;
|
||||
|
||||
-- name: GetDocumentDaysRead :one
|
||||
WITH document_days AS (
|
||||
SELECT DATE(start_time, time_offset) AS dates
|
||||
FROM activity
|
||||
JOIN users ON users.id = activity.user_id
|
||||
WHERE document_id = $document_id
|
||||
AND user_id = $user_id
|
||||
GROUP BY dates
|
||||
)
|
||||
SELECT CAST(COUNT(*) AS INTEGER) AS days_read
|
||||
FROM document_days;
|
||||
|
||||
-- name: GetDocumentReadStats :one
|
||||
-- name: GetDocumentProgress :one
|
||||
SELECT
|
||||
COUNT(DISTINCT page) AS pages_read,
|
||||
SUM(duration) AS total_time
|
||||
FROM activity
|
||||
WHERE document_id = $document_id
|
||||
AND user_id = $user_id
|
||||
AND start_time >= $start_time;
|
||||
|
||||
-- name: GetDocumentReadStatsCapped :one
|
||||
WITH capped_stats AS (
|
||||
SELECT MIN(SUM(duration), CAST($page_duration_cap AS INTEGER)) AS durations
|
||||
FROM activity
|
||||
WHERE document_id = $document_id
|
||||
AND user_id = $user_id
|
||||
AND start_time >= $start_time
|
||||
GROUP BY page
|
||||
)
|
||||
SELECT
|
||||
CAST(COUNT(*) AS INTEGER) AS pages_read,
|
||||
CAST(SUM(durations) AS INTEGER) AS total_time
|
||||
FROM capped_stats;
|
||||
document_progress.*,
|
||||
devices.device_name
|
||||
FROM document_progress
|
||||
JOIN devices ON document_progress.device_id = devices.id
|
||||
WHERE
|
||||
document_progress.user_id = $user_id
|
||||
AND document_progress.document_id = $document_id
|
||||
ORDER BY
|
||||
document_progress.created_at
|
||||
DESC
|
||||
LIMIT 1;
|
||||
|
||||
-- name: GetDocumentWithStats :one
|
||||
SELECT
|
||||
@@ -188,24 +174,22 @@ SELECT
|
||||
docs.filepath,
|
||||
docs.words,
|
||||
|
||||
CAST(COALESCE(dus.wpm, 0.0) AS INTEGER) AS wpm,
|
||||
COALESCE(dus.page, 0) AS page,
|
||||
COALESCE(dus.pages, 0) AS pages,
|
||||
COALESCE(dus.read_pages, 0) AS read_pages,
|
||||
CAST(COALESCE(dus.total_wpm, 0.0) AS INTEGER) AS wpm,
|
||||
COALESCE(dus.read_percentage, 0) AS read_percentage,
|
||||
COALESCE(dus.total_time_seconds, 0) AS total_time_seconds,
|
||||
STRFTIME('%Y-%m-%d %H:%M:%S', COALESCE(dus.last_read, "1970-01-01"), users.time_offset)
|
||||
STRFTIME('%Y-%m-%d %H:%M:%S', LOCAL_TIME(COALESCE(dus.last_read, STRFTIME('%Y-%m-%dT%H:%M:%SZ', 0, 'unixepoch')), users.timezone))
|
||||
AS last_read,
|
||||
CASE
|
||||
WHEN dus.percentage > 97.0 THEN 100.0
|
||||
ROUND(CAST(CASE
|
||||
WHEN dus.percentage IS NULL THEN 0.0
|
||||
ELSE dus.percentage
|
||||
END AS percentage,
|
||||
WHEN (dus.percentage * 100.0) > 97.0 THEN 100.0
|
||||
ELSE dus.percentage * 100.0
|
||||
END AS REAL), 2) AS percentage,
|
||||
CAST(CASE
|
||||
WHEN dus.total_time_seconds IS NULL THEN 0.0
|
||||
ELSE
|
||||
CAST(dus.total_time_seconds AS REAL)
|
||||
/ CAST(dus.read_pages AS REAL)
|
||||
END AS INTEGER) AS seconds_per_page
|
||||
CAST(dus.total_time_seconds AS REAL)
|
||||
/ (dus.read_percentage * 100.0)
|
||||
END AS INTEGER) AS seconds_per_percent
|
||||
FROM documents AS docs
|
||||
LEFT JOIN users ON users.id = $user_id
|
||||
LEFT JOIN
|
||||
@@ -221,6 +205,16 @@ ORDER BY created_at DESC
|
||||
LIMIT $limit
|
||||
OFFSET $offset;
|
||||
|
||||
-- name: GetDocumentsSize :one
|
||||
SELECT
|
||||
COUNT(rowid) AS length
|
||||
FROM documents AS docs
|
||||
WHERE $query IS NULL OR (
|
||||
docs.title LIKE $query OR
|
||||
docs.author LIKE $query
|
||||
)
|
||||
LIMIT 1;
|
||||
|
||||
-- name: GetDocumentsWithStats :many
|
||||
SELECT
|
||||
docs.id,
|
||||
@@ -232,32 +226,37 @@ SELECT
|
||||
docs.filepath,
|
||||
docs.words,
|
||||
|
||||
CAST(COALESCE(dus.wpm, 0.0) AS INTEGER) AS wpm,
|
||||
COALESCE(dus.page, 0) AS page,
|
||||
COALESCE(dus.pages, 0) AS pages,
|
||||
COALESCE(dus.read_pages, 0) AS read_pages,
|
||||
CAST(COALESCE(dus.total_wpm, 0.0) AS INTEGER) AS wpm,
|
||||
COALESCE(dus.read_percentage, 0) AS read_percentage,
|
||||
COALESCE(dus.total_time_seconds, 0) AS total_time_seconds,
|
||||
STRFTIME('%Y-%m-%d %H:%M:%S', COALESCE(dus.last_read, "1970-01-01"), users.time_offset)
|
||||
STRFTIME('%Y-%m-%d %H:%M:%S', LOCAL_TIME(COALESCE(dus.last_read, STRFTIME('%Y-%m-%dT%H:%M:%SZ', 0, 'unixepoch')), users.timezone))
|
||||
AS last_read,
|
||||
CASE
|
||||
WHEN dus.percentage > 97.0 THEN 100.0
|
||||
ROUND(CAST(CASE
|
||||
WHEN dus.percentage IS NULL THEN 0.0
|
||||
ELSE dus.percentage
|
||||
END AS percentage,
|
||||
WHEN (dus.percentage * 100.0) > 97.0 THEN 100.0
|
||||
ELSE dus.percentage * 100.0
|
||||
END AS REAL), 2) AS percentage,
|
||||
|
||||
CASE
|
||||
WHEN dus.total_time_seconds IS NULL THEN 0.0
|
||||
ELSE
|
||||
ROUND(
|
||||
CAST(dus.total_time_seconds AS REAL)
|
||||
/ CAST(dus.read_pages AS REAL)
|
||||
/ (dus.read_percentage * 100.0)
|
||||
)
|
||||
END AS seconds_per_page
|
||||
END AS seconds_per_percent
|
||||
FROM documents AS docs
|
||||
LEFT JOIN users ON users.id = $user_id
|
||||
LEFT JOIN
|
||||
document_user_statistics AS dus
|
||||
ON dus.document_id = docs.id AND dus.user_id = $user_id
|
||||
WHERE docs.deleted = false
|
||||
WHERE
|
||||
docs.deleted = false AND (
|
||||
$query IS NULL OR (
|
||||
docs.title LIKE $query OR
|
||||
docs.author LIKE $query
|
||||
)
|
||||
)
|
||||
ORDER BY dus.last_read DESC, docs.created_at DESC
|
||||
LIMIT $limit
|
||||
OFFSET $offset;
|
||||
@@ -276,19 +275,30 @@ WHERE
|
||||
AND documents.deleted = false
|
||||
AND documents.id NOT IN (sqlc.slice('document_ids'));
|
||||
|
||||
-- name: GetProgress :one
|
||||
-- name: GetProgress :many
|
||||
SELECT
|
||||
document_progress.*,
|
||||
devices.device_name
|
||||
FROM document_progress
|
||||
JOIN devices ON document_progress.device_id = devices.id
|
||||
documents.title,
|
||||
documents.author,
|
||||
devices.device_name,
|
||||
ROUND(CAST(progress.percentage AS REAL) * 100, 2) AS percentage,
|
||||
progress.document_id,
|
||||
progress.user_id,
|
||||
LOCAL_TIME(progress.created_at, users.timezone) AS created_at
|
||||
FROM document_progress AS progress
|
||||
LEFT JOIN users ON progress.user_id = users.id
|
||||
LEFT JOIN devices ON progress.device_id = devices.id
|
||||
LEFT JOIN documents ON progress.document_id = documents.id
|
||||
WHERE
|
||||
document_progress.user_id = $user_id
|
||||
AND document_progress.document_id = $document_id
|
||||
ORDER BY
|
||||
document_progress.created_at
|
||||
DESC
|
||||
LIMIT 1;
|
||||
progress.user_id = $user_id
|
||||
AND (
|
||||
(
|
||||
CAST($doc_filter AS BOOLEAN) = TRUE
|
||||
AND document_id = $document_id
|
||||
) OR $doc_filter = FALSE
|
||||
)
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $limit
|
||||
OFFSET $offset;
|
||||
|
||||
-- name: GetUser :one
|
||||
SELECT * FROM users
|
||||
@@ -299,64 +309,53 @@ SELECT * FROM user_streaks
|
||||
WHERE user_id = $user_id;
|
||||
|
||||
-- name: GetUsers :many
|
||||
SELECT * FROM users
|
||||
WHERE
|
||||
users.id = $user
|
||||
OR ?1 IN (
|
||||
SELECT id
|
||||
FROM users
|
||||
WHERE id = $user
|
||||
AND admin = 1
|
||||
)
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $limit
|
||||
OFFSET $offset;
|
||||
SELECT * FROM users;
|
||||
|
||||
-- name: GetWPMLeaderboard :many
|
||||
-- name: GetUserStatistics :many
|
||||
SELECT
|
||||
user_id,
|
||||
CAST(SUM(words_read) AS INTEGER) AS total_words_read,
|
||||
|
||||
CAST(SUM(total_words_read) AS INTEGER) AS total_words_read,
|
||||
CAST(SUM(total_time_seconds) AS INTEGER) AS total_seconds,
|
||||
ROUND(CAST(SUM(words_read) AS REAL) / (SUM(total_time_seconds) / 60.0), 2)
|
||||
AS wpm
|
||||
ROUND(COALESCE(CAST(SUM(total_words_read) AS REAL) / (SUM(total_time_seconds) / 60.0), 0.0), 2)
|
||||
AS total_wpm,
|
||||
|
||||
CAST(SUM(yearly_words_read) AS INTEGER) AS yearly_words_read,
|
||||
CAST(SUM(yearly_time_seconds) AS INTEGER) AS yearly_seconds,
|
||||
ROUND(COALESCE(CAST(SUM(yearly_words_read) AS REAL) / (SUM(yearly_time_seconds) / 60.0), 0.0), 2)
|
||||
AS yearly_wpm,
|
||||
|
||||
CAST(SUM(monthly_words_read) AS INTEGER) AS monthly_words_read,
|
||||
CAST(SUM(monthly_time_seconds) AS INTEGER) AS monthly_seconds,
|
||||
ROUND(COALESCE(CAST(SUM(monthly_words_read) AS REAL) / (SUM(monthly_time_seconds) / 60.0), 0.0), 2)
|
||||
AS monthly_wpm,
|
||||
|
||||
CAST(SUM(weekly_words_read) AS INTEGER) AS weekly_words_read,
|
||||
CAST(SUM(weekly_time_seconds) AS INTEGER) AS weekly_seconds,
|
||||
ROUND(COALESCE(CAST(SUM(weekly_words_read) AS REAL) / (SUM(weekly_time_seconds) / 60.0), 0.0), 2)
|
||||
AS weekly_wpm
|
||||
|
||||
FROM document_user_statistics
|
||||
WHERE words_read > 0
|
||||
WHERE total_words_read > 0
|
||||
GROUP BY user_id
|
||||
ORDER BY wpm DESC;
|
||||
ORDER BY total_wpm DESC;
|
||||
|
||||
-- name: GetWantedDocuments :many
|
||||
SELECT
|
||||
CAST(value AS TEXT) AS id,
|
||||
CAST((documents.filepath IS NULL) AS BOOLEAN) AS want_file,
|
||||
CAST((IFNULL(documents.synced, false) != true) AS BOOLEAN) AS want_metadata
|
||||
CAST((documents.id IS NULL) AS BOOLEAN) AS want_metadata
|
||||
FROM json_each(?1)
|
||||
LEFT JOIN documents
|
||||
ON value = documents.id
|
||||
WHERE (
|
||||
documents.id IS NOT NULL
|
||||
AND documents.deleted = false
|
||||
AND (
|
||||
documents.synced = false
|
||||
OR documents.filepath IS NULL
|
||||
)
|
||||
AND documents.filepath IS NULL
|
||||
)
|
||||
OR (documents.id IS NULL)
|
||||
OR CAST($document_ids AS TEXT) != CAST($document_ids AS TEXT);
|
||||
|
||||
-- name: UpdateDocumentDeleted :one
|
||||
UPDATE documents
|
||||
SET
|
||||
deleted = $deleted
|
||||
WHERE id = $id
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateDocumentSync :one
|
||||
UPDATE documents
|
||||
SET
|
||||
synced = $synced
|
||||
WHERE id = $id
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateProgress :one
|
||||
INSERT OR REPLACE INTO document_progress (
|
||||
user_id,
|
||||
@@ -372,10 +371,21 @@ RETURNING *;
|
||||
UPDATE users
|
||||
SET
|
||||
pass = COALESCE($password, pass),
|
||||
time_offset = COALESCE($time_offset, time_offset)
|
||||
auth_hash = COALESCE($auth_hash, auth_hash),
|
||||
timezone = COALESCE($timezone, timezone),
|
||||
admin = COALESCE($admin, admin)
|
||||
WHERE id = $user_id
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateSettings :one
|
||||
INSERT INTO settings (name, value)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT DO UPDATE
|
||||
SET
|
||||
name = COALESCE(excluded.name, name),
|
||||
value = COALESCE(excluded.value, value)
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpsertDevice :one
|
||||
INSERT INTO devices (id, user_id, last_synced, device_name)
|
||||
VALUES (?, ?, ?, ?)
|
||||
@@ -389,6 +399,7 @@ RETURNING *;
|
||||
INSERT INTO documents (
|
||||
id,
|
||||
md5,
|
||||
basepath,
|
||||
filepath,
|
||||
coverfile,
|
||||
title,
|
||||
@@ -403,10 +414,11 @@ INSERT INTO documents (
|
||||
isbn10,
|
||||
isbn13
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT DO UPDATE
|
||||
SET
|
||||
md5 = COALESCE(excluded.md5, md5),
|
||||
basepath = COALESCE(excluded.basepath, basepath),
|
||||
filepath = COALESCE(excluded.filepath, filepath),
|
||||
coverfile = COALESCE(excluded.coverfile, coverfile),
|
||||
title = COALESCE(excluded.title, title),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,3 @@
|
||||
PRAGMA foreign_keys = ON;
|
||||
PRAGMA journal_mode = WAL;
|
||||
|
||||
---------------------------------------------------------------
|
||||
------------------------ Normal Tables ------------------------
|
||||
---------------------------------------------------------------
|
||||
@@ -10,8 +7,9 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
|
||||
pass TEXT NOT NULL,
|
||||
auth_hash TEXT NOT NULL,
|
||||
admin BOOLEAN NOT NULL DEFAULT 0 CHECK (admin IN (0, 1)),
|
||||
time_offset TEXT NOT NULL DEFAULT '0 hours',
|
||||
timezone TEXT NOT NULL DEFAULT 'Europe/London',
|
||||
|
||||
created_at DATETIME NOT NULL DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
||||
);
|
||||
@@ -21,6 +19,7 @@ CREATE TABLE IF NOT EXISTS documents (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
|
||||
md5 TEXT,
|
||||
basepath TEXT,
|
||||
filepath TEXT,
|
||||
coverfile TEXT,
|
||||
title TEXT,
|
||||
@@ -46,7 +45,6 @@ CREATE TABLE IF NOT EXISTS documents (
|
||||
-- Metadata
|
||||
CREATE TABLE IF NOT EXISTS metadata (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
document_id TEXT NOT NULL,
|
||||
|
||||
title TEXT,
|
||||
@@ -91,16 +89,17 @@ CREATE TABLE IF NOT EXISTS document_progress (
|
||||
PRIMARY KEY (user_id, document_id, device_id)
|
||||
);
|
||||
|
||||
-- Raw Read Activity
|
||||
CREATE TABLE IF NOT EXISTS raw_activity (
|
||||
-- Read Activity
|
||||
CREATE TABLE IF NOT EXISTS activity (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT NOT NULL,
|
||||
document_id TEXT NOT NULL,
|
||||
device_id TEXT NOT NULL,
|
||||
|
||||
start_time DATETIME NOT NULL,
|
||||
page INTEGER NOT NULL,
|
||||
pages INTEGER NOT NULL,
|
||||
start_percentage REAL NOT NULL,
|
||||
end_percentage REAL NOT NULL,
|
||||
|
||||
duration INTEGER NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||||
|
||||
@@ -109,25 +108,46 @@ CREATE TABLE IF NOT EXISTS raw_activity (
|
||||
FOREIGN KEY (device_id) REFERENCES devices (id)
|
||||
);
|
||||
|
||||
---------------------------------------------------------------
|
||||
----------------------- Temporary Tables ----------------------
|
||||
---------------------------------------------------------------
|
||||
-- Settings
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
-- Temporary Activity Table (Cached from View)
|
||||
CREATE TEMPORARY TABLE IF NOT EXISTS activity (
|
||||
user_id TEXT NOT NULL,
|
||||
document_id TEXT NOT NULL,
|
||||
device_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
|
||||
created_at DATETIME NOT NULL,
|
||||
start_time DATETIME NOT NULL,
|
||||
page INTEGER NOT NULL,
|
||||
pages INTEGER NOT NULL,
|
||||
duration INTEGER NOT NULL
|
||||
created_at DATETIME NOT NULL DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
||||
);
|
||||
|
||||
-- Temporary User Streaks Table (Cached from View)
|
||||
CREATE TEMPORARY TABLE IF NOT EXISTS user_streaks (
|
||||
-- Document User Statistics Table
|
||||
CREATE TABLE IF NOT EXISTS document_user_statistics (
|
||||
document_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
percentage REAL NOT NULL,
|
||||
last_read DATETIME NOT NULL,
|
||||
last_seen DATETIME NOT NULL,
|
||||
read_percentage REAL NOT NULL,
|
||||
|
||||
total_time_seconds INTEGER NOT NULL,
|
||||
total_words_read INTEGER NOT NULL,
|
||||
total_wpm REAL NOT NULL,
|
||||
|
||||
yearly_time_seconds INTEGER NOT NULL,
|
||||
yearly_words_read INTEGER NOT NULL,
|
||||
yearly_wpm REAL NOT NULL,
|
||||
|
||||
monthly_time_seconds INTEGER NOT NULL,
|
||||
monthly_words_read INTEGER NOT NULL,
|
||||
monthly_wpm REAL NOT NULL,
|
||||
|
||||
weekly_time_seconds INTEGER NOT NULL,
|
||||
weekly_words_read INTEGER NOT NULL,
|
||||
weekly_wpm REAL NOT NULL,
|
||||
|
||||
UNIQUE(document_id, user_id) ON CONFLICT REPLACE
|
||||
);
|
||||
|
||||
-- User Streaks Table
|
||||
CREATE TABLE IF NOT EXISTS user_streaks (
|
||||
user_id TEXT NOT NULL,
|
||||
window TEXT NOT NULL,
|
||||
|
||||
@@ -137,291 +157,28 @@ CREATE TEMPORARY TABLE IF NOT EXISTS user_streaks (
|
||||
|
||||
current_streak INTEGER NOT NULL,
|
||||
current_streak_start_date TEXT NOT NULL,
|
||||
current_streak_end_date TEXT NOT NULL
|
||||
);
|
||||
current_streak_end_date TEXT NOT NULL,
|
||||
|
||||
CREATE TEMPORARY TABLE IF NOT EXISTS document_user_statistics (
|
||||
document_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
last_read TEXT NOT NULL,
|
||||
page INTEGER NOT NULL,
|
||||
pages INTEGER NOT NULL,
|
||||
total_time_seconds INTEGER NOT NULL,
|
||||
read_pages INTEGER NOT NULL,
|
||||
percentage REAL NOT NULL,
|
||||
words_read INTEGER NOT NULL,
|
||||
wpm REAL NOT NULL
|
||||
);
|
||||
last_timezone TEXT NOT NULL,
|
||||
last_seen TEXT NOT NULL,
|
||||
last_record TEXT NOT NULL,
|
||||
last_calculated TEXT NOT NULL,
|
||||
|
||||
UNIQUE(user_id, window) ON CONFLICT REPLACE
|
||||
);
|
||||
|
||||
---------------------------------------------------------------
|
||||
--------------------------- Indexes ---------------------------
|
||||
---------------------------------------------------------------
|
||||
|
||||
CREATE INDEX IF NOT EXISTS temp.activity_start_time ON activity (start_time);
|
||||
CREATE INDEX IF NOT EXISTS temp.activity_user_id ON activity (user_id);
|
||||
CREATE INDEX IF NOT EXISTS temp.activity_user_id_document_id ON activity (
|
||||
CREATE INDEX IF NOT EXISTS activity_start_time ON activity (start_time);
|
||||
CREATE INDEX IF NOT EXISTS activity_created_at ON activity (created_at);
|
||||
CREATE INDEX IF NOT EXISTS activity_user_id ON activity (user_id);
|
||||
CREATE INDEX IF NOT EXISTS activity_user_id_document_id ON activity (
|
||||
user_id,
|
||||
document_id
|
||||
);
|
||||
|
||||
---------------------------------------------------------------
|
||||
---------------------------- Views ----------------------------
|
||||
---------------------------------------------------------------
|
||||
|
||||
--------------------------------
|
||||
------- Rescaled Activity ------
|
||||
--------------------------------
|
||||
|
||||
CREATE VIEW IF NOT EXISTS view_rescaled_activity AS
|
||||
|
||||
WITH RECURSIVE nums (idx) AS (
|
||||
SELECT 1 AS idx
|
||||
UNION ALL
|
||||
SELECT idx + 1
|
||||
FROM nums
|
||||
LIMIT 1000
|
||||
),
|
||||
|
||||
current_pages AS (
|
||||
SELECT
|
||||
document_id,
|
||||
user_id,
|
||||
pages
|
||||
FROM raw_activity
|
||||
GROUP BY document_id, user_id
|
||||
HAVING MAX(start_time)
|
||||
ORDER BY start_time DESC
|
||||
),
|
||||
|
||||
intermediate AS (
|
||||
SELECT
|
||||
raw_activity.document_id,
|
||||
raw_activity.device_id,
|
||||
raw_activity.user_id,
|
||||
raw_activity.created_at,
|
||||
raw_activity.start_time,
|
||||
raw_activity.duration,
|
||||
raw_activity.page,
|
||||
current_pages.pages,
|
||||
|
||||
-- Derive first page
|
||||
((raw_activity.page - 1) * current_pages.pages) / raw_activity.pages
|
||||
+ 1 AS first_page,
|
||||
|
||||
-- Derive last page
|
||||
MAX(
|
||||
((raw_activity.page - 1) * current_pages.pages)
|
||||
/ raw_activity.pages
|
||||
+ 1,
|
||||
(raw_activity.page * current_pages.pages) / raw_activity.pages
|
||||
) AS last_page
|
||||
|
||||
FROM raw_activity
|
||||
INNER JOIN current_pages ON
|
||||
current_pages.document_id = raw_activity.document_id
|
||||
AND current_pages.user_id = raw_activity.user_id
|
||||
),
|
||||
|
||||
num_limit AS (
|
||||
SELECT * FROM nums
|
||||
LIMIT (SELECT MAX(last_page - first_page + 1) FROM intermediate)
|
||||
),
|
||||
|
||||
rescaled_raw AS (
|
||||
SELECT
|
||||
intermediate.document_id,
|
||||
intermediate.device_id,
|
||||
intermediate.user_id,
|
||||
intermediate.created_at,
|
||||
intermediate.start_time,
|
||||
intermediate.last_page,
|
||||
intermediate.pages,
|
||||
intermediate.first_page + num_limit.idx - 1 AS page,
|
||||
intermediate.duration / (
|
||||
intermediate.last_page - intermediate.first_page + 1.0
|
||||
) AS duration
|
||||
FROM intermediate
|
||||
LEFT JOIN num_limit ON
|
||||
num_limit.idx <= (intermediate.last_page - intermediate.first_page + 1)
|
||||
)
|
||||
|
||||
SELECT
|
||||
user_id,
|
||||
document_id,
|
||||
device_id,
|
||||
created_at,
|
||||
start_time,
|
||||
page,
|
||||
pages,
|
||||
|
||||
-- Round up if last page (maintains total duration)
|
||||
CAST(CASE
|
||||
WHEN page = last_page AND duration != CAST(duration AS INTEGER)
|
||||
THEN duration + 1
|
||||
ELSE duration
|
||||
END AS INTEGER) AS duration
|
||||
FROM rescaled_raw;
|
||||
|
||||
--------------------------------
|
||||
--------- User Streaks ---------
|
||||
--------------------------------
|
||||
|
||||
CREATE VIEW IF NOT EXISTS view_user_streaks AS
|
||||
|
||||
WITH document_windows AS (
|
||||
SELECT
|
||||
activity.user_id,
|
||||
users.time_offset,
|
||||
DATE(
|
||||
activity.start_time,
|
||||
users.time_offset,
|
||||
'weekday 0', '-7 day'
|
||||
) AS weekly_read,
|
||||
DATE(activity.start_time, users.time_offset) AS daily_read
|
||||
FROM raw_activity AS activity
|
||||
LEFT JOIN users ON users.id = activity.user_id
|
||||
GROUP BY activity.user_id, weekly_read, daily_read
|
||||
),
|
||||
|
||||
weekly_partitions AS (
|
||||
SELECT
|
||||
user_id,
|
||||
time_offset,
|
||||
'WEEK' AS "window",
|
||||
weekly_read AS read_window,
|
||||
row_number() OVER (
|
||||
PARTITION BY user_id ORDER BY weekly_read DESC
|
||||
) AS seqnum
|
||||
FROM document_windows
|
||||
GROUP BY user_id, weekly_read
|
||||
),
|
||||
|
||||
daily_partitions AS (
|
||||
SELECT
|
||||
user_id,
|
||||
time_offset,
|
||||
'DAY' AS "window",
|
||||
daily_read AS read_window,
|
||||
row_number() OVER (
|
||||
PARTITION BY user_id ORDER BY daily_read DESC
|
||||
) AS seqnum
|
||||
FROM document_windows
|
||||
GROUP BY user_id, daily_read
|
||||
),
|
||||
|
||||
streaks AS (
|
||||
SELECT
|
||||
COUNT(*) AS streak,
|
||||
MIN(read_window) AS start_date,
|
||||
MAX(read_window) AS end_date,
|
||||
window,
|
||||
user_id,
|
||||
time_offset
|
||||
FROM daily_partitions
|
||||
GROUP BY
|
||||
time_offset,
|
||||
user_id,
|
||||
DATE(read_window, '+' || seqnum || ' day')
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
COUNT(*) AS streak,
|
||||
MIN(read_window) AS start_date,
|
||||
MAX(read_window) AS end_date,
|
||||
window,
|
||||
user_id,
|
||||
time_offset
|
||||
FROM weekly_partitions
|
||||
GROUP BY
|
||||
time_offset,
|
||||
user_id,
|
||||
DATE(read_window, '+' || (seqnum * 7) || ' day')
|
||||
),
|
||||
max_streak AS (
|
||||
SELECT
|
||||
MAX(streak) AS max_streak,
|
||||
start_date AS max_streak_start_date,
|
||||
end_date AS max_streak_end_date,
|
||||
window,
|
||||
user_id
|
||||
FROM streaks
|
||||
GROUP BY user_id, window
|
||||
),
|
||||
current_streak AS (
|
||||
SELECT
|
||||
streak AS current_streak,
|
||||
start_date AS current_streak_start_date,
|
||||
end_date AS current_streak_end_date,
|
||||
window,
|
||||
user_id
|
||||
FROM streaks
|
||||
WHERE CASE
|
||||
WHEN window = "WEEK" THEN
|
||||
DATE('now', time_offset, 'weekday 0', '-14 day') = current_streak_end_date
|
||||
OR DATE('now', time_offset, 'weekday 0', '-7 day') = current_streak_end_date
|
||||
WHEN window = "DAY" THEN
|
||||
DATE('now', time_offset, '-1 day') = current_streak_end_date
|
||||
OR DATE('now', time_offset) = current_streak_end_date
|
||||
END
|
||||
GROUP BY user_id, window
|
||||
)
|
||||
SELECT
|
||||
max_streak.user_id,
|
||||
max_streak.window,
|
||||
IFNULL(max_streak, 0) AS max_streak,
|
||||
IFNULL(max_streak_start_date, "N/A") AS max_streak_start_date,
|
||||
IFNULL(max_streak_end_date, "N/A") AS max_streak_end_date,
|
||||
IFNULL(current_streak, 0) AS current_streak,
|
||||
IFNULL(current_streak_start_date, "N/A") AS current_streak_start_date,
|
||||
IFNULL(current_streak_end_date, "N/A") AS current_streak_end_date
|
||||
FROM max_streak
|
||||
LEFT JOIN current_streak ON
|
||||
current_streak.user_id = max_streak.user_id
|
||||
AND current_streak.window = max_streak.window;
|
||||
|
||||
--------------------------------
|
||||
------- Document Stats ---------
|
||||
--------------------------------
|
||||
|
||||
CREATE VIEW IF NOT EXISTS view_document_user_statistics AS
|
||||
|
||||
WITH true_progress AS (
|
||||
SELECT
|
||||
document_id,
|
||||
user_id,
|
||||
start_time AS last_read,
|
||||
page,
|
||||
pages,
|
||||
SUM(duration) AS total_time_seconds,
|
||||
|
||||
-- Determine Read Pages
|
||||
COUNT(DISTINCT page) AS read_pages,
|
||||
|
||||
-- Derive Percentage of Book
|
||||
ROUND(CAST(page AS REAL) / CAST(pages AS REAL) * 100, 2) AS percentage
|
||||
FROM view_rescaled_activity
|
||||
GROUP BY document_id, user_id
|
||||
HAVING MAX(start_time)
|
||||
)
|
||||
SELECT
|
||||
true_progress.*,
|
||||
(CAST(COALESCE(documents.words, 0.0) AS REAL) / pages * read_pages)
|
||||
AS words_read,
|
||||
(CAST(COALESCE(documents.words, 0.0) AS REAL) / pages * read_pages)
|
||||
/ (total_time_seconds / 60.0) AS wpm
|
||||
FROM true_progress
|
||||
INNER JOIN documents ON documents.id = true_progress.document_id
|
||||
ORDER BY wpm DESC;
|
||||
|
||||
---------------------------------------------------------------
|
||||
------------------ Populate Temporary Tables ------------------
|
||||
---------------------------------------------------------------
|
||||
INSERT INTO activity SELECT * FROM view_rescaled_activity;
|
||||
INSERT INTO user_streaks SELECT * FROM view_user_streaks;
|
||||
INSERT INTO document_user_statistics SELECT * FROM view_document_user_statistics;
|
||||
|
||||
---------------------------------------------------------------
|
||||
--------------------------- Triggers --------------------------
|
||||
---------------------------------------------------------------
|
||||
@@ -433,3 +190,11 @@ UPDATE documents
|
||||
SET updated_at = STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now')
|
||||
WHERE id = old.id;
|
||||
END;
|
||||
|
||||
-- Delete User
|
||||
CREATE TRIGGER IF NOT EXISTS user_deleted
|
||||
BEFORE DELETE ON users BEGIN
|
||||
DELETE FROM activity WHERE activity.user_id=OLD.id;
|
||||
DELETE FROM devices WHERE devices.user_id=OLD.id;
|
||||
DELETE FROM document_progress WHERE document_progress.user_id=OLD.id;
|
||||
END;
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
DELETE FROM activity;
|
||||
INSERT INTO activity SELECT * FROM view_rescaled_activity;
|
||||
DELETE FROM user_streaks;
|
||||
INSERT INTO user_streaks SELECT * FROM view_user_streaks;
|
||||
DELETE FROM document_user_statistics;
|
||||
INSERT INTO document_user_statistics
|
||||
SELECT *
|
||||
FROM view_document_user_statistics;
|
||||
154
database/user_streaks.sql
Normal file
154
database/user_streaks.sql
Normal file
@@ -0,0 +1,154 @@
|
||||
WITH updated_users AS (
|
||||
SELECT a.user_id
|
||||
FROM activity AS a
|
||||
LEFT JOIN users AS u ON u.id = a.user_id
|
||||
LEFT JOIN user_streaks AS s ON a.user_id = s.user_id AND s.window = 'DAY'
|
||||
WHERE
|
||||
a.created_at > COALESCE(s.last_seen, '1970-01-01')
|
||||
AND LOCAL_DATE(s.last_record, u.timezone) != LOCAL_DATE(a.start_time, u.timezone)
|
||||
GROUP BY a.user_id
|
||||
),
|
||||
|
||||
outdated_users AS (
|
||||
SELECT
|
||||
a.user_id,
|
||||
u.timezone AS last_timezone,
|
||||
MAX(a.created_at) AS last_seen,
|
||||
MAX(a.start_time) AS last_record,
|
||||
STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now') AS last_calculated
|
||||
FROM activity AS a
|
||||
LEFT JOIN users AS u ON u.id = a.user_id
|
||||
LEFT JOIN user_streaks AS s ON a.user_id = s.user_id AND s.window = 'DAY'
|
||||
GROUP BY a.user_id
|
||||
HAVING
|
||||
-- User Changed Timezones
|
||||
s.last_timezone != u.timezone
|
||||
|
||||
-- Users Date Changed
|
||||
OR LOCAL_DATE(COALESCE(s.last_calculated, '1970-01-01T00:00:00Z'), u.timezone) !=
|
||||
LOCAL_DATE(STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'), u.timezone)
|
||||
|
||||
-- User Added New Data
|
||||
OR a.user_id IN updated_users
|
||||
),
|
||||
|
||||
document_windows AS (
|
||||
SELECT
|
||||
activity.user_id,
|
||||
users.timezone,
|
||||
DATE(
|
||||
LOCAL_DATE(activity.start_time, users.timezone),
|
||||
'weekday 0', '-7 day'
|
||||
) AS weekly_read,
|
||||
LOCAL_DATE(activity.start_time, users.timezone) AS daily_read
|
||||
FROM activity
|
||||
INNER JOIN outdated_users ON outdated_users.user_id = activity.user_id
|
||||
LEFT JOIN users ON users.id = activity.user_id
|
||||
GROUP BY activity.user_id, weekly_read, daily_read
|
||||
),
|
||||
|
||||
weekly_partitions AS (
|
||||
SELECT
|
||||
user_id,
|
||||
timezone,
|
||||
'WEEK' AS "window",
|
||||
weekly_read AS read_window,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY user_id ORDER BY weekly_read DESC
|
||||
) AS seqnum
|
||||
FROM document_windows
|
||||
GROUP BY user_id, weekly_read
|
||||
),
|
||||
|
||||
daily_partitions AS (
|
||||
SELECT
|
||||
user_id,
|
||||
timezone,
|
||||
'DAY' AS "window",
|
||||
daily_read AS read_window,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY user_id ORDER BY daily_read DESC
|
||||
) AS seqnum
|
||||
FROM document_windows
|
||||
GROUP BY user_id, daily_read
|
||||
),
|
||||
|
||||
streaks AS (
|
||||
SELECT
|
||||
COUNT(*) AS streak,
|
||||
MIN(read_window) AS start_date,
|
||||
MAX(read_window) AS end_date,
|
||||
window,
|
||||
user_id,
|
||||
timezone
|
||||
FROM daily_partitions
|
||||
GROUP BY
|
||||
timezone,
|
||||
user_id,
|
||||
DATE(read_window, '+' || seqnum || ' day')
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
COUNT(*) AS streak,
|
||||
MIN(read_window) AS start_date,
|
||||
MAX(read_window) AS end_date,
|
||||
window,
|
||||
user_id,
|
||||
timezone
|
||||
FROM weekly_partitions
|
||||
GROUP BY
|
||||
timezone,
|
||||
user_id,
|
||||
DATE(read_window, '+' || (seqnum * 7) || ' day')
|
||||
),
|
||||
|
||||
max_streak AS (
|
||||
SELECT
|
||||
MAX(streak) AS max_streak,
|
||||
start_date AS max_streak_start_date,
|
||||
end_date AS max_streak_end_date,
|
||||
window,
|
||||
user_id
|
||||
FROM streaks
|
||||
GROUP BY user_id, window
|
||||
),
|
||||
|
||||
current_streak AS (
|
||||
SELECT
|
||||
streak AS current_streak,
|
||||
start_date AS current_streak_start_date,
|
||||
end_date AS current_streak_end_date,
|
||||
window,
|
||||
user_id
|
||||
FROM streaks
|
||||
WHERE CASE
|
||||
WHEN window = "WEEK" THEN
|
||||
DATE(LOCAL_DATE(STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'), timezone), 'weekday 0', '-14 day') = current_streak_end_date
|
||||
OR DATE(LOCAL_DATE(STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'), timezone), 'weekday 0', '-7 day') = current_streak_end_date
|
||||
WHEN window = "DAY" THEN
|
||||
DATE(LOCAL_DATE(STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'), timezone), '-1 day') = current_streak_end_date
|
||||
OR DATE(LOCAL_DATE(STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'), timezone)) = current_streak_end_date
|
||||
END
|
||||
GROUP BY user_id, window
|
||||
)
|
||||
|
||||
INSERT INTO user_streaks
|
||||
SELECT
|
||||
max_streak.user_id,
|
||||
max_streak.window,
|
||||
IFNULL(max_streak, 0) AS max_streak,
|
||||
IFNULL(max_streak_start_date, "N/A") AS max_streak_start_date,
|
||||
IFNULL(max_streak_end_date, "N/A") AS max_streak_end_date,
|
||||
IFNULL(current_streak.current_streak, 0) AS current_streak,
|
||||
IFNULL(current_streak.current_streak_start_date, "N/A") AS current_streak_start_date,
|
||||
IFNULL(current_streak.current_streak_end_date, "N/A") AS current_streak_end_date,
|
||||
outdated_users.last_timezone AS last_timezone,
|
||||
outdated_users.last_seen AS last_seen,
|
||||
outdated_users.last_record AS last_record,
|
||||
outdated_users.last_calculated AS last_calculated
|
||||
FROM max_streak
|
||||
JOIN outdated_users ON max_streak.user_id = outdated_users.user_id
|
||||
LEFT JOIN current_streak ON
|
||||
current_streak.user_id = max_streak.user_id
|
||||
AND current_streak.window = max_streak.window;
|
||||
204
database/users_test.go
Normal file
204
database/users_test.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"reichard.io/antholume/config"
|
||||
"reichard.io/antholume/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
testUserID string = "testUser"
|
||||
testUserPass string = "testPass"
|
||||
)
|
||||
|
||||
type UsersTestSuite struct {
|
||||
suite.Suite
|
||||
dbm *DBManager
|
||||
}
|
||||
|
||||
func TestUsers(t *testing.T) {
|
||||
suite.Run(t, new(UsersTestSuite))
|
||||
}
|
||||
|
||||
func (suite *UsersTestSuite) SetupTest() {
|
||||
cfg := config.Config{
|
||||
DBType: "memory",
|
||||
}
|
||||
|
||||
suite.dbm = NewMgr(&cfg)
|
||||
|
||||
// Create User
|
||||
rawAuthHash, _ := utils.GenerateToken(64)
|
||||
authHash := fmt.Sprintf("%x", rawAuthHash)
|
||||
_, err := suite.dbm.Queries.CreateUser(suite.dbm.Ctx, CreateUserParams{
|
||||
ID: testUserID,
|
||||
Pass: &testUserPass,
|
||||
AuthHash: &authHash,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
// Create Document
|
||||
_, err = suite.dbm.Queries.UpsertDocument(suite.dbm.Ctx, UpsertDocumentParams{
|
||||
ID: documentID,
|
||||
Title: &documentTitle,
|
||||
Author: &documentAuthor,
|
||||
Words: &documentWords,
|
||||
})
|
||||
suite.NoError(err)
|
||||
|
||||
// Create Device
|
||||
_, err = suite.dbm.Queries.UpsertDevice(suite.dbm.Ctx, UpsertDeviceParams{
|
||||
ID: deviceID,
|
||||
UserID: testUserID,
|
||||
DeviceName: deviceName,
|
||||
})
|
||||
suite.NoError(err)
|
||||
}
|
||||
|
||||
func (suite *UsersTestSuite) TestGetUser() {
|
||||
user, err := suite.dbm.Queries.GetUser(suite.dbm.Ctx, testUserID)
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Equal(testUserPass, *user.Pass)
|
||||
}
|
||||
|
||||
func (suite *UsersTestSuite) TestCreateUser() {
|
||||
testUser := "user1"
|
||||
testPass := "pass1"
|
||||
|
||||
// Generate Auth Hash
|
||||
rawAuthHash, err := utils.GenerateToken(64)
|
||||
suite.Nil(err, "should have nil err")
|
||||
|
||||
authHash := fmt.Sprintf("%x", rawAuthHash)
|
||||
changed, err := suite.dbm.Queries.CreateUser(suite.dbm.Ctx, CreateUserParams{
|
||||
ID: testUser,
|
||||
Pass: &testPass,
|
||||
AuthHash: &authHash,
|
||||
})
|
||||
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Equal(int64(1), changed)
|
||||
|
||||
user, err := suite.dbm.Queries.GetUser(suite.dbm.Ctx, testUser)
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Equal(testPass, *user.Pass)
|
||||
}
|
||||
|
||||
func (suite *UsersTestSuite) TestDeleteUser() {
|
||||
changed, err := suite.dbm.Queries.DeleteUser(suite.dbm.Ctx, testUserID)
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Equal(int64(1), changed, "should have one changed row")
|
||||
|
||||
_, err = suite.dbm.Queries.GetUser(suite.dbm.Ctx, testUserID)
|
||||
suite.ErrorIs(err, sql.ErrNoRows, "should have no rows error")
|
||||
}
|
||||
|
||||
func (suite *UsersTestSuite) TestGetUsers() {
|
||||
users, err := suite.dbm.Queries.GetUsers(suite.dbm.Ctx)
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Len(users, 1, "should have single user")
|
||||
}
|
||||
|
||||
func (suite *UsersTestSuite) TestUpdateUser() {
|
||||
newPassword := "newPass123"
|
||||
user, err := suite.dbm.Queries.UpdateUser(suite.dbm.Ctx, UpdateUserParams{
|
||||
UserID: testUserID,
|
||||
Password: &newPassword,
|
||||
})
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Equal(newPassword, *user.Pass, "should have new password")
|
||||
}
|
||||
|
||||
func (suite *UsersTestSuite) TestGetUserStatistics() {
|
||||
err := suite.dbm.CacheTempTables()
|
||||
suite.NoError(err)
|
||||
|
||||
// Ensure Zero Items
|
||||
userStats, err := suite.dbm.Queries.GetUserStatistics(suite.dbm.Ctx)
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Empty(userStats, "should be empty")
|
||||
|
||||
// Create Activity
|
||||
end := time.Now()
|
||||
start := end.AddDate(0, 0, -9)
|
||||
var counter int64 = 0
|
||||
|
||||
for d := start; d.After(end) == false; d = d.AddDate(0, 0, 1) {
|
||||
counter += 1
|
||||
|
||||
// Add Item
|
||||
activity, err := suite.dbm.Queries.AddActivity(suite.dbm.Ctx, AddActivityParams{
|
||||
DocumentID: documentID,
|
||||
DeviceID: deviceID,
|
||||
UserID: testUserID,
|
||||
StartTime: d.UTC().Format(time.RFC3339),
|
||||
Duration: 60,
|
||||
StartPercentage: float64(counter) / 100.0,
|
||||
EndPercentage: float64(counter+1) / 100.0,
|
||||
})
|
||||
|
||||
suite.Nil(err, fmt.Sprintf("[%d] should have nil err for add activity", counter))
|
||||
suite.Equal(counter, activity.ID, fmt.Sprintf("[%d] should have correct id for add activity", counter))
|
||||
}
|
||||
|
||||
err = suite.dbm.CacheTempTables()
|
||||
suite.NoError(err)
|
||||
|
||||
// Ensure One Item
|
||||
userStats, err = suite.dbm.Queries.GetUserStatistics(suite.dbm.Ctx)
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Len(userStats, 1, "should have length of one")
|
||||
}
|
||||
|
||||
func (suite *UsersTestSuite) TestGetUsersStreaks() {
|
||||
err := suite.dbm.CacheTempTables()
|
||||
suite.NoError(err)
|
||||
|
||||
// Ensure Zero Items
|
||||
userStats, err := suite.dbm.Queries.GetUserStreaks(suite.dbm.Ctx, testUserID)
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Empty(userStats, "should be empty")
|
||||
|
||||
// Create Activity
|
||||
end := time.Now()
|
||||
start := end.AddDate(0, 0, -9)
|
||||
var counter int64 = 0
|
||||
|
||||
for d := start; d.After(end) == false; d = d.AddDate(0, 0, 1) {
|
||||
counter += 1
|
||||
|
||||
// Add Item
|
||||
activity, err := suite.dbm.Queries.AddActivity(suite.dbm.Ctx, AddActivityParams{
|
||||
DocumentID: documentID,
|
||||
DeviceID: deviceID,
|
||||
UserID: testUserID,
|
||||
StartTime: d.UTC().Format(time.RFC3339),
|
||||
Duration: 60,
|
||||
StartPercentage: float64(counter) / 100.0,
|
||||
EndPercentage: float64(counter+1) / 100.0,
|
||||
})
|
||||
|
||||
suite.Nil(err, fmt.Sprintf("[%d] should have nil err for add activity", counter))
|
||||
suite.Equal(counter, activity.ID, fmt.Sprintf("[%d] should have correct id for add activity", counter))
|
||||
}
|
||||
|
||||
err = suite.dbm.CacheTempTables()
|
||||
suite.NoError(err)
|
||||
|
||||
// Ensure Two Item
|
||||
userStats, err = suite.dbm.Queries.GetUserStreaks(suite.dbm.Ctx, testUserID)
|
||||
suite.Nil(err, "should have nil err")
|
||||
suite.Len(userStats, 2, "should have length of two")
|
||||
|
||||
// Ensure Streak Stats
|
||||
dayStats := userStats[0]
|
||||
weekStats := userStats[1]
|
||||
suite.Equal(int64(10), dayStats.CurrentStreak, "should be 10 days")
|
||||
suite.Greater(weekStats.CurrentStreak, int64(1), "should be 2 or 3")
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
services:
|
||||
bookmanager:
|
||||
antholume:
|
||||
environment:
|
||||
- CONFIG_PATH=/data
|
||||
- DATA_PATH=/data
|
||||
|
||||
100
go.mod
100
go.mod
@@ -1,70 +1,84 @@
|
||||
module reichard.io/bbank
|
||||
module reichard.io/antholume
|
||||
|
||||
go 1.19
|
||||
go 1.23
|
||||
|
||||
toolchain go1.23.3
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.8.1
|
||||
github.com/alexedwards/argon2id v0.0.0-20230305115115-4b3c3280a736
|
||||
github.com/gabriel-vasile/mimetype v1.4.2
|
||||
github.com/gin-contrib/multitemplate v0.0.0-20230212012517-45920c92c271
|
||||
github.com/gin-contrib/sessions v0.0.4
|
||||
github.com/PuerkitoBio/goquery v1.10.1
|
||||
github.com/a-h/templ v0.3.819
|
||||
github.com/alexedwards/argon2id v1.0.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.3
|
||||
github.com/gin-contrib/multitemplate v0.0.0-20231230012943-32b233489a81
|
||||
github.com/gin-contrib/sessions v0.0.5
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/microcosm-cc/bluemonday v1.0.25
|
||||
github.com/itchyny/gojq v0.12.14
|
||||
github.com/jarcoal/httpmock v1.3.1
|
||||
github.com/microcosm-cc/bluemonday v1.0.26
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pressly/goose/v3 v3.17.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/taylorskalyo/goreader v0.0.0-20230626212555-e7f5644f8115
|
||||
github.com/urfave/cli/v2 v2.25.7
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
|
||||
golang.org/x/net v0.15.0
|
||||
modernc.org/sqlite v1.26.0
|
||||
github.com/urfave/cli/v2 v2.27.1
|
||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
modernc.org/sqlite v1.28.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/cascadia v1.3.1 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/bytedance/sonic v1.10.0 // indirect
|
||||
github.com/bytedance/sonic v1.10.2 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||
github.com/chenzhuoyu/iasm v0.9.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/chenzhuoyu/iasm v0.9.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.15.3 // indirect
|
||||
github.com/go-playground/validator/v10 v10.17.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/gorilla/context v1.1.1 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
github.com/gorilla/sessions v1.2.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/context v1.1.2 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/gorilla/sessions v1.2.2 // indirect
|
||||
github.com/itchyny/timefmt-go v0.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sethvargo/go-retry v0.2.4 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
golang.org/x/arch v0.4.0 // indirect
|
||||
golang.org/x/crypto v0.13.0 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/sys v0.12.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
golang.org/x/tools v0.13.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/arch v0.7.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/mod v0.20.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/tools v0.24.0 // indirect
|
||||
google.golang.org/protobuf v1.32.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
modernc.org/cc/v3 v3.40.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.13 // indirect
|
||||
modernc.org/libc v1.24.1 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.6.0 // indirect
|
||||
lukechampine.com/uint128 v1.3.0 // indirect
|
||||
modernc.org/cc/v3 v3.41.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.15 // indirect
|
||||
modernc.org/libc v1.40.7 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.7.2 // indirect
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/token v1.0.1 // indirect
|
||||
modernc.org/strutil v1.2.0 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
)
|
||||
|
||||
358
go.sum
358
go.sum
@@ -1,91 +1,148 @@
|
||||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||
github.com/alexedwards/argon2id v0.0.0-20230305115115-4b3c3280a736 h1:qZaEtLxnqY5mJ0fVKbk31NVhlgi0yrKm51Pq/I5wcz4=
|
||||
github.com/alexedwards/argon2id v0.0.0-20230305115115-4b3c3280a736/go.mod h1:mTeFRcTdnpzOlRjMoFYC/80HwVUreupyAiqPkCZQOXc=
|
||||
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
|
||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||
github.com/antonlindstrom/pgstore v0.0.0-20200229204646-b08ebf1105e0/go.mod h1:2Ti6VUHVxpC0VSmTZzEvpzysnaGAfGBOoMIz5ykPyyw=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/ClickHouse/ch-go v0.58.2 h1:jSm2szHbT9MCAB1rJ3WuCJqmGLi5UTjlNu+f530UTS0=
|
||||
github.com/ClickHouse/ch-go v0.58.2/go.mod h1:Ap/0bEmiLa14gYjCiRkYGbXvbe8vwdrfTYWhsuQ99aw=
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.16.0 h1:rhMfnPewXPnY4Q4lQRGdYuTLRBRKJEIEYHtbUMrzmvI=
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.16.0/go.mod h1:J7SPfIxwR+x4mQ+o8MLSe0oY50NNntEqCIjFe/T1VPM=
|
||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
||||
github.com/PuerkitoBio/goquery v1.10.1 h1:Y8JGYUkXWTGRB6Ars3+j3kN0xg1YqqlwvdTV8WTFQcU=
|
||||
github.com/PuerkitoBio/goquery v1.10.1/go.mod h1:IYiHrOMps66ag56LEH7QYDDupKXyo5A8qrjIx3ZtujY=
|
||||
github.com/a-h/templ v0.3.819 h1:KDJ5jTFN15FyJnmSmo2gNirIqt7hfvBD2VXVDTySckM=
|
||||
github.com/a-h/templ v0.3.819/go.mod h1:iDJKJktpttVKdWoTkRNNLcllRI+BlpopJc+8au3gOUo=
|
||||
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
|
||||
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
||||
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
||||
github.com/bytedance/sonic v1.10.0 h1:qtNZduETEIWJVIyDl01BeNxur2rW9OwTQ/yBqFRkKEk=
|
||||
github.com/bytedance/sonic v1.10.0/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
|
||||
github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
|
||||
github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
|
||||
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/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
|
||||
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
|
||||
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
|
||||
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||
github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8=
|
||||
github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
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/docker/cli v24.0.7+incompatible h1:wa/nIwYFW7BVTGa7SWPVyyXU9lgORqUb1xfI36MSkFg=
|
||||
github.com/docker/cli v24.0.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM=
|
||||
github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/gin-contrib/multitemplate v0.0.0-20230212012517-45920c92c271 h1:s+boMV47gwTyff2PL+k6V33edJpp+K5y3QPzZlRhno8=
|
||||
github.com/gin-contrib/multitemplate v0.0.0-20230212012517-45920c92c271/go.mod h1:XLLtIXoP9+9zGcEDc7gAGV3AksGPO+vzv4kXHMJSdU0=
|
||||
github.com/gin-contrib/sessions v0.0.4 h1:gq4fNa1Zmp564iHP5G6EBuktilEos8VKhe2sza1KMgo=
|
||||
github.com/gin-contrib/sessions v0.0.4/go.mod h1:pQ3sIyviBBGcxgyR8mkeJuXbeV3h3NYmhJADQTq5+Vo=
|
||||
github.com/elastic/go-sysinfo v1.11.2 h1:mcm4OSYVMyws6+n2HIVMGkln5HOpo5Ie1ZmbbNn0jg4=
|
||||
github.com/elastic/go-sysinfo v1.11.2/go.mod h1:GKqR8bbMK/1ITnez9NIsIfXQr25aLhRJa7AfT8HpBFQ=
|
||||
github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0=
|
||||
github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gin-contrib/multitemplate v0.0.0-20231230012943-32b233489a81 h1:hQ/WeoPMTbN8NHk5i96dWy3D4uF7yCU+kORyWG+P4oU=
|
||||
github.com/gin-contrib/multitemplate v0.0.0-20231230012943-32b233489a81/go.mod h1:XLLtIXoP9+9zGcEDc7gAGV3AksGPO+vzv4kXHMJSdU0=
|
||||
github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE=
|
||||
github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
|
||||
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
|
||||
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
|
||||
github.com/go-faster/errors v0.6.1 h1:nNIPOBkprlKzkThvS/0YaX8Zs9KewLCOSFQS5BU06FI=
|
||||
github.com/go-faster/errors v0.6.1/go.mod h1:5MGV2/2T9yvlrbhe9pD9LO5Z/2zCSq2T8j+Jpi2LAyY=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||
github.com/go-playground/validator/v10 v10.15.3 h1:S+sSpunYjNPDuXkWbK+x+bA7iXiW296KG4dL3X7xUZo=
|
||||
github.com/go-playground/validator/v10 v10.15.3/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74=
|
||||
github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
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/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
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/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
|
||||
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
|
||||
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
|
||||
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
|
||||
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||
github.com/itchyny/gojq v0.12.14 h1:6k8vVtsrhQSYgSGg827AD+PVVaB1NLXEdX+dda2oZCc=
|
||||
github.com/itchyny/gojq v0.12.14/go.mod h1:y1G7oO7XkcR1LPZO59KyoCRy08T3j9vDYRV0GgYSS+s=
|
||||
github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
|
||||
github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI=
|
||||
github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww=
|
||||
github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
|
||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4=
|
||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak=
|
||||
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
|
||||
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
|
||||
github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4=
|
||||
github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
|
||||
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
@@ -95,35 +152,51 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
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/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
|
||||
github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg=
|
||||
github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE=
|
||||
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-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g=
|
||||
github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
|
||||
github.com/opencontainers/runc v1.1.10 h1:EaL5WeO9lv9wmS6SASjszOeQdSctvpbu0DdBQBizE40=
|
||||
github.com/opencontainers/runc v1.1.10/go.mod h1:+/R6+KmDlh+hOO8NkjmgkG9Qzvypzk0yXxAPYYR65+M=
|
||||
github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4=
|
||||
github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg=
|
||||
github.com/paulmach/orb v0.10.0 h1:guVYVqzxHE/CQ1KpfGO077TR0ATHSNjp4s6XGLn3W9s=
|
||||
github.com/paulmach/orb v0.10.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
|
||||
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/pressly/goose/v3 v3.17.0 h1:fT4CL3LRm4kfyLuPWzDFAoxjR5ZHjeJ6uQhibQtBaIs=
|
||||
github.com/pressly/goose/v3 v3.17.0/go.mod h1:22aw7NpnCPlS86oqkO/+3+o9FuCaJg4ZVWRUO3oGzHQ=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
@@ -131,13 +204,18 @@ github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUA
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
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/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/sethvargo/go-retry v0.2.4 h1:T+jHEQy/zKJf5s95UkguisicE0zuF9y7+/vgz08Ocec=
|
||||
github.com/sethvargo/go-retry v0.2.4/go.mod h1:1afjQuvh7s4gflMObvjLPaWgluLLyhA1wmVZ6KLpICw=
|
||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
@@ -150,52 +228,74 @@ github.com/taylorskalyo/goreader v0.0.0-20230626212555-e7f5644f8115 h1:OEAIMYp5l
|
||||
github.com/taylorskalyo/goreader v0.0.0-20230626212555-e7f5644f8115/go.mod h1:AIVbkIe1G7fpFHiKOdxZnU5p9tFPYNTQyH3H5IrRkGw=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
|
||||
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
|
||||
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||
github.com/vertica/vertica-sql-go v1.3.3 h1:fL+FKEAEy5ONmsvya2WH5T8bhkvY27y/Ik3ReR2T+Qw=
|
||||
github.com/vertica/vertica-sql-go v1.3.3/go.mod h1:jnn2GFuv+O2Jcjktb7zyc4Utlbu9YVqpHH/lx63+1M4=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI=
|
||||
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
github.com/ydb-platform/ydb-go-genproto v0.0.0-20231012155159-f85a672542fd h1:dzWP1Lu+A40W883dK/Mr3xyDSM/2MggS8GtHT0qgAnE=
|
||||
github.com/ydb-platform/ydb-go-genproto v0.0.0-20231012155159-f85a672542fd/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I=
|
||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.54.2 h1:E0yUuuX7UmPxXm92+yQCjMveLFO3zfvYFIJVuAqsVRA=
|
||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.54.2/go.mod h1:fjBLQ2TdQNl4bMjuWl9adoTGBypwUTPoGC+EqYqiIcU=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opentelemetry.io/otel v1.20.0 h1:vsb/ggIY+hUjD/zCAQHpzTmndPqv/ml2ArbsbfBYTAc=
|
||||
go.opentelemetry.io/otel v1.20.0/go.mod h1:oUIGj3D77RwJdM6PPZImDpSZGDvkD9fhesHny69JFrs=
|
||||
go.opentelemetry.io/otel/trace v1.20.0 h1:+yxVAPZPbQhbC3OfAkeIVTky6iTFpcr4SiY9om7mXSQ=
|
||||
go.opentelemetry.io/otel/trace v1.20.0/go.mod h1:HJSK7F/hA5RlzpZ0zKDCHCDHm556LCDtKaAo6JmBFUU=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc=
|
||||
golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
|
||||
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
|
||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
||||
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/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
||||
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
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.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/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.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -204,68 +304,94 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/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.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||
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.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
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.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/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.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
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/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA=
|
||||
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
|
||||
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
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=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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=
|
||||
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
|
||||
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
|
||||
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
|
||||
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
|
||||
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
|
||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
|
||||
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q=
|
||||
modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
|
||||
modernc.org/ccgo/v3 v3.16.15 h1:KbDR3ZAVU+wiLyMESPtbtE/Add4elztFyfsWoNTgxS0=
|
||||
modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI=
|
||||
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
|
||||
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
|
||||
modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM=
|
||||
modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.6.0 h1:i6mzavxrE9a30whzMfwf7XWVODx2r5OYXvU46cirX7o=
|
||||
modernc.org/memory v1.6.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
||||
modernc.org/libc v1.40.7 h1:oeLS0G067ZqUu+v143Dqad0btMfKmNS7SuOsnkq0Ysg=
|
||||
modernc.org/libc v1.40.7/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
|
||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
||||
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sqlite v1.26.0 h1:SocQdLRSYlA8W99V8YH0NES75thx19d9sB/aFc4R8Lw=
|
||||
modernc.org/sqlite v1.26.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU=
|
||||
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
|
||||
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
||||
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
|
||||
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
|
||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
|
||||
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
|
||||
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=
|
||||
modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
||||
@@ -5,10 +5,16 @@ import (
|
||||
"math"
|
||||
)
|
||||
|
||||
type SVGRawData struct {
|
||||
Value int
|
||||
Label string
|
||||
}
|
||||
|
||||
type SVGGraphPoint struct {
|
||||
X int
|
||||
Y int
|
||||
Size int
|
||||
Data SVGRawData
|
||||
}
|
||||
|
||||
type SVGGraphData struct {
|
||||
@@ -26,12 +32,12 @@ type SVGBezierOpposedLine struct {
|
||||
Angle int
|
||||
}
|
||||
|
||||
func GetSVGGraphData(inputData []int64, svgWidth int, svgHeight int) SVGGraphData {
|
||||
func GetSVGGraphData(inputData []SVGRawData, svgWidth int, svgHeight int) SVGGraphData {
|
||||
// Derive Height
|
||||
var maxHeight int = 0
|
||||
for _, item := range inputData {
|
||||
if int(item) > maxHeight {
|
||||
maxHeight = int(item)
|
||||
if int(item.Value) > maxHeight {
|
||||
maxHeight = int(item.Value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,20 +58,22 @@ func GetSVGGraphData(inputData []int64, svgWidth int, svgHeight int) SVGGraphDat
|
||||
var maxBX int = 0
|
||||
var maxBY int = 0
|
||||
var minBX int = 0
|
||||
for idx, item := range inputData {
|
||||
itemSize := int(float32(item) * sizeRatio)
|
||||
for idx, datum := range inputData {
|
||||
itemSize := int(float32(datum.Value) * sizeRatio)
|
||||
itemY := svgHeight - itemSize
|
||||
lineX := (idx + 1) * blockOffset
|
||||
barPoints = append(barPoints, SVGGraphPoint{
|
||||
X: lineX - (blockOffset / 2),
|
||||
Y: itemY,
|
||||
Size: itemSize,
|
||||
Data: datum,
|
||||
})
|
||||
|
||||
linePoints = append(linePoints, SVGGraphPoint{
|
||||
X: lineX,
|
||||
Y: itemY,
|
||||
Size: itemSize,
|
||||
Data: datum,
|
||||
})
|
||||
|
||||
if lineX > maxBX {
|
||||
@@ -101,9 +109,6 @@ func getSVGBezierOpposedLine(pointA SVGGraphPoint, pointB SVGGraphPoint) SVGBezi
|
||||
Length: int(math.Sqrt(math.Pow(lengthX, 2) + math.Pow(lengthY, 2))),
|
||||
Angle: int(math.Atan2(lengthY, lengthX)),
|
||||
}
|
||||
|
||||
// length = Math.sqrt(Math.pow(lengthX, 2) + Math.pow(lengthY, 2)),
|
||||
// angle = Math.atan2(lengthY, lengthX)
|
||||
}
|
||||
|
||||
func getSVGBezierControlPoint(currentPoint *SVGGraphPoint, prevPoint *SVGGraphPoint, nextPoint *SVGGraphPoint, isReverse bool) SVGGraphPoint {
|
||||
@@ -118,7 +123,7 @@ func getSVGBezierControlPoint(currentPoint *SVGGraphPoint, prevPoint *SVGGraphPo
|
||||
// Modifiers
|
||||
var smoothingRatio float64 = 0.2
|
||||
var directionModifier float64 = 0
|
||||
if isReverse == true {
|
||||
if isReverse {
|
||||
directionModifier = math.Pi
|
||||
}
|
||||
|
||||
|
||||
55
main.go
55
main.go
@@ -1,34 +1,31 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
"reichard.io/bbank/server"
|
||||
"reichard.io/antholume/config"
|
||||
"reichard.io/antholume/server"
|
||||
)
|
||||
|
||||
type UTCFormatter struct {
|
||||
log.Formatter
|
||||
}
|
||||
|
||||
func (u UTCFormatter) Format(e *log.Entry) ([]byte, error) {
|
||||
e.Time = e.Time.UTC()
|
||||
return u.Formatter.Format(e)
|
||||
}
|
||||
//go:embed templates/* assets/*
|
||||
var embeddedAssets embed.FS
|
||||
|
||||
func main() {
|
||||
log.SetFormatter(UTCFormatter{&log.TextFormatter{FullTimestamp: true}})
|
||||
|
||||
app := &cli.App{
|
||||
Name: "Book Bank",
|
||||
Usage: "A self hosted e-book progress tracker.",
|
||||
Name: "AnthoLume",
|
||||
Usage: "A self hosted e-book progress tracker.",
|
||||
EnableBashCompletion: true,
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "serve",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "Start Book Bank web server.",
|
||||
Usage: "Start AnthoLume web server.",
|
||||
Action: cmdServer,
|
||||
},
|
||||
},
|
||||
@@ -40,17 +37,29 @@ func main() {
|
||||
}
|
||||
|
||||
func cmdServer(ctx *cli.Context) error {
|
||||
log.Info("Starting Book Bank Server")
|
||||
server := server.NewServer()
|
||||
server.StartServer()
|
||||
var assets fs.FS = embeddedAssets
|
||||
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
<-c
|
||||
// Load config
|
||||
c := config.Load()
|
||||
if c.Version == "develop" {
|
||||
assets = os.DirFS("./")
|
||||
}
|
||||
|
||||
log.Info("Stopping Server")
|
||||
server.StopServer()
|
||||
log.Info("Server Stopped")
|
||||
log.Info("Starting AnthoLume Server")
|
||||
|
||||
// Create notify channel
|
||||
signals := make(chan os.Signal, 1)
|
||||
signal.Notify(signals, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
// Start server
|
||||
s := server.New(c, assets)
|
||||
s.Start()
|
||||
|
||||
// Wait & close
|
||||
<-signals
|
||||
s.Stop()
|
||||
|
||||
// Stop server
|
||||
os.Exit(0)
|
||||
|
||||
return nil
|
||||
|
||||
110
metadata/_test_files/gbooks_id_response.json
Normal file
110
metadata/_test_files/gbooks_id_response.json
Normal file
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"kind": "books#volume",
|
||||
"id": "ZxwpakTv_MIC",
|
||||
"etag": "mhqr3GsebaQ",
|
||||
"selfLink": "https://www.googleapis.com/books/v1/volumes/ZxwpakTv_MIC",
|
||||
"volumeInfo": {
|
||||
"title": "Alice in Wonderland",
|
||||
"authors": [
|
||||
"Lewis Carroll"
|
||||
],
|
||||
"publisher": "The Floating Press",
|
||||
"publishedDate": "2009-01-01",
|
||||
"description": "Alice in Wonderland (also known as Alice's Adventures in Wonderland), from 1865, is the peculiar and imaginative tale of a girl who falls down a rabbit-hole into a bizarre world of eccentric and unusual creatures. Lewis Carroll's prominent example of the genre of \"literary nonsense\" has endured in popularity with its clever way of playing with logic and a narrative structure that has influence generations of fiction writing.",
|
||||
"industryIdentifiers": [
|
||||
{
|
||||
"type": "ISBN_10",
|
||||
"identifier": "1877527815"
|
||||
},
|
||||
{
|
||||
"type": "ISBN_13",
|
||||
"identifier": "9781877527814"
|
||||
}
|
||||
],
|
||||
"readingModes": {
|
||||
"text": true,
|
||||
"image": false
|
||||
},
|
||||
"pageCount": 104,
|
||||
"printedPageCount": 112,
|
||||
"printType": "BOOK",
|
||||
"categories": [
|
||||
"Fiction / Classics",
|
||||
"Juvenile Fiction / General"
|
||||
],
|
||||
"averageRating": 5,
|
||||
"ratingsCount": 1,
|
||||
"maturityRating": "NOT_MATURE",
|
||||
"allowAnonLogging": true,
|
||||
"contentVersion": "0.2.3.0.preview.2",
|
||||
"panelizationSummary": {
|
||||
"containsEpubBubbles": false,
|
||||
"containsImageBubbles": false
|
||||
},
|
||||
"imageLinks": {
|
||||
"smallThumbnail": "http://books.google.com/books/content?id=ZxwpakTv_MIC&printsec=frontcover&img=1&zoom=5&edge=curl&imgtk=AFLRE71e5b-TeAKTiPSvXNUPeUi8rItzur2xSzwH8QU3qjKH0A2opmoq1o5I9RqJFt1BtcCCqILhnYRcB2aFLJmEvom11gx3Qn3PNN1iBLj2H5y2JHjM8wIwGT7iWFQmEn0Od7s6sOdk&source=gbs_api",
|
||||
"thumbnail": "http://books.google.com/books/content?id=ZxwpakTv_MIC&printsec=frontcover&img=1&zoom=1&edge=curl&imgtk=AFLRE70QORt9J_DmKJgfyf9UEjQkdDMZ0qAu0GP315a1Q4CRS3snEjKnJJO2fYFdxjMwsSpmHoXDFPZbsy4gw-kMvF7lL8LtwxGbJGlfETHw_jbQBKBlKTrneK4XFvvV-EXNrZRgylxj&source=gbs_api",
|
||||
"small": "http://books.google.com/books/content?id=ZxwpakTv_MIC&printsec=frontcover&img=1&zoom=2&edge=curl&imgtk=AFLRE70r1pAUt6VhuEEW8vXFhu8LvKln3yj0mdlaWPO4ZQuODLFQnH0fTebKMMX4ANR5i4PtC0oaI48XkwF-EdzlEM1WmUcR5383N4kRMXcta_i9nmb2y38dnh3hObwQW5VoAxbc9psn&source=gbs_api",
|
||||
"medium": "http://books.google.com/books/content?id=ZxwpakTv_MIC&printsec=frontcover&img=1&zoom=3&edge=curl&imgtk=AFLRE7019EVuXvhzbhmtbz1QFh-ajB6kTKRHGhqijFf8big_GPRMMdpCdKlklFbkCfXvy8F64t5NKlThUHb3tFP-51bbDXkrVErFbCqKGzGnDSSm8cewqT8HiYDNHqn0hXYnuYvN4vYf&source=gbs_api",
|
||||
"large": "http://books.google.com/books/content?id=ZxwpakTv_MIC&printsec=frontcover&img=1&zoom=4&edge=curl&imgtk=AFLRE72I15XZqp_8c8BAj4EskxkdC6nQz8F0Fs6VJhkykwIqfjzwuM34tUSQa3UnMGbx-UYjZjSLmCNFlePS8aR7yy-0UP9BRnYD-h5Qbesnnt_xdOb3u7Wdiobi6VbciNCBwUwbCyeH&source=gbs_api",
|
||||
"extraLarge": "http://books.google.com/books/content?id=ZxwpakTv_MIC&printsec=frontcover&img=1&zoom=6&edge=curl&imgtk=AFLRE70rC6ktY6U0K_hqG1HxPl_9hMjpKb10p9DryVIwQgUjoJfWQOjpNA3EQ-5yk167yYDlO27gylqNAdJBYWu7ZHr3GuqkjTDpXjDvzBBppVyWaVNxKwhOz3gfJ-gzM6cC4kLHP26R&source=gbs_api"
|
||||
},
|
||||
"language": "en",
|
||||
"previewLink": "http://books.google.com/books?id=ZxwpakTv_MIC&hl=&source=gbs_api",
|
||||
"infoLink": "https://play.google.com/store/books/details?id=ZxwpakTv_MIC&source=gbs_api",
|
||||
"canonicalVolumeLink": "https://play.google.com/store/books/details?id=ZxwpakTv_MIC"
|
||||
},
|
||||
"layerInfo": {
|
||||
"layers": [
|
||||
{
|
||||
"layerId": "geo",
|
||||
"volumeAnnotationsVersion": "2"
|
||||
}
|
||||
]
|
||||
},
|
||||
"saleInfo": {
|
||||
"country": "US",
|
||||
"saleability": "FOR_SALE",
|
||||
"isEbook": true,
|
||||
"listPrice": {
|
||||
"amount": 3.99,
|
||||
"currencyCode": "USD"
|
||||
},
|
||||
"retailPrice": {
|
||||
"amount": 3.99,
|
||||
"currencyCode": "USD"
|
||||
},
|
||||
"buyLink": "https://play.google.com/store/books/details?id=ZxwpakTv_MIC&rdid=book-ZxwpakTv_MIC&rdot=1&source=gbs_api",
|
||||
"offers": [
|
||||
{
|
||||
"finskyOfferType": 1,
|
||||
"listPrice": {
|
||||
"amountInMicros": 3990000,
|
||||
"currencyCode": "USD"
|
||||
},
|
||||
"retailPrice": {
|
||||
"amountInMicros": 3990000,
|
||||
"currencyCode": "USD"
|
||||
},
|
||||
"giftable": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"accessInfo": {
|
||||
"country": "US",
|
||||
"viewability": "PARTIAL",
|
||||
"embeddable": true,
|
||||
"publicDomain": false,
|
||||
"textToSpeechPermission": "ALLOWED",
|
||||
"epub": {
|
||||
"isAvailable": true,
|
||||
"acsTokenLink": "http://books.google.com/books/download/Alice_in_Wonderland-sample-epub.acsm?id=ZxwpakTv_MIC&format=epub&output=acs4_fulfillment_token&dl_type=sample&source=gbs_api"
|
||||
},
|
||||
"pdf": {
|
||||
"isAvailable": false
|
||||
},
|
||||
"webReaderLink": "http://play.google.com/books/reader?id=ZxwpakTv_MIC&hl=&source=gbs_api",
|
||||
"accessViewStatus": "SAMPLE",
|
||||
"quoteSharingAllowed": false
|
||||
}
|
||||
}
|
||||
105
metadata/_test_files/gbooks_query_response.json
Normal file
105
metadata/_test_files/gbooks_query_response.json
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"kind": "books#volumes",
|
||||
"totalItems": 1,
|
||||
"items": [
|
||||
{
|
||||
"kind": "books#volume",
|
||||
"id": "ZxwpakTv_MIC",
|
||||
"etag": "F2eR9VV6VwQ",
|
||||
"selfLink": "https://www.googleapis.com/books/v1/volumes/ZxwpakTv_MIC",
|
||||
"volumeInfo": {
|
||||
"title": "Alice in Wonderland",
|
||||
"authors": [
|
||||
"Lewis Carroll"
|
||||
],
|
||||
"publisher": "The Floating Press",
|
||||
"publishedDate": "2009-01-01",
|
||||
"description": "Alice in Wonderland (also known as Alice's Adventures in Wonderland), from 1865, is the peculiar and imaginative tale of a girl who falls down a rabbit-hole into a bizarre world of eccentric and unusual creatures. Lewis Carroll's prominent example of the genre of \"literary nonsense\" has endured in popularity with its clever way of playing with logic and a narrative structure that has influence generations of fiction writing.",
|
||||
"industryIdentifiers": [
|
||||
{
|
||||
"type": "ISBN_13",
|
||||
"identifier": "9781877527814"
|
||||
},
|
||||
{
|
||||
"type": "ISBN_10",
|
||||
"identifier": "1877527815"
|
||||
}
|
||||
],
|
||||
"readingModes": {
|
||||
"text": true,
|
||||
"image": false
|
||||
},
|
||||
"pageCount": 104,
|
||||
"printType": "BOOK",
|
||||
"categories": [
|
||||
"Fiction"
|
||||
],
|
||||
"averageRating": 5,
|
||||
"ratingsCount": 1,
|
||||
"maturityRating": "NOT_MATURE",
|
||||
"allowAnonLogging": true,
|
||||
"contentVersion": "0.2.3.0.preview.2",
|
||||
"panelizationSummary": {
|
||||
"containsEpubBubbles": false,
|
||||
"containsImageBubbles": false
|
||||
},
|
||||
"imageLinks": {
|
||||
"smallThumbnail": "http://books.google.com/books/content?id=ZxwpakTv_MIC&printsec=frontcover&img=1&zoom=5&edge=curl&source=gbs_api",
|
||||
"thumbnail": "http://books.google.com/books/content?id=ZxwpakTv_MIC&printsec=frontcover&img=1&zoom=1&edge=curl&source=gbs_api"
|
||||
},
|
||||
"language": "en",
|
||||
"previewLink": "http://books.google.com/books?id=ZxwpakTv_MIC&printsec=frontcover&dq=isbn:1877527815&hl=&cd=1&source=gbs_api",
|
||||
"infoLink": "https://play.google.com/store/books/details?id=ZxwpakTv_MIC&source=gbs_api",
|
||||
"canonicalVolumeLink": "https://play.google.com/store/books/details?id=ZxwpakTv_MIC"
|
||||
},
|
||||
"saleInfo": {
|
||||
"country": "US",
|
||||
"saleability": "FOR_SALE",
|
||||
"isEbook": true,
|
||||
"listPrice": {
|
||||
"amount": 3.99,
|
||||
"currencyCode": "USD"
|
||||
},
|
||||
"retailPrice": {
|
||||
"amount": 3.99,
|
||||
"currencyCode": "USD"
|
||||
},
|
||||
"buyLink": "https://play.google.com/store/books/details?id=ZxwpakTv_MIC&rdid=book-ZxwpakTv_MIC&rdot=1&source=gbs_api",
|
||||
"offers": [
|
||||
{
|
||||
"finskyOfferType": 1,
|
||||
"listPrice": {
|
||||
"amountInMicros": 3990000,
|
||||
"currencyCode": "USD"
|
||||
},
|
||||
"retailPrice": {
|
||||
"amountInMicros": 3990000,
|
||||
"currencyCode": "USD"
|
||||
},
|
||||
"giftable": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"accessInfo": {
|
||||
"country": "US",
|
||||
"viewability": "PARTIAL",
|
||||
"embeddable": true,
|
||||
"publicDomain": false,
|
||||
"textToSpeechPermission": "ALLOWED",
|
||||
"epub": {
|
||||
"isAvailable": true,
|
||||
"acsTokenLink": "http://books.google.com/books/download/Alice_in_Wonderland-sample-epub.acsm?id=ZxwpakTv_MIC&format=epub&output=acs4_fulfillment_token&dl_type=sample&source=gbs_api"
|
||||
},
|
||||
"pdf": {
|
||||
"isAvailable": false
|
||||
},
|
||||
"webReaderLink": "http://play.google.com/books/reader?id=ZxwpakTv_MIC&hl=&source=gbs_api",
|
||||
"accessViewStatus": "SAMPLE",
|
||||
"quoteSharingAllowed": false
|
||||
},
|
||||
"searchInfo": {
|
||||
"textSnippet": "Alice in Wonderland (also known as Alice's Adventures in Wonderland), from 1865, is the peculiar and imaginative tale of a girl who falls down a rabbit-hole into a bizarre world of eccentric and unusual creatures."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
@@ -14,11 +15,34 @@ func getEPUBMetadata(filepath string) (*MetadataInfo, error) {
|
||||
}
|
||||
rf := rc.Rootfiles[0]
|
||||
|
||||
return &MetadataInfo{
|
||||
parsedMetadata := &MetadataInfo{
|
||||
Type: TYPE_EPUB,
|
||||
Title: &rf.Title,
|
||||
Author: &rf.Creator,
|
||||
Description: &rf.Description,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Parse Possible ISBN
|
||||
if rf.Source != "" {
|
||||
replaceRE := regexp.MustCompile(`[-\s]`)
|
||||
possibleISBN := replaceRE.ReplaceAllString(rf.Source, "")
|
||||
|
||||
// ISBN Matches
|
||||
isbn13RE := regexp.MustCompile(`(?P<ISBN>\d{13})`)
|
||||
isbn10RE := regexp.MustCompile(`(?P<ISBN>\d{10})`)
|
||||
isbn13Matches := isbn13RE.FindStringSubmatch(possibleISBN)
|
||||
isbn10Matches := isbn10RE.FindStringSubmatch(possibleISBN)
|
||||
|
||||
if len(isbn13Matches) > 0 {
|
||||
isbnIndex := isbn13RE.SubexpIndex("ISBN")
|
||||
parsedMetadata.ISBN13 = &isbn13Matches[isbnIndex]
|
||||
} else if len(isbn10Matches) > 0 {
|
||||
isbnIndex := isbn10RE.SubexpIndex("ISBN")
|
||||
parsedMetadata.ISBN10 = &isbn10Matches[isbnIndex]
|
||||
}
|
||||
}
|
||||
|
||||
return parsedMetadata, nil
|
||||
}
|
||||
|
||||
func countEPUBWords(filepath string) (int64, error) {
|
||||
|
||||
@@ -121,34 +121,34 @@ func getGBooksMetadata(metadataSearch MetadataInfo) ([]MetadataInfo, error) {
|
||||
func saveGBooksCover(gbid string, coverFilePath string, overwrite bool) error {
|
||||
// Validate File Doesn't Exists
|
||||
_, err := os.Stat(coverFilePath)
|
||||
if err == nil && overwrite == false {
|
||||
log.Warn("[saveGBooksCover] File Alreads Exists")
|
||||
if err == nil && !overwrite {
|
||||
log.Warn("File Alreads Exists")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create File
|
||||
out, err := os.Create(coverFilePath)
|
||||
if err != nil {
|
||||
log.Error("[saveGBooksCover] File Create Error")
|
||||
log.Error("File Create Error")
|
||||
return errors.New("File Failure")
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// Download File
|
||||
log.Info("[saveGBooksCover] Downloading Cover")
|
||||
log.Info("Downloading Cover")
|
||||
coverURL := fmt.Sprintf(GBOOKS_GBID_COVER_URL, gbid)
|
||||
resp, err := http.Get(coverURL)
|
||||
if err != nil {
|
||||
log.Error("[saveGBooksCover] Cover URL API Failure")
|
||||
log.Error("Cover URL API Failure")
|
||||
return errors.New("API Failure")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Copy File to Disk
|
||||
log.Info("[saveGBooksCover] Saving Cover")
|
||||
log.Info("Saving Cover")
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
if err != nil {
|
||||
log.Error("[saveGBooksCover] File Copy Error")
|
||||
log.Error("File Copy Error")
|
||||
return errors.New("File Failure")
|
||||
}
|
||||
|
||||
@@ -157,22 +157,22 @@ func saveGBooksCover(gbid string, coverFilePath string, overwrite bool) error {
|
||||
|
||||
func performSearchRequest(searchQuery string) (*gBooksQueryResponse, error) {
|
||||
apiQuery := fmt.Sprintf(GBOOKS_QUERY_URL, searchQuery)
|
||||
log.Info("[performSearchRequest] Acquiring Metadata: ", apiQuery)
|
||||
log.Info("Acquiring Metadata: ", apiQuery)
|
||||
resp, err := http.Get(apiQuery)
|
||||
if err != nil {
|
||||
log.Error("[performSearchRequest] Google Books Query URL API Failure")
|
||||
log.Error("Google Books Query URL API Failure")
|
||||
return nil, errors.New("API Failure")
|
||||
}
|
||||
|
||||
parsedResp := gBooksQueryResponse{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&parsedResp)
|
||||
if err != nil {
|
||||
log.Error("[performSearchRequest] Google Books Query API Decode Failure")
|
||||
log.Error("Google Books Query API Decode Failure")
|
||||
return nil, errors.New("API Failure")
|
||||
}
|
||||
|
||||
if len(parsedResp.Items) == 0 {
|
||||
log.Warn("[performSearchRequest] No Results")
|
||||
log.Warn("No Results")
|
||||
return nil, errors.New("No Results")
|
||||
}
|
||||
|
||||
@@ -182,17 +182,17 @@ func performSearchRequest(searchQuery string) (*gBooksQueryResponse, error) {
|
||||
func performGBIDRequest(id string) (*gBooksQueryItem, error) {
|
||||
apiQuery := fmt.Sprintf(GBOOKS_GBID_INFO_URL, id)
|
||||
|
||||
log.Info("[performGBIDRequest] Acquiring CoverID")
|
||||
log.Info("Acquiring CoverID")
|
||||
resp, err := http.Get(apiQuery)
|
||||
if err != nil {
|
||||
log.Error("[performGBIDRequest] Cover URL API Failure")
|
||||
log.Error("Cover URL API Failure")
|
||||
return nil, errors.New("API Failure")
|
||||
}
|
||||
|
||||
parsedResp := gBooksQueryItem{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&parsedResp)
|
||||
if err != nil {
|
||||
log.Error("[performGBIDRequest] Google Books ID API Decode Failure")
|
||||
log.Error("Google Books ID API Decode Failure")
|
||||
return nil, errors.New("API Failure")
|
||||
}
|
||||
|
||||
|
||||
126
metadata/gbooks_test.go
Normal file
126
metadata/gbooks_test.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package metadata
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jarcoal/httpmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
//go:embed _test_files/gbooks_id_response.json
|
||||
var idResp string
|
||||
|
||||
//go:embed _test_files/gbooks_query_response.json
|
||||
var queryResp string
|
||||
|
||||
type details struct {
|
||||
URLs []string
|
||||
}
|
||||
|
||||
// Hook API Helper
|
||||
func hookAPI() *details {
|
||||
// Start HTTPMock
|
||||
httpmock.Activate()
|
||||
|
||||
// Create details struct
|
||||
d := &details{
|
||||
URLs: []string{},
|
||||
}
|
||||
|
||||
// Create Hook
|
||||
matchRE := regexp.MustCompile(`^https://www\.googleapis\.com/books/v1/volumes.*`)
|
||||
httpmock.RegisterRegexpResponder("GET", matchRE, func(req *http.Request) (*http.Response, error) {
|
||||
// Append URL
|
||||
d.URLs = append(d.URLs, req.URL.String())
|
||||
|
||||
// Get Raw Response
|
||||
var rawResp string
|
||||
if req.URL.Query().Get("q") != "" {
|
||||
rawResp = queryResp
|
||||
} else {
|
||||
rawResp = idResp
|
||||
}
|
||||
|
||||
// Convert to JSON Response
|
||||
var responseData map[string]any
|
||||
_ = json.Unmarshal([]byte(rawResp), &responseData)
|
||||
|
||||
// Return Response
|
||||
return httpmock.NewJsonResponse(200, responseData)
|
||||
})
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
func TestGBooksGBIDMetadata(t *testing.T) {
|
||||
hookDetails := hookAPI()
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
GBID := "ZxwpakTv_MIC"
|
||||
expectedURL := fmt.Sprintf(GBOOKS_GBID_INFO_URL, GBID)
|
||||
metadataResp, err := getGBooksMetadata(MetadataInfo{ID: &GBID})
|
||||
|
||||
assert.Nil(t, err, "should not have error")
|
||||
assert.Contains(t, hookDetails.URLs, expectedURL, "should have intercepted URL")
|
||||
assert.Equal(t, 1, len(metadataResp), "should have single result")
|
||||
|
||||
mResult := metadataResp[0]
|
||||
validateResult(t, &mResult)
|
||||
}
|
||||
|
||||
func TestGBooksISBNQuery(t *testing.T) {
|
||||
hookDetails := hookAPI()
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
ISBN10 := "1877527815"
|
||||
expectedURL := fmt.Sprintf(GBOOKS_QUERY_URL, "isbn:"+ISBN10)
|
||||
metadataResp, err := getGBooksMetadata(MetadataInfo{
|
||||
ISBN10: &ISBN10,
|
||||
})
|
||||
|
||||
assert.Nil(t, err, "should not have error")
|
||||
assert.Contains(t, hookDetails.URLs, expectedURL, "should have intercepted URL")
|
||||
assert.Equal(t, 1, len(metadataResp), "should have single result")
|
||||
|
||||
mResult := metadataResp[0]
|
||||
validateResult(t, &mResult)
|
||||
}
|
||||
|
||||
func TestGBooksTitleQuery(t *testing.T) {
|
||||
hookDetails := hookAPI()
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
title := "Alice in Wonderland 1877527815"
|
||||
expectedURL := fmt.Sprintf(GBOOKS_QUERY_URL, url.QueryEscape(strings.TrimSpace(title)))
|
||||
metadataResp, err := getGBooksMetadata(MetadataInfo{
|
||||
Title: &title,
|
||||
})
|
||||
|
||||
assert.Nil(t, err, "should not have error")
|
||||
assert.Contains(t, hookDetails.URLs, expectedURL, "should have intercepted URL")
|
||||
assert.NotEqual(t, 0, len(metadataResp), "should not have no results")
|
||||
|
||||
mResult := metadataResp[0]
|
||||
validateResult(t, &mResult)
|
||||
}
|
||||
|
||||
func validateResult(t *testing.T, m *MetadataInfo) {
|
||||
expectedTitle := "Alice in Wonderland"
|
||||
expectedAuthor := "Lewis Carroll"
|
||||
expectedDesc := "Alice in Wonderland (also known as Alice's Adventures in Wonderland), from 1865, is the peculiar and imaginative tale of a girl who falls down a rabbit-hole into a bizarre world of eccentric and unusual creatures. Lewis Carroll's prominent example of the genre of \"literary nonsense\" has endured in popularity with its clever way of playing with logic and a narrative structure that has influence generations of fiction writing."
|
||||
expectedISBN10 := "1877527815"
|
||||
expectedISBN13 := "9781877527814"
|
||||
|
||||
assert.Equal(t, expectedTitle, *m.Title, "should have title")
|
||||
assert.Equal(t, expectedAuthor, *m.Author, "should have author")
|
||||
assert.Equal(t, expectedDesc, *m.Description, "should have description")
|
||||
assert.Equal(t, expectedISBN10, *m.ISBN10, "should have ISBN10")
|
||||
assert.Equal(t, expectedISBN13, *m.ISBN13, "should have ISBN10")
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
//go:build integration
|
||||
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGBooksGBIDMetadata(t *testing.T) {
|
||||
GBID := "ZxwpakTv_MIC"
|
||||
metadataResp, err := getGBooksMetadata(MetadataInfo{
|
||||
ID: &GBID,
|
||||
})
|
||||
|
||||
if len(metadataResp) != 1 {
|
||||
t.Fatalf(`Expected: %v, Got: %v, Error: %v`, 1, len(metadataResp), err)
|
||||
}
|
||||
|
||||
mResult := metadataResp[0]
|
||||
validateResult(&mResult, t)
|
||||
}
|
||||
|
||||
func TestGBooksISBNQuery(t *testing.T) {
|
||||
ISBN10 := "1877527815"
|
||||
metadataResp, err := getGBooksMetadata(MetadataInfo{
|
||||
ISBN10: &ISBN10,
|
||||
})
|
||||
|
||||
if len(metadataResp) != 1 {
|
||||
t.Fatalf(`Expected: %v, Got: %v, Error: %v`, 1, len(metadataResp), err)
|
||||
}
|
||||
|
||||
mResult := metadataResp[0]
|
||||
validateResult(&mResult, t)
|
||||
}
|
||||
|
||||
func TestGBooksTitleQuery(t *testing.T) {
|
||||
title := "Alice in Wonderland 1877527815"
|
||||
metadataResp, err := getGBooksMetadata(MetadataInfo{
|
||||
Title: &title,
|
||||
})
|
||||
|
||||
if len(metadataResp) == 0 {
|
||||
t.Fatalf(`Expected: %v, Got: %v, Error: %v`, "> 0", len(metadataResp), err)
|
||||
}
|
||||
|
||||
mResult := metadataResp[0]
|
||||
validateResult(&mResult, t)
|
||||
}
|
||||
|
||||
func validateResult(m *MetadataInfo, t *testing.T) {
|
||||
expect := "Lewis Carroll"
|
||||
if *m.Author != expect {
|
||||
t.Fatalf(`Expected: %v, Got: %v`, expect, *m.Author)
|
||||
}
|
||||
|
||||
expect = "Alice in Wonderland"
|
||||
if *m.Title != expect {
|
||||
t.Fatalf(`Expected: %v, Got: %v`, expect, *m.Title)
|
||||
}
|
||||
|
||||
expect = "Alice in Wonderland (also known as Alice's Adventures in Wonderland), from 1865, is the peculiar and imaginative tale of a girl who falls down a rabbit-hole into a bizarre world of eccentric and unusual creatures. Lewis Carroll's prominent example of the genre of \"literary nonsense\" has endured in popularity with its clever way of playing with logic and a narrative structure that has influence generations of fiction writing."
|
||||
if *m.Description != expect {
|
||||
t.Fatalf(`Expected: %v, Got: %v`, expect, *m.Description)
|
||||
}
|
||||
|
||||
expect = "1877527815"
|
||||
if *m.ISBN10 != expect {
|
||||
t.Fatalf(`Expected: %v, Got: %v`, expect, *m.ISBN10)
|
||||
}
|
||||
|
||||
expect = "9781877527814"
|
||||
if *m.ISBN13 != expect {
|
||||
t.Fatalf(`Expected: %v, Got: %v`, expect, *m.ISBN13)
|
||||
}
|
||||
}
|
||||
@@ -3,27 +3,47 @@ package metadata
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"reichard.io/antholume/utils"
|
||||
)
|
||||
|
||||
type MetadataHandler func(string) (*MetadataInfo, error)
|
||||
|
||||
type DocumentType string
|
||||
|
||||
const (
|
||||
TYPE_EPUB DocumentType = ".epub"
|
||||
)
|
||||
|
||||
var extensionHandlerMap = map[DocumentType]MetadataHandler{
|
||||
TYPE_EPUB: getEPUBMetadata,
|
||||
}
|
||||
|
||||
type Source int
|
||||
|
||||
const (
|
||||
GBOOK Source = iota
|
||||
OLIB
|
||||
SOURCE_GBOOK Source = iota
|
||||
SOURCE_OLIB
|
||||
)
|
||||
|
||||
type MetadataInfo struct {
|
||||
ID *string
|
||||
ID *string
|
||||
MD5 *string
|
||||
PartialMD5 *string
|
||||
WordCount *int64
|
||||
|
||||
Title *string
|
||||
Author *string
|
||||
Description *string
|
||||
ISBN10 *string
|
||||
ISBN13 *string
|
||||
Type DocumentType
|
||||
}
|
||||
|
||||
// Downloads the Google Books cover file and saves it to the provided directory.
|
||||
func CacheCover(gbid string, coverDir string, documentID string, overwrite bool) (*string, error) {
|
||||
// Get Filepath
|
||||
coverFile := "." + filepath.Clean(fmt.Sprintf("/%s.jpg", documentID))
|
||||
@@ -39,44 +59,128 @@ func CacheCover(gbid string, coverDir string, documentID string, overwrite bool)
|
||||
return &coverFile, nil
|
||||
}
|
||||
|
||||
// Searches source for metadata based on the provided information.
|
||||
func SearchMetadata(s Source, metadataSearch MetadataInfo) ([]MetadataInfo, error) {
|
||||
switch s {
|
||||
case GBOOK:
|
||||
case SOURCE_GBOOK:
|
||||
return getGBooksMetadata(metadataSearch)
|
||||
case OLIB:
|
||||
return nil, errors.New("Not implemented")
|
||||
case SOURCE_OLIB:
|
||||
return nil, errors.New("not implemented")
|
||||
default:
|
||||
return nil, errors.New("Not implemented")
|
||||
return nil, errors.New("not implemented")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func GetWordCount(filepath string) (int64, error) {
|
||||
fileMime, err := mimetype.DetectFile(filepath)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if fileExtension := fileMime.Extension(); fileExtension == ".epub" {
|
||||
totalWords, err := countEPUBWords(filepath)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return totalWords, nil
|
||||
} else {
|
||||
return 0, errors.New("Invalid Extension")
|
||||
}
|
||||
}
|
||||
|
||||
func GetMetadata(filepath string) (*MetadataInfo, error) {
|
||||
// Returns the word count of the provided filepath. An error will be returned
|
||||
// if the file is not supported.
|
||||
func GetWordCount(filepath string) (*int64, error) {
|
||||
fileMime, err := mimetype.DetectFile(filepath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if fileExtension := fileMime.Extension(); fileExtension == ".epub" {
|
||||
return getEPUBMetadata(filepath)
|
||||
totalWords, err := countEPUBWords(filepath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &totalWords, nil
|
||||
} else {
|
||||
return nil, errors.New("Invalid Extension")
|
||||
return nil, fmt.Errorf("Invalid extension: %s", fileExtension)
|
||||
}
|
||||
}
|
||||
|
||||
// Returns embedded metadata of the provided file. An error will be returned if
|
||||
// the file is not supported.
|
||||
func GetMetadata(filepath string) (*MetadataInfo, error) {
|
||||
// Detect Extension Type
|
||||
fileMime, err := mimetype.DetectFile(filepath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get Extension Type Metadata Handler
|
||||
fileExtension := fileMime.Extension()
|
||||
handler, ok := extensionHandlerMap[DocumentType(fileExtension)]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid extension %s", fileExtension)
|
||||
}
|
||||
|
||||
// Acquire Metadata
|
||||
metadataInfo, err := handler(filepath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to acquire metadata")
|
||||
}
|
||||
|
||||
// Calculate MD5 & Partial MD5
|
||||
partialMD5, err := utils.CalculatePartialMD5(filepath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to calculate partial MD5")
|
||||
}
|
||||
|
||||
// Calculate Actual MD5
|
||||
MD5, err := utils.CalculateMD5(filepath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to calculate MD5")
|
||||
}
|
||||
|
||||
// Calculate Word Count
|
||||
wordCount, err := GetWordCount(filepath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to calculate word count")
|
||||
}
|
||||
|
||||
metadataInfo.WordCount = wordCount
|
||||
metadataInfo.PartialMD5 = partialMD5
|
||||
metadataInfo.MD5 = MD5
|
||||
|
||||
return metadataInfo, nil
|
||||
}
|
||||
|
||||
// Returns the extension of the provided filepath (e.g. ".epub"). An error
|
||||
// will be returned if the file is not supported.
|
||||
func GetDocumentType(filepath string) (*DocumentType, error) {
|
||||
// Detect Extension Type
|
||||
fileMime, err := mimetype.DetectFile(filepath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Detect
|
||||
fileExtension := fileMime.Extension()
|
||||
docType, ok := ParseDocumentType(fileExtension)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("filetype not supported")
|
||||
}
|
||||
|
||||
return &docType, nil
|
||||
}
|
||||
|
||||
// Returns the extension of the provided file reader (e.g. ".epub"). An error
|
||||
// will be returned if the file is not supported.
|
||||
func GetDocumentTypeReader(r io.Reader) (*DocumentType, error) {
|
||||
// Detect Extension Type
|
||||
fileMime, err := mimetype.DetectReader(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Detect
|
||||
fileExtension := fileMime.Extension()
|
||||
docType, ok := ParseDocumentType(fileExtension)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("filetype not supported")
|
||||
}
|
||||
|
||||
return &docType, nil
|
||||
}
|
||||
|
||||
// Given a filetype string, attempt to resolve a DocumentType
|
||||
func ParseDocumentType(input string) (DocumentType, bool) {
|
||||
validTypes := map[string]DocumentType{
|
||||
string(TYPE_EPUB): TYPE_EPUB,
|
||||
}
|
||||
found, ok := validTypes[input]
|
||||
return found, ok
|
||||
}
|
||||
|
||||
@@ -1,36 +1,46 @@
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetWordCount(t *testing.T) {
|
||||
var want int64 = 30080
|
||||
wordCount, err := countEPUBWords("../_test_files/alice.epub")
|
||||
var desiredCount int64 = 30080
|
||||
actualCount, err := countEPUBWords("../_test_files/alice.epub")
|
||||
|
||||
assert.Nil(t, err, "should have no error")
|
||||
assert.Equal(t, desiredCount, actualCount, "should be correct word count")
|
||||
|
||||
if wordCount != want {
|
||||
t.Fatalf(`Expected: %v, Got: %v, Error: %v`, want, wordCount, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetMetadata(t *testing.T) {
|
||||
metadataInfo, err := getEPUBMetadata("../_test_files/alice.epub")
|
||||
if err != nil {
|
||||
t.Fatalf(`Expected: *MetadataInfo, Got: nil, Error: %v`, err)
|
||||
}
|
||||
desiredTitle := "Alice's Adventures in Wonderland / Illustrated by Arthur Rackham. With a Proem by Austin Dobson"
|
||||
desiredAuthor := "Lewis Carroll"
|
||||
desiredDescription := ""
|
||||
|
||||
want := "Alice's Adventures in Wonderland / Illustrated by Arthur Rackham. With a Proem by Austin Dobson"
|
||||
if *metadataInfo.Title != want {
|
||||
t.Fatalf(`Expected: %v, Got: %v, Error: %v`, want, *metadataInfo.Title, err)
|
||||
}
|
||||
metadataInfo, err := GetMetadata("../_test_files/alice.epub")
|
||||
|
||||
want = "Lewis Carroll"
|
||||
if *metadataInfo.Author != want {
|
||||
t.Fatalf(`Expected: %v, Got: %v, Error: %v`, want, *metadataInfo.Author, err)
|
||||
}
|
||||
|
||||
want = ""
|
||||
if *metadataInfo.Description != want {
|
||||
t.Fatalf(`Expected: %v, Got: %v, Error: %v`, want, *metadataInfo.Description, err)
|
||||
}
|
||||
assert.Nil(t, err, "should have no error")
|
||||
assert.Equal(t, desiredTitle, *metadataInfo.Title, "should be correct title")
|
||||
assert.Equal(t, desiredAuthor, *metadataInfo.Author, "should be correct author")
|
||||
assert.Equal(t, desiredDescription, *metadataInfo.Description, "should be correct author")
|
||||
assert.Equal(t, TYPE_EPUB, metadataInfo.Type, "should be correct type")
|
||||
}
|
||||
|
||||
func TestGetExtension(t *testing.T) {
|
||||
docType, err := GetDocumentType("../_test_files/alice.epub")
|
||||
|
||||
assert.Nil(t, err, "should have no error")
|
||||
assert.Equal(t, TYPE_EPUB, *docType)
|
||||
}
|
||||
|
||||
func TestGetExtensionReader(t *testing.T) {
|
||||
file, _ := os.Open("../_test_files/alice.epub")
|
||||
docType, err := GetDocumentTypeReader(file)
|
||||
|
||||
assert.Nil(t, err, "should have no error")
|
||||
assert.Equal(t, TYPE_EPUB, *docType)
|
||||
}
|
||||
|
||||
@@ -32,24 +32,24 @@ const OLIB_ISBN_LINK_URL string = "https://openlibrary.org/isbn/%s"
|
||||
|
||||
func GetCoverOLIDs(title *string, author *string) ([]string, error) {
|
||||
if title == nil || author == nil {
|
||||
log.Error("[metadata] Invalid Search Query")
|
||||
log.Error("Invalid Search Query")
|
||||
return nil, errors.New("Invalid Query")
|
||||
}
|
||||
|
||||
searchQuery := url.QueryEscape(fmt.Sprintf("%s %s", *title, *author))
|
||||
apiQuery := fmt.Sprintf(OLIB_QUERY_URL, searchQuery)
|
||||
|
||||
log.Info("[metadata] Acquiring CoverID")
|
||||
log.Info("Acquiring CoverID")
|
||||
resp, err := http.Get(apiQuery)
|
||||
if err != nil {
|
||||
log.Error("[metadata] Cover URL API Failure")
|
||||
log.Error("Cover URL API Failure")
|
||||
return nil, errors.New("API Failure")
|
||||
}
|
||||
|
||||
target := oLibQueryResponse{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&target)
|
||||
if err != nil {
|
||||
log.Error("[metadata] Cover URL API Decode Failure")
|
||||
log.Error("Cover URL API Decode Failure")
|
||||
return nil, errors.New("API Failure")
|
||||
}
|
||||
|
||||
@@ -73,24 +73,24 @@ func DownloadAndSaveCover(coverID string, dirPath string) (*string, error) {
|
||||
// Validate File Doesn't Exists
|
||||
_, err := os.Stat(safePath)
|
||||
if err == nil {
|
||||
log.Warn("[metadata] File Alreads Exists")
|
||||
log.Warn("File Alreads Exists")
|
||||
return &safePath, nil
|
||||
}
|
||||
|
||||
// Create File
|
||||
out, err := os.Create(safePath)
|
||||
if err != nil {
|
||||
log.Error("[metadata] File Create Error")
|
||||
log.Error("File Create Error")
|
||||
return nil, errors.New("File Failure")
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// Download File
|
||||
log.Info("[metadata] Downloading Cover")
|
||||
log.Info("Downloading Cover")
|
||||
coverURL := fmt.Sprintf(OLIB_OLID_COVER_URL, coverID)
|
||||
resp, err := http.Get(coverURL)
|
||||
if err != nil {
|
||||
log.Error("[metadata] Cover URL API Failure")
|
||||
log.Error("Cover URL API Failure")
|
||||
return nil, errors.New("API Failure")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
@@ -98,7 +98,7 @@ func DownloadAndSaveCover(coverID string, dirPath string) (*string, error) {
|
||||
// Copy File to Disk
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
if err != nil {
|
||||
log.Error("[metadata] File Copy Error")
|
||||
log.Error("File Copy Error")
|
||||
return nil, errors.New("File Failure")
|
||||
}
|
||||
|
||||
|
||||
151
ngtemplates/common/utils.go
Normal file
151
ngtemplates/common/utils.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Route string
|
||||
|
||||
var (
|
||||
RouteHome Route = "HOME"
|
||||
RouteDocuments Route = "DOCUMENTS"
|
||||
RouteProgress Route = "PROGRESS"
|
||||
RouteActivity Route = "ACTIVITY"
|
||||
RouteSearch Route = "SEARCH"
|
||||
RouteAdmin Route = "ADMIN"
|
||||
RouteAdminImport Route = "ADMIN_IMPORT"
|
||||
RouteAdminUsers Route = "ADMIN_USERS"
|
||||
RouteAdminLogs Route = "ADMIN_LOGS"
|
||||
)
|
||||
|
||||
func (r Route) IsAdmin() bool {
|
||||
return strings.HasPrefix("ADMIN", string(r))
|
||||
}
|
||||
|
||||
func (r Route) Name() string {
|
||||
var pathSplit []string
|
||||
for _, rawPath := range strings.Split(string(r), "_") {
|
||||
pathLoc := strings.ToUpper(rawPath[:1]) + strings.ToLower(rawPath[1:])
|
||||
pathSplit = append(pathSplit, pathLoc)
|
||||
|
||||
}
|
||||
return strings.Join(pathSplit, " - ")
|
||||
}
|
||||
|
||||
type Settings struct {
|
||||
Route Route
|
||||
User string
|
||||
Version string
|
||||
IsAdmin bool
|
||||
SearchEnabled bool
|
||||
}
|
||||
|
||||
type UserMetadata struct {
|
||||
DocumentCount int
|
||||
ActivityCount int
|
||||
ProgressCount int
|
||||
DeviceCount int
|
||||
}
|
||||
|
||||
type UserStatistics struct {
|
||||
WPM map[string][]UserStatisticEntry
|
||||
Duration map[string][]UserStatisticEntry
|
||||
Words map[string][]UserStatisticEntry
|
||||
}
|
||||
|
||||
type UserStatisticEntry struct {
|
||||
UserID string
|
||||
Value string
|
||||
}
|
||||
|
||||
// getTimeZones returns a string slice of IANA timezones.
|
||||
func GetTimeZones() []string {
|
||||
return []string{
|
||||
"Africa/Cairo",
|
||||
"Africa/Johannesburg",
|
||||
"Africa/Lagos",
|
||||
"Africa/Nairobi",
|
||||
"America/Adak",
|
||||
"America/Anchorage",
|
||||
"America/Buenos_Aires",
|
||||
"America/Chicago",
|
||||
"America/Denver",
|
||||
"America/Los_Angeles",
|
||||
"America/Mexico_City",
|
||||
"America/New_York",
|
||||
"America/Nuuk",
|
||||
"America/Phoenix",
|
||||
"America/Puerto_Rico",
|
||||
"America/Sao_Paulo",
|
||||
"America/St_Johns",
|
||||
"America/Toronto",
|
||||
"Asia/Dubai",
|
||||
"Asia/Hong_Kong",
|
||||
"Asia/Kolkata",
|
||||
"Asia/Seoul",
|
||||
"Asia/Shanghai",
|
||||
"Asia/Singapore",
|
||||
"Asia/Tokyo",
|
||||
"Atlantic/Azores",
|
||||
"Australia/Melbourne",
|
||||
"Australia/Sydney",
|
||||
"Europe/Berlin",
|
||||
"Europe/London",
|
||||
"Europe/Moscow",
|
||||
"Europe/Paris",
|
||||
"Pacific/Auckland",
|
||||
"Pacific/Honolulu",
|
||||
}
|
||||
}
|
||||
|
||||
// niceSeconds takes in an int (in seconds) and returns a string readable
|
||||
// representation. For example 1928371 -> "22d 7h 39m 31s".
|
||||
func NiceSeconds(input int64) (result string) {
|
||||
if input == 0 {
|
||||
return "N/A"
|
||||
}
|
||||
|
||||
days := math.Floor(float64(input) / 60 / 60 / 24)
|
||||
seconds := input % (60 * 60 * 24)
|
||||
hours := math.Floor(float64(seconds) / 60 / 60)
|
||||
seconds = input % (60 * 60)
|
||||
minutes := math.Floor(float64(seconds) / 60)
|
||||
seconds = input % 60
|
||||
|
||||
if days > 0 {
|
||||
result += fmt.Sprintf("%dd ", int(days))
|
||||
}
|
||||
if hours > 0 {
|
||||
result += fmt.Sprintf("%dh ", int(hours))
|
||||
}
|
||||
if minutes > 0 {
|
||||
result += fmt.Sprintf("%dm ", int(minutes))
|
||||
}
|
||||
if seconds > 0 {
|
||||
result += fmt.Sprintf("%ds", int(seconds))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// niceNumbers takes in an int and returns a string representation. For example
|
||||
// 19823 -> "19.8k".
|
||||
func NiceNumbers(input int64) string {
|
||||
if input == 0 {
|
||||
return "0"
|
||||
}
|
||||
|
||||
abbreviations := []string{"", "k", "M", "B", "T"}
|
||||
abbrevIndex := int(math.Log10(float64(input)) / 3)
|
||||
scaledNumber := float64(input) / math.Pow(10, float64(abbrevIndex*3))
|
||||
|
||||
if scaledNumber >= 100 {
|
||||
return fmt.Sprintf("%.0f%s", scaledNumber, abbreviations[abbrevIndex])
|
||||
} else if scaledNumber >= 10 {
|
||||
return fmt.Sprintf("%.1f%s", scaledNumber, abbreviations[abbrevIndex])
|
||||
} else {
|
||||
return fmt.Sprintf("%.2f%s", scaledNumber, abbreviations[abbrevIndex])
|
||||
}
|
||||
}
|
||||
48
ngtemplates/components/buttom.templ
Normal file
48
ngtemplates/components/buttom.templ
Normal file
@@ -0,0 +1,48 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type ButtonVariant string
|
||||
|
||||
const (
|
||||
ButtonVariantPrimary ButtonVariant = "PRIMARY"
|
||||
ButtonVariantSecondary ButtonVariant = "SECONDAY"
|
||||
|
||||
baseClass string = "transition duration-100 ease-in font-medium w-full h-full px-2 py-1 text-white"
|
||||
)
|
||||
|
||||
func (v ButtonVariant) getClass() string {
|
||||
variantClass, ok := variantClassMap[v]
|
||||
if !ok {
|
||||
variantClass = variantClassMap[ButtonVariantPrimary]
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %s", variantClass, baseClass)
|
||||
}
|
||||
|
||||
var variantClassMap = map[ButtonVariant]string{
|
||||
ButtonVariantPrimary: "bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100",
|
||||
ButtonVariantSecondary: "bg-black shadow-md hover:text-black hover:bg-white",
|
||||
}
|
||||
|
||||
templ ButtonForm(title, formName string, variant ButtonVariant) {
|
||||
<button
|
||||
class={ variant.getClass() }
|
||||
type="submit"
|
||||
if formName != "" {
|
||||
form={ formName }
|
||||
}
|
||||
>
|
||||
{ title }
|
||||
</button>
|
||||
}
|
||||
|
||||
templ Button(title string, variant ButtonVariant) {
|
||||
@ButtonForm(title, "", variant)
|
||||
}
|
||||
|
||||
templ ButtonLink(title, url string, variant ButtonVariant) {
|
||||
<a href={ templ.SafeURL(url) } class={ "text-center", variant.getClass() } type="submit">{ title }</a>
|
||||
}
|
||||
219
ngtemplates/components/buttom_templ.go
Normal file
219
ngtemplates/components/buttom_templ.go
Normal file
@@ -0,0 +1,219 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.819
|
||||
package components
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type ButtonVariant string
|
||||
|
||||
const (
|
||||
ButtonVariantPrimary ButtonVariant = "PRIMARY"
|
||||
ButtonVariantSecondary ButtonVariant = "SECONDAY"
|
||||
|
||||
baseClass string = "transition duration-100 ease-in font-medium w-full h-full px-2 py-1 text-white"
|
||||
)
|
||||
|
||||
func (v ButtonVariant) getClass() string {
|
||||
variantClass, ok := variantClassMap[v]
|
||||
if !ok {
|
||||
variantClass = variantClassMap[ButtonVariantPrimary]
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %s", variantClass, baseClass)
|
||||
}
|
||||
|
||||
var variantClassMap = map[ButtonVariant]string{
|
||||
ButtonVariantPrimary: "bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100",
|
||||
ButtonVariantSecondary: "bg-black shadow-md hover:text-black hover:bg-white",
|
||||
}
|
||||
|
||||
func ButtonForm(title, formName string, variant ButtonVariant) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var2 = []any{variant.getClass()}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<button class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var2).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/buttom.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" type=\"submit\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if formName != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " form=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(formName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/buttom.templ`, Line: 35, Col: 18}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, ">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/buttom.templ`, Line: 38, Col: 9}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</button>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func Button(title string, variant ButtonVariant) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var6 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var6 == nil {
|
||||
templ_7745c5c3_Var6 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = ButtonForm(title, "", variant).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func ButtonLink(title, url string, variant ButtonVariant) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var7 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var7 == nil {
|
||||
templ_7745c5c3_Var7 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var8 = []any{"text-center", variant.getClass()}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 templ.SafeURL = templ.SafeURL(url)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var9)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var8).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/buttom.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" type=\"submit\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/buttom.templ`, Line: 47, Col: 97}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
47
ngtemplates/components/daily_read_card.templ
Normal file
47
ngtemplates/components/daily_read_card.templ
Normal file
@@ -0,0 +1,47 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reichard.io/antholume/graph"
|
||||
)
|
||||
|
||||
templ DailyReadChart(dailyReadSVG graph.SVGGraphData) {
|
||||
<div class="relative w-full bg-white shadow-lg dark:bg-gray-700 rounded">
|
||||
<p class="absolute top-3 left-5 text-sm font-semibold text-gray-700 border-b border-gray-200 w-max dark:text-white dark:border-gray-500">
|
||||
Daily Read Totals
|
||||
</p>
|
||||
<div class="relative">
|
||||
<svg viewBox={ fmt.Sprintf("26 0 755 %d", dailyReadSVG.Height) } preserveAspectRatio="none" width="100%" height="6em">
|
||||
<!-- Bezier Line Graph -->
|
||||
<path
|
||||
fill="#316BBE"
|
||||
fill-opacity="0.5"
|
||||
stroke="none"
|
||||
d={ fmt.Sprintf("%s %s", dailyReadSVG.BezierPath, dailyReadSVG.BezierFill) }
|
||||
></path>
|
||||
<path fill="none" stroke="#316BBE" d={ dailyReadSVG.BezierPath }></path>
|
||||
</svg>
|
||||
<div
|
||||
class="flex absolute w-full h-full top-0"
|
||||
style="width: calc(100%*31/30); transform: translateX(-50%); left: 50%"
|
||||
>
|
||||
<!-- Required for iOS "Hover" Events (onclick) -->
|
||||
for _, item := range dailyReadSVG.LinePoints {
|
||||
<div
|
||||
onclick
|
||||
class="opacity-0 hover:opacity-100 w-full"
|
||||
style="background: linear-gradient(rgba(128, 128, 128, 0.5), rgba(128, 128, 128, 0.5)) no-repeat center/2px 100%"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col items-center p-2 rounded absolute top-3 dark:text-white text-xs pointer-events-none"
|
||||
style="transform: translateX(-50%); background-color: rgba(128, 128, 128, 0.2); left: 50%"
|
||||
>
|
||||
<span>{ item.Data.Label }</span>
|
||||
<span>{ fmt.Sprint(item.Data.Value) } minutes</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
120
ngtemplates/components/daily_read_card_templ.go
Normal file
120
ngtemplates/components/daily_read_card_templ.go
Normal file
@@ -0,0 +1,120 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.819
|
||||
package components
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reichard.io/antholume/graph"
|
||||
)
|
||||
|
||||
func DailyReadChart(dailyReadSVG graph.SVGGraphData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"relative w-full bg-white shadow-lg dark:bg-gray-700 rounded\"><p class=\"absolute top-3 left-5 text-sm font-semibold text-gray-700 border-b border-gray-200 w-max dark:text-white dark:border-gray-500\">Daily Read Totals</p><div class=\"relative\"><svg viewBox=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("26 0 755 %d", dailyReadSVG.Height))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/daily_read_card.templ`, Line: 14, Col: 65}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" preserveAspectRatio=\"none\" width=\"100%\" height=\"6em\"><!-- Bezier Line Graph --><path fill=\"#316BBE\" fill-opacity=\"0.5\" stroke=\"none\" d=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s %s", dailyReadSVG.BezierPath, dailyReadSVG.BezierFill))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/daily_read_card.templ`, Line: 20, Col: 79}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"></path> <path fill=\"none\" stroke=\"#316BBE\" d=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(dailyReadSVG.BezierPath)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/daily_read_card.templ`, Line: 22, Col: 66}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\"></path></svg><div class=\"flex absolute w-full h-full top-0\" style=\"width: calc(100%*31/30); transform: translateX(-50%); left: 50%\"><!-- Required for iOS \"Hover\" Events (onclick) -->")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, item := range dailyReadSVG.LinePoints {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div onclick class=\"opacity-0 hover:opacity-100 w-full\" style=\"background: linear-gradient(rgba(128, 128, 128, 0.5), rgba(128, 128, 128, 0.5)) no-repeat center/2px 100%\"><div class=\"flex flex-col items-center p-2 rounded absolute top-3 dark:text-white text-xs pointer-events-none\" style=\"transform: translateX(-50%); background-color: rgba(128, 128, 128, 0.2); left: 50%\"><span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(item.Data.Label)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/daily_read_card.templ`, Line: 39, Col: 30}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</span> <span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(item.Data.Value))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/daily_read_card.templ`, Line: 40, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " minutes</span></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
81
ngtemplates/components/document_card.templ
Normal file
81
ngtemplates/components/document_card.templ
Normal file
@@ -0,0 +1,81 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reichard.io/antholume/database"
|
||||
"reichard.io/antholume/ngtemplates/common"
|
||||
)
|
||||
|
||||
func FirstNonZero[T comparable](all ...T) T {
|
||||
var zeroT T
|
||||
for _, item := range all {
|
||||
if item != zeroT {
|
||||
return item
|
||||
}
|
||||
}
|
||||
return zeroT
|
||||
}
|
||||
|
||||
func orUnknown(val *string) string {
|
||||
if val == nil {
|
||||
return "Unknown"
|
||||
}
|
||||
return *val
|
||||
}
|
||||
|
||||
templ DocumentCard(document database.GetDocumentsWithStatsRow) {
|
||||
<div class="w-full relative">
|
||||
<div
|
||||
class="flex gap-4 w-full h-full p-4 shadow-lg bg-white dark:bg-gray-700 rounded"
|
||||
>
|
||||
<div class="min-w-fit my-auto h-48 relative">
|
||||
<a href={ templ.SafeURL(fmt.Sprintf("./documents/%s", document.ID)) }>
|
||||
<img
|
||||
class="rounded object-cover h-full"
|
||||
src={ fmt.Sprintf("./documents/%s/cover", document.ID) }
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex flex-col justify-around dark:text-white w-full text-sm">
|
||||
<div class="inline-flex shrink-0 items-center">
|
||||
<div>
|
||||
<p class="text-gray-400">Title</p>
|
||||
<p class="font-medium">{ orUnknown(document.Title) }</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-flex shrink-0 items-center">
|
||||
<div>
|
||||
<p class="text-gray-400">Author</p>
|
||||
<p class="font-medium">{ orUnknown(document.Author) }</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-flex shrink-0 items-center">
|
||||
<div>
|
||||
<p class="text-gray-400">Progress</p>
|
||||
<p class="font-medium">{ fmt.Sprintf("%.2f%%", document.Percentage) }</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-flex shrink-0 items-center">
|
||||
<div>
|
||||
<p class="text-gray-400">Time Read</p>
|
||||
<p class="font-medium">{ common.NiceSeconds(document.TotalTimeSeconds) }</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="absolute flex flex-col gap-2 right-4 bottom-4 text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<a href="./activity?document={{ .ID }}">
|
||||
@ActivitySVG("")
|
||||
</a>
|
||||
if document.Filepath != nil && *document.Filepath != "" {
|
||||
<a href={ templ.SafeURL(fmt.Sprintf("./documents/%s/file", document.ID)) }>
|
||||
@DownloadSVG("")
|
||||
</a>
|
||||
} else {
|
||||
@DownloadSVG("text-gray-200 dark:text-gray-600")
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
177
ngtemplates/components/document_card_templ.go
Normal file
177
ngtemplates/components/document_card_templ.go
Normal file
@@ -0,0 +1,177 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.819
|
||||
package components
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reichard.io/antholume/database"
|
||||
"reichard.io/antholume/ngtemplates/common"
|
||||
)
|
||||
|
||||
func FirstNonZero[T comparable](all ...T) T {
|
||||
var zeroT T
|
||||
for _, item := range all {
|
||||
if item != zeroT {
|
||||
return item
|
||||
}
|
||||
}
|
||||
return zeroT
|
||||
}
|
||||
|
||||
func orUnknown(val *string) string {
|
||||
if val == nil {
|
||||
return "Unknown"
|
||||
}
|
||||
return *val
|
||||
}
|
||||
|
||||
func DocumentCard(document database.GetDocumentsWithStatsRow) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"w-full relative\"><div class=\"flex gap-4 w-full h-full p-4 shadow-lg bg-white dark:bg-gray-700 rounded\"><div class=\"min-w-fit my-auto h-48 relative\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 templ.SafeURL = templ.SafeURL(fmt.Sprintf("./documents/%s", document.ID))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var2)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"><img class=\"rounded object-cover h-full\" src=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("./documents/%s/cover", document.ID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/document_card.templ`, Line: 35, Col: 60}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"></a></div><div class=\"flex flex-col justify-around dark:text-white w-full text-sm\"><div class=\"inline-flex shrink-0 items-center\"><div><p class=\"text-gray-400\">Title</p><p class=\"font-medium\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(orUnknown(document.Title))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/document_card.templ`, Line: 43, Col: 56}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</p></div></div><div class=\"inline-flex shrink-0 items-center\"><div><p class=\"text-gray-400\">Author</p><p class=\"font-medium\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(orUnknown(document.Author))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/document_card.templ`, Line: 49, Col: 57}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</p></div></div><div class=\"inline-flex shrink-0 items-center\"><div><p class=\"text-gray-400\">Progress</p><p class=\"font-medium\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.2f%%", document.Percentage))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/document_card.templ`, Line: 55, Col: 73}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</p></div></div><div class=\"inline-flex shrink-0 items-center\"><div><p class=\"text-gray-400\">Time Read</p><p class=\"font-medium\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(common.NiceSeconds(document.TotalTimeSeconds))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/document_card.templ`, Line: 61, Col: 76}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</p></div></div></div><div class=\"absolute flex flex-col gap-2 right-4 bottom-4 text-gray-500 dark:text-gray-400\"><a href=\"./activity?document={{ .ID }}\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = ActivitySVG("").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if document.Filepath != nil && *document.Filepath != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 templ.SafeURL = templ.SafeURL(fmt.Sprintf("./documents/%s/file", document.ID))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var8)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = DownloadSVG("").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = DownloadSVG("text-gray-200 dark:text-gray-600").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
26
ngtemplates/components/info_card.templ
Normal file
26
ngtemplates/components/info_card.templ
Normal file
@@ -0,0 +1,26 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
templ InfoCard(name string, metric int) {
|
||||
<div class="w-full">
|
||||
@infoCardInner(name, metric)
|
||||
</div>
|
||||
}
|
||||
|
||||
templ InfoCardLink(name string, metric int, link string) {
|
||||
<a href={ templ.SafeURL(link) } class="w-full">
|
||||
@infoCardInner(name, metric)
|
||||
</a>
|
||||
}
|
||||
|
||||
templ infoCardInner(name string, metric int) {
|
||||
<div class="flex gap-4 w-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded">
|
||||
<div class="flex flex-col justify-around dark:text-white w-full text-sm">
|
||||
<p class="text-2xl font-bold text-black dark:text-white">{ fmt.Sprint(metric) }</p>
|
||||
<p class="text-sm text-gray-400">{ name }</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
153
ngtemplates/components/info_card_templ.go
Normal file
153
ngtemplates/components/info_card_templ.go
Normal file
@@ -0,0 +1,153 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.819
|
||||
package components
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func InfoCard(name string, metric int) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"w-full\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = infoCardInner(name, metric).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func InfoCardLink(name string, metric int, link string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var2 == nil {
|
||||
templ_7745c5c3_Var2 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 templ.SafeURL = templ.SafeURL(link)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var3)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" class=\"w-full\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = infoCardInner(name, metric).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func infoCardInner(name string, metric int) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var4 == nil {
|
||||
templ_7745c5c3_Var4 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"flex gap-4 w-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded\"><div class=\"flex flex-col justify-around dark:text-white w-full text-sm\"><p class=\"text-2xl font-bold text-black dark:text-white\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(metric))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/info_card.templ`, Line: 22, Col: 80}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</p><p class=\"text-sm text-gray-400\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/info_card.templ`, Line: 23, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</p></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
91
ngtemplates/components/leaderboard_card.templ
Normal file
91
ngtemplates/components/leaderboard_card.templ
Normal file
@@ -0,0 +1,91 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reichard.io/antholume/ngtemplates/common"
|
||||
)
|
||||
|
||||
templ LeaderboardCard(name string, stats map[string][]common.UserStatisticEntry) {
|
||||
<div class="w-full">
|
||||
<div class="flex flex-col justify-between h-full w-full px-4 py-6 bg-white shadow-lg dark:bg-gray-700 rounded">
|
||||
<div>
|
||||
<div class="flex justify-between">
|
||||
<p class="text-sm font-semibold text-gray-700 border-b border-gray-200 w-max dark:text-white dark:border-gray-500">
|
||||
{ name } Leaderboard
|
||||
</p>
|
||||
<div class="flex gap-2 text-xs text-gray-400 items-center">
|
||||
<label
|
||||
for={ fmt.Sprintf("all-%s", name) }
|
||||
class="cursor-pointer hover:text-black dark:hover:text-white"
|
||||
>all</label>
|
||||
<label
|
||||
for={ fmt.Sprintf("year-%s", name) }
|
||||
class="cursor-pointer hover:text-black dark:hover:text-white"
|
||||
>year</label>
|
||||
<label
|
||||
for={ fmt.Sprintf("month-%s", name) }
|
||||
class="cursor-pointer hover:text-black dark:hover:text-white"
|
||||
>month</label>
|
||||
<label
|
||||
for={ fmt.Sprintf("week-%s", name) }
|
||||
class="cursor-pointer hover:text-black dark:hover:text-white"
|
||||
>week</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="radio"
|
||||
name={ fmt.Sprintf("options-%s", name) }
|
||||
id={ fmt.Sprintf("all-%s", name) }
|
||||
class="hidden peer/All"
|
||||
checked
|
||||
/>
|
||||
<input
|
||||
type="radio"
|
||||
name={ fmt.Sprintf("options-%s", name) }
|
||||
id={ fmt.Sprintf("year-%s", name) }
|
||||
class="hidden peer/Year"
|
||||
/>
|
||||
<input
|
||||
type="radio"
|
||||
name={ fmt.Sprintf("options-%s", name) }
|
||||
id={ fmt.Sprintf("month-%s", name) }
|
||||
class="hidden peer/Month"
|
||||
/>
|
||||
<input
|
||||
type="radio"
|
||||
name={ fmt.Sprintf("options-%s", name) }
|
||||
id={ fmt.Sprintf("week-%s", name) }
|
||||
class="hidden peer/Week"
|
||||
/>
|
||||
for name, data := range stats {
|
||||
<div class={ "flex items-end my-6 space-x-2 hidden", fmt.Sprintf("peer-checked/%s:block", name) }>
|
||||
if len(data) == 0 {
|
||||
<p class="text-5xl font-bold text-black dark:text-white">N/A</p>
|
||||
} else {
|
||||
<p class="text-5xl font-bold text-black dark:text-white">{ data[0].UserID }</p>
|
||||
}
|
||||
</div>
|
||||
<div class={ "hidden dark:text-white", fmt.Sprintf("peer-checked/%s:block", name) }>
|
||||
for idx, item := range data {
|
||||
if idx == 0 {
|
||||
<div class="flex items-center justify-between pt-2 pb-2 text-sm">
|
||||
<div>
|
||||
<p>{ item.UserID }</p>
|
||||
</div>
|
||||
<div class="flex items-end font-bold">{ item.Value }</div>
|
||||
</div>
|
||||
} else if idx < 3 {
|
||||
<div class="flex items-center justify-between pt-2 pb-2 text-sm border-t border-gray-200">
|
||||
<div>
|
||||
<p>{ item.UserID }</p>
|
||||
</div>
|
||||
<div class="flex items-end font-bold">{ item.Value }</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
361
ngtemplates/components/leaderboard_card_templ.go
Normal file
361
ngtemplates/components/leaderboard_card_templ.go
Normal file
@@ -0,0 +1,361 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.819
|
||||
package components
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reichard.io/antholume/ngtemplates/common"
|
||||
)
|
||||
|
||||
func LeaderboardCard(name string, stats map[string][]common.UserStatisticEntry) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"w-full\"><div class=\"flex flex-col justify-between h-full w-full px-4 py-6 bg-white shadow-lg dark:bg-gray-700 rounded\"><div><div class=\"flex justify-between\"><p class=\"text-sm font-semibold text-gray-700 border-b border-gray-200 w-max dark:text-white dark:border-gray-500\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 14, Col: 12}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " Leaderboard</p><div class=\"flex gap-2 text-xs text-gray-400 items-center\"><label for=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("all-%s", name))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 18, Col: 40}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" class=\"cursor-pointer hover:text-black dark:hover:text-white\">all</label> <label for=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("year-%s", name))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 22, Col: 41}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" class=\"cursor-pointer hover:text-black dark:hover:text-white\">year</label> <label for=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("month-%s", name))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 26, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" class=\"cursor-pointer hover:text-black dark:hover:text-white\">month</label> <label for=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("week-%s", name))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 30, Col: 41}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" class=\"cursor-pointer hover:text-black dark:hover:text-white\">week</label></div></div></div><input type=\"radio\" name=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("options-%s", name))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 38, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("all-%s", name))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 39, Col: 36}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" class=\"hidden peer/All\" checked> <input type=\"radio\" name=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("options-%s", name))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 45, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("year-%s", name))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 46, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" class=\"hidden peer/Year\"> <input type=\"radio\" name=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("options-%s", name))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 51, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("month-%s", name))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 52, Col: 38}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" class=\"hidden peer/Month\"> <input type=\"radio\" name=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("options-%s", name))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 57, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("week-%s", name))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 58, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" class=\"hidden peer/Week\"> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for name, data := range stats {
|
||||
var templ_7745c5c3_Var15 = []any{"flex items-end my-6 space-x-2 hidden", fmt.Sprintf("peer-checked/%s:block", name)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<div class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var15).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(data) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<p class=\"text-5xl font-bold text-black dark:text-white\">N/A</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<p class=\"text-5xl font-bold text-black dark:text-white\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(data[0].UserID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 66, Col: 79}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 = []any{"hidden dark:text-white", fmt.Sprintf("peer-checked/%s:block", name)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var18...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<div class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var18).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for idx, item := range data {
|
||||
if idx == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<div class=\"flex items-center justify-between pt-2 pb-2 text-sm\"><div><p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var20 string
|
||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(item.UserID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 74, Col: 25}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</p></div><div class=\"flex items-end font-bold\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(item.Value)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 76, Col: 58}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else if idx < 3 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<div class=\"flex items-center justify-between pt-2 pb-2 text-sm border-t border-gray-200\"><div><p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var22 string
|
||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(item.UserID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 81, Col: 25}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</p></div><div class=\"flex items-end font-bold\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var23 string
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(item.Value)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/leaderboard_card.templ`, Line: 83, Col: 58}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
57
ngtemplates/components/streak_card.templ
Normal file
57
ngtemplates/components/streak_card.templ
Normal file
@@ -0,0 +1,57 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reichard.io/antholume/database"
|
||||
)
|
||||
|
||||
templ StreakCard(streak database.UserStreak) {
|
||||
<div class="w-full">
|
||||
<div class="relative w-full px-4 py-6 bg-white shadow-lg dark:bg-gray-700 rounded">
|
||||
<p class="text-sm font-semibold text-gray-700 border-b border-gray-200 w-max dark:text-white dark:border-gray-500">
|
||||
if streak.Window == "WEEK" {
|
||||
Weekly Read Streak
|
||||
} else {
|
||||
Daily Read Streak
|
||||
}
|
||||
</p>
|
||||
<div class="flex items-end my-6 space-x-2">
|
||||
<p class="text-5xl font-bold text-black dark:text-white">
|
||||
{ fmt.Sprint(streak.CurrentStreak) }
|
||||
</p>
|
||||
</div>
|
||||
<div class="dark:text-white">
|
||||
<div class="flex items-center justify-between pb-2 mb-2 text-sm border-b border-gray-200">
|
||||
<div>
|
||||
<p>
|
||||
if streak.Window == "WEEK" {
|
||||
Current Read Streak
|
||||
} else {
|
||||
Current Read Streak
|
||||
}
|
||||
</p>
|
||||
<div class="flex items-end text-sm text-gray-400">
|
||||
{ streak.CurrentStreakStartDate } ➞ { streak.CurrentStreakEndDate }
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end font-bold">{ fmt.Sprint(streak.CurrentStreak) }</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between pb-2 mb-2 text-sm">
|
||||
<div>
|
||||
<p>
|
||||
if streak.Window == "WEEK" {
|
||||
Best Weekly Streak
|
||||
} else {
|
||||
Best Daily Streak
|
||||
}
|
||||
</p>
|
||||
<div class="flex items-end text-sm text-gray-400">
|
||||
{ streak.MaxStreakStartDate } ➞ { streak.MaxStreakEndDate }
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end font-bold">{ fmt.Sprint(streak.MaxStreak) }</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
181
ngtemplates/components/streak_card_templ.go
Normal file
181
ngtemplates/components/streak_card_templ.go
Normal file
@@ -0,0 +1,181 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.819
|
||||
package components
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reichard.io/antholume/database"
|
||||
)
|
||||
|
||||
func StreakCard(streak database.UserStreak) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"w-full\"><div class=\"relative w-full px-4 py-6 bg-white shadow-lg dark:bg-gray-700 rounded\"><p class=\"text-sm font-semibold text-gray-700 border-b border-gray-200 w-max dark:text-white dark:border-gray-500\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if streak.Window == "WEEK" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "Weekly Read Streak")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "Daily Read Streak")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</p><div class=\"flex items-end my-6 space-x-2\"><p class=\"text-5xl font-bold text-black dark:text-white\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(streak.CurrentStreak))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/streak_card.templ`, Line: 20, Col: 39}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</p></div><div class=\"dark:text-white\"><div class=\"flex items-center justify-between pb-2 mb-2 text-sm border-b border-gray-200\"><div><p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if streak.Window == "WEEK" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "Current Read Streak")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "Current Read Streak")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</p><div class=\"flex items-end text-sm text-gray-400\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(streak.CurrentStreakStartDate)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/streak_card.templ`, Line: 34, Col: 38}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " ➞ ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(streak.CurrentStreakEndDate)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/streak_card.templ`, Line: 34, Col: 74}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div></div><div class=\"flex items-end font-bold\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(streak.CurrentStreak))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/streak_card.templ`, Line: 37, Col: 77}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div></div><div class=\"flex items-center justify-between pb-2 mb-2 text-sm\"><div><p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if streak.Window == "WEEK" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "Best Weekly Streak")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "Best Daily Streak")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</p><div class=\"flex items-end text-sm text-gray-400\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(streak.MaxStreakStartDate)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/streak_card.templ`, Line: 49, Col: 34}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " ➞ ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(streak.MaxStreakEndDate)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/streak_card.templ`, Line: 49, Col: 66}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div></div><div class=\"flex items-end font-bold\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(streak.MaxStreak))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/streak_card.templ`, Line: 52, Col: 73}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</div></div></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
342
ngtemplates/components/svgs.templ
Normal file
342
ngtemplates/components/svgs.templ
Normal file
@@ -0,0 +1,342 @@
|
||||
package components
|
||||
|
||||
templ ActivitySVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M9.5 2C8.67157 2 8 2.67157 8 3.5V4.5C8 5.32843 8.67157 6 9.5 6H14.5C15.3284 6 16 5.32843 16 4.5V3.5C16 2.67157 15.3284 2 14.5 2H9.5Z"></path>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.5 4.03662C5.24209 4.10719 4.44798 4.30764 3.87868 4.87694C3 5.75562 3 7.16983 3 9.99826V15.9983C3 18.8267 3 20.2409 3.87868 21.1196C4.75736 21.9983 6.17157 21.9983 9 21.9983H15C17.8284 21.9983 19.2426 21.9983 20.1213 21.1196C21 20.2409 21 18.8267 21 15.9983V9.99826C21 7.16983 21 5.75562 20.1213 4.87694C19.552 4.30764 18.7579 4.10719 17.5 4.03662V4.5C17.5 6.15685 16.1569 7.5 14.5 7.5H9.5C7.84315 7.5 6.5 6.15685 6.5 4.5V4.03662ZM7 9.75C6.58579 9.75 6.25 10.0858 6.25 10.5C6.25 10.9142 6.58579 11.25 7 11.25H7.5C7.91421 11.25 8.25 10.9142 8.25 10.5C8.25 10.0858 7.91421 9.75 7.5 9.75H7ZM10.5 9.75C10.0858 9.75 9.75 10.0858 9.75 10.5C9.75 10.9142 10.0858 11.25 10.5 11.25H17C17.4142 11.25 17.75 10.9142 17.75 10.5C17.75 10.0858 17.4142 9.75 17 9.75H10.5ZM7 13.25C6.58579 13.25 6.25 13.5858 6.25 14C6.25 14.4142 6.58579 14.75 7 14.75H7.5C7.91421 14.75 8.25 14.4142 8.25 14C8.25 13.5858 7.91421 13.25 7.5 13.25H7ZM10.5 13.25C10.0858 13.25 9.75 13.5858 9.75 14C9.75 14.4142 10.0858 14.75 10.5 14.75H17C17.4142 14.75 17.75 14.4142 17.75 14C17.75 13.5858 17.4142 13.25 17 13.25H10.5ZM7 16.75C6.58579 16.75 6.25 17.0858 6.25 17.5C6.25 17.9142 6.58579 18.25 7 18.25H7.5C7.91421 18.25 8.25 17.9142 8.25 17.5C8.25 17.0858 7.91421 16.75 7.5 16.75H7ZM10.5 16.75C10.0858 16.75 9.75 17.0858 9.75 17.5C9.75 17.9142 10.0858 18.25 10.5 18.25H17C17.4142 18.25 17.75 17.9142 17.75 17.5C17.75 17.0858 17.4142 16.75 17 16.75H10.5Z"></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ AddSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22ZM12 8.25C12.4142 8.25 12.75 8.58579 12.75 9V11.25H15C15.4142 11.25 15.75 11.5858 15.75 12C15.75 12.4142 15.4142 12.75 15 12.75H12.75L12.75 15C12.75 15.4142 12.4142 15.75 12 15.75C11.5858 15.75 11.25 15.4142 11.25 15V12.75H9C8.58579 12.75 8.25 12.4142 8.25 12C8.25 11.5858 8.58579 11.25 9 11.25H11.25L11.25 9C11.25 8.58579 11.5858 8.25 12 8.25Z"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ ClockSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z"
|
||||
></path>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12 7.25C12.4142 7.25 12.75 7.58579 12.75 8V11.6893L15.0303 13.9697C15.3232 14.2626 15.3232 14.7374 15.0303 15.0303C14.7374 15.3232 14.2626 15.3232 13.9697 15.0303L11.4697 12.5303C11.329 12.3897 11.25 12.1989 11.25 12V8C11.25 7.58579 11.5858 7.25 12 7.25Z"
|
||||
fill="white"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ DeleteSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M3 6.52381C3 6.12932 3.32671 5.80952 3.72973 5.80952H8.51787C8.52437 4.9683 8.61554 3.81504 9.45037 3.01668C10.1074 2.38839 11.0081 2 12 2C12.9919 2 13.8926 2.38839 14.5496 3.01668C15.3844 3.81504 15.4756 4.9683 15.4821 5.80952H20.2703C20.6733 5.80952 21 6.12932 21 6.52381C21 6.9183 20.6733 7.2381 20.2703 7.2381H3.72973C3.32671 7.2381 3 6.9183 3 6.52381Z"
|
||||
></path>
|
||||
<path
|
||||
d="M11.6066 22H12.3935C15.101 22 16.4547 22 17.3349 21.1368C18.2151 20.2736 18.3052 18.8576 18.4853 16.0257L18.7448 11.9452C18.8425 10.4086 18.8913 9.64037 18.4498 9.15352C18.0082 8.66667 17.2625 8.66667 15.7712 8.66667H8.22884C6.7375 8.66667 5.99183 8.66667 5.55026 9.15352C5.1087 9.64037 5.15756 10.4086 5.25528 11.9452L5.51479 16.0257C5.69489 18.8576 5.78494 20.2736 6.66513 21.1368C7.54532 22 8.89906 22 11.6066 22Z"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ DocumentSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.27103 2.11151C5.46135 2.21816 5.03258 2.41324 4.72718 2.71244C4.42179 3.01165 4.22268 3.43172 4.11382 4.225C4.00176 5.04159 4 6.12387 4 7.67568V16.2442C4.38867 15.9781 4.82674 15.7756 5.29899 15.6517C5.82716 15.513 6.44305 15.5132 7.34563 15.5135L20 15.5135V7.67568C20 6.12387 19.9982 5.04159 19.8862 4.22499C19.7773 3.43172 19.5782 3.01165 19.2728 2.71244C18.9674 2.41324 18.5387 2.21816 17.729 2.11151C16.8955 2.00172 15.7908 2 14.2069 2H9.7931C8.2092 2 7.10452 2.00172 6.27103 2.11151ZM6.75862 6.59459C6.75862 6.1468 7.12914 5.78378 7.58621 5.78378H16.4138C16.8709 5.78378 17.2414 6.1468 17.2414 6.59459C17.2414 7.04239 16.8709 7.40541 16.4138 7.40541H7.58621C7.12914 7.40541 6.75862 7.04239 6.75862 6.59459ZM7.58621 9.56757C7.12914 9.56757 6.75862 9.93058 6.75862 10.3784C6.75862 10.8262 7.12914 11.1892 7.58621 11.1892H13.1034C13.5605 11.1892 13.931 10.8262 13.931 10.3784C13.931 9.93058 13.5605 9.56757 13.1034 9.56757H7.58621Z"></path>
|
||||
<path d="M7.47341 17.1351H8.68965H13.1034H19.9991C19.9956 18.2657 19.9776 19.1088 19.8862 19.775C19.7773 20.5683 19.5782 20.9884 19.2728 21.2876C18.9674 21.5868 18.5387 21.7818 17.729 21.8885C16.8955 21.9983 15.7908 22 14.2069 22H9.7931C8.2092 22 7.10452 21.9983 6.27103 21.8885C5.46135 21.7818 5.03258 21.5868 4.72718 21.2876C4.42179 20.9884 4.22268 20.5683 4.11382 19.775C4.07259 19.4746 4.0463 19.1382 4.02952 18.7558C4.30088 18.0044 4.93365 17.4264 5.72738 17.218C6.01657 17.1421 6.39395 17.1351 7.47341 17.1351Z"></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ DownloadSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12ZM12 6.25C12.4142 6.25 12.75 6.58579 12.75 7V12.1893L14.4697 10.4697C14.7626 10.1768 15.2374 10.1768 15.5303 10.4697C15.8232 10.7626 15.8232 11.2374 15.5303 11.5303L12.5303 14.5303C12.3897 14.671 12.1989 14.75 12 14.75C11.8011 14.75 11.6103 14.671 11.4697 14.5303L8.46967 11.5303C8.17678 11.2374 8.17678 10.7626 8.46967 10.4697C8.76256 10.1768 9.23744 10.1768 9.53033 10.4697L11.25 12.1893V7C11.25 6.58579 11.5858 6.25 12 6.25ZM8 16.25C7.58579 16.25 7.25 16.5858 7.25 17C7.25 17.4142 7.58579 17.75 8 17.75H16C16.4142 17.75 16.75 17.4142 16.75 17C16.75 16.5858 16.4142 16.25 16 16.25H8Z"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ DropdownSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
viewBox="0 0 1792 1792"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M1408 704q0 26-19 45l-448 448q-19 19-45 19t-45-19l-448-448q-19-19-19-45t19-45 45-19h896q26 0 45 19t19 45z"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ EditSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M21.1938 2.80624C22.2687 3.88124 22.2687 5.62415 21.1938 6.69914L20.6982 7.19469C20.5539 7.16345 20.3722 7.11589 20.1651 7.04404C19.6108 6.85172 18.8823 6.48827 18.197 5.803C17.5117 5.11774 17.1483 4.38923 16.956 3.8349C16.8841 3.62781 16.8366 3.44609 16.8053 3.30179L17.3009 2.80624C18.3759 1.73125 20.1188 1.73125 21.1938 2.80624Z"
|
||||
></path>
|
||||
<path
|
||||
d="M14.5801 13.3128C14.1761 13.7168 13.9741 13.9188 13.7513 14.0926C13.4886 14.2975 13.2043 14.4732 12.9035 14.6166C12.6485 14.7381 12.3775 14.8284 11.8354 15.0091L8.97709 15.9619C8.71035 16.0508 8.41626 15.9814 8.21744 15.7826C8.01862 15.5837 7.9492 15.2897 8.03811 15.0229L8.99089 12.1646C9.17157 11.6225 9.26191 11.3515 9.38344 11.0965C9.52679 10.7957 9.70249 10.5114 9.90743 10.2487C10.0812 10.0259 10.2832 9.82394 10.6872 9.41993L15.6033 4.50385C15.867 5.19804 16.3293 6.05663 17.1363 6.86366C17.9434 7.67069 18.802 8.13296 19.4962 8.39674L14.5801 13.3128Z"
|
||||
></path>
|
||||
<path
|
||||
d="M20.5355 20.5355C22 19.0711 22 16.714 22 12C22 10.4517 22 9.15774 21.9481 8.0661L15.586 14.4283C15.2347 14.7797 14.9708 15.0437 14.6738 15.2753C14.3252 15.5473 13.948 15.7804 13.5488 15.9706C13.2088 16.1327 12.8546 16.2506 12.3833 16.4076L9.45143 17.3849C8.64568 17.6535 7.75734 17.4438 7.15678 16.8432C6.55621 16.2427 6.34651 15.3543 6.61509 14.5486L7.59235 11.6167C7.74936 11.1454 7.86732 10.7912 8.02935 10.4512C8.21958 10.052 8.45272 9.6748 8.72466 9.32615C8.9563 9.02918 9.22032 8.76528 9.57173 8.41404L15.9339 2.05188C14.8423 2 13.5483 2 12 2C7.28595 2 4.92893 2 3.46447 3.46447C2 4.92893 2 7.28595 2 12C2 16.714 2 19.0711 3.46447 20.5355C4.92893 22 7.28595 22 12 22C16.714 22 19.0711 22 20.5355 20.5355Z"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ GitSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
fill="currentColor"
|
||||
viewBox="0 0 219 92"
|
||||
>
|
||||
<defs>
|
||||
<clipPath id="a">
|
||||
<path d="M159 .79h25V69h-25Zm0 0"></path>
|
||||
</clipPath>
|
||||
<clipPath id="b">
|
||||
<path d="M183 9h35.371v60H183Zm0 0"></path>
|
||||
</clipPath>
|
||||
<clipPath id="c">
|
||||
<path d="M0 .79h92V92H0Zm0 0"></path>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<path
|
||||
style="stroke: none; fill-rule: nonzero; fill-opacity: 1"
|
||||
d="M130.871 31.836c-4.785 0-8.351 2.352-8.351 8.008 0 4.261 2.347 7.222 8.093 7.222 4.871 0 8.18-2.867 8.18-7.398 0-5.133-2.961-7.832-7.922-7.832Zm-9.57 39.95c-1.133 1.39-2.262 2.87-2.262 4.612 0 3.48 4.434 4.524 10.527 4.524 5.051 0 11.926-.352 11.926-5.043 0-2.793-3.308-2.965-7.488-3.227Zm25.761-39.688c1.563 2.004 3.22 4.789 3.22 8.793 0 9.656-7.571 15.316-18.536 15.316-2.789 0-5.312-.348-6.879-.785l-2.87 4.613 8.526.52c15.059.96 23.934 1.398 23.934 12.968 0 10.008-8.789 15.665-23.934 15.665-15.75 0-21.757-4.004-21.757-10.88 0-3.917 1.742-6 4.789-8.878-2.875-1.211-3.828-3.387-3.828-5.739 0-1.914.953-3.656 2.523-5.312 1.566-1.652 3.305-3.305 5.395-5.219-4.262-2.09-7.485-6.617-7.485-13.058 0-10.008 6.613-16.88 19.93-16.88 3.742 0 6.004.344 8.008.872h16.972v7.394l-8.007.61"
|
||||
></path>
|
||||
<g clip-path="url(#a)">
|
||||
<path
|
||||
style="stroke: none; fill-rule: nonzero; fill-opacity: 1"
|
||||
d="M170.379 16.281c-4.961 0-7.832-2.87-7.832-7.836 0-4.957 2.871-7.656 7.832-7.656 5.05 0 7.922 2.7 7.922 7.656 0 4.965-2.871 7.836-7.922 7.836Zm-11.227 52.305V61.71l4.438-.606c1.219-.175 1.394-.437 1.394-1.746V33.773c0-.953-.261-1.566-1.132-1.824l-4.7-1.656.957-7.047h18.016V59.36c0 1.399.086 1.57 1.395 1.746l4.437.606v6.875h-24.805"
|
||||
></path>
|
||||
</g>
|
||||
<g clip-path="url(#b)">
|
||||
<path
|
||||
style="stroke: none; fill-rule: nonzero; fill-opacity: 1"
|
||||
d="M218.371 65.21c-3.742 1.825-9.223 3.481-14.187 3.481-10.356 0-14.27-4.175-14.27-14.015V31.879c0-.524 0-.871-.7-.871h-6.093v-7.746c7.664-.871 10.707-4.703 11.664-14.188h8.27v12.36c0 .609 0 .87.695.87h12.27v8.704h-12.965v20.797c0 5.136 1.218 7.136 5.918 7.136 2.437 0 4.96-.609 7.047-1.39l2.351 7.66"
|
||||
></path>
|
||||
</g>
|
||||
<g clip-path="url(#c)">
|
||||
<path
|
||||
style="stroke: none; fill-rule: nonzero; fill-opacity: 1"
|
||||
d="M89.422 42.371 49.629 2.582a5.868 5.868 0 0 0-8.3 0l-8.263 8.262 10.48 10.484a6.965 6.965 0 0 1 7.173 1.668 6.98 6.98 0 0 1 1.656 7.215l10.102 10.105a6.963 6.963 0 0 1 7.214 1.657 6.976 6.976 0 0 1 0 9.875 6.98 6.98 0 0 1-9.879 0 6.987 6.987 0 0 1-1.519-7.594l-9.422-9.422v24.793a6.979 6.979 0 0 1 1.848 1.32 6.988 6.988 0 0 1 0 9.88c-2.73 2.726-7.153 2.726-9.875 0a6.98 6.98 0 0 1 0-9.88 6.893 6.893 0 0 1 2.285-1.523V34.398a6.893 6.893 0 0 1-2.285-1.523 6.988 6.988 0 0 1-1.508-7.637L29.004 14.902 1.719 42.187a5.868 5.868 0 0 0 0 8.301l39.793 39.793a5.868 5.868 0 0 0 8.3 0l39.61-39.605a5.873 5.873 0 0 0 0-8.305"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ HomeSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.5192 7.82274C2 8.77128 2 9.91549 2 12.2039V13.725C2 17.6258 2 19.5763 3.17157 20.7881C4.34315 22 6.22876 22 10 22H14C17.7712 22 19.6569 22 20.8284 20.7881C22 19.5763 22 17.6258 22 13.725V12.2039C22 9.91549 22 8.77128 21.4808 7.82274C20.9616 6.87421 20.0131 6.28551 18.116 5.10812L16.116 3.86687C14.1106 2.62229 13.1079 2 12 2C10.8921 2 9.88939 2.62229 7.88403 3.86687L5.88403 5.10813C3.98695 6.28551 3.0384 6.87421 2.5192 7.82274ZM11.25 18C11.25 18.4142 11.5858 18.75 12 18.75C12.4142 18.75 12.75 18.4142 12.75 18V15C12.75 14.5858 12.4142 14.25 12 14.25C11.5858 14.25 11.25 14.5858 11.25 15V18Z"></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ ImportSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2.06935 5.00839C2 5.37595 2 5.81722 2 6.69975V13.75C2 17.5212 2 19.4069 3.17157 20.5784C4.34315 21.75 6.22876 21.75 10 21.75H14C17.7712 21.75 19.6569 21.75 20.8284 20.5784C22 19.4069 22 17.5212 22 13.75V11.5479C22 8.91554 22 7.59935 21.2305 6.74383C21.1598 6.66514 21.0849 6.59024 21.0062 6.51946C20.1506 5.75 18.8345 5.75 16.2021 5.75H15.8284C14.6747 5.75 14.0979 5.75 13.5604 5.59678C13.2651 5.5126 12.9804 5.39471 12.7121 5.24543C12.2237 4.97367 11.8158 4.56578 11 3.75L10.4497 3.19975C10.1763 2.92633 10.0396 2.78961 9.89594 2.67051C9.27652 2.15704 8.51665 1.84229 7.71557 1.76738C7.52976 1.75 7.33642 1.75 6.94975 1.75C6.06722 1.75 5.62595 1.75 5.25839 1.81935C3.64031 2.12464 2.37464 3.39031 2.06935 5.00839ZM12 11C12.4142 11 12.75 11.3358 12.75 11.75V13H14C14.4142 13 14.75 13.3358 14.75 13.75C14.75 14.1642 14.4142 14.5 14 14.5H12.75V15.75C12.75 16.1642 12.4142 16.5 12 16.5C11.5858 16.5 11.25 16.1642 11.25 15.75V14.5H10C9.58579 14.5 9.25 14.1642 9.25 13.75C9.25 13.3358 9.58579 13 10 13H11.25V11.75C11.25 11.3358 11.5858 11 12 11Z"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ InfoSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22ZM12 17.75C12.4142 17.75 12.75 17.4142 12.75 17V11C12.75 10.5858 12.4142 10.25 12 10.25C11.5858 10.25 11.25 10.5858 11.25 11V17C11.25 17.4142 11.5858 17.75 12 17.75ZM12 7C12.5523 7 13 7.44772 13 8C13 8.55228 12.5523 9 12 9C11.4477 9 11 8.55228 11 8C11 7.44772 11.4477 7 12 7Z"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ LoadingSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<style>
|
||||
.spinner_l9ve {
|
||||
animation: spinner_rcyq 1.2s cubic-bezier(0.52, 0.6, 0.25, 0.99) infinite;
|
||||
}
|
||||
.spinner_cMYp {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
.spinner_gHR3 {
|
||||
animation-delay: 0.8s;
|
||||
}
|
||||
@keyframes spinner_rcyq {
|
||||
0% {
|
||||
transform: translate(12px, 12px) scale(0);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<path
|
||||
class="spinner_l9ve"
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,20a9,9,0,1,1,9-9A9,9,0,0,1,12,21Z"
|
||||
transform="translate(12, 12) scale(0)"
|
||||
></path>
|
||||
<path
|
||||
class="spinner_l9ve spinner_cMYp"
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,20a9,9,0,1,1,9-9A9,9,0,0,1,12,21Z"
|
||||
transform="translate(12, 12) scale(0)"
|
||||
></path>
|
||||
<path
|
||||
class="spinner_l9ve spinner_gHR3"
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,20a9,9,0,1,1,9-9A9,9,0,0,1,12,21Z"
|
||||
transform="translate(12, 12) scale(0)"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ PasswordSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
fill="currentColor"
|
||||
viewBox="0 0 1792 1792"
|
||||
>
|
||||
<path
|
||||
d="M1376 768q40 0 68 28t28 68v576q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-576q0-40 28-68t68-28h32v-320q0-185 131.5-316.5t316.5-131.5 316.5 131.5 131.5 316.5q0 26-19 45t-45 19h-64q-26 0-45-19t-19-45q0-106-75-181t-181-75-181 75-75 181v320h736z"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ SearchSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12ZM9 11.5C9 10.1193 10.1193 9 11.5 9C12.8807 9 14 10.1193 14 11.5C14 12.8807 12.8807 14 11.5 14C10.1193 14 9 12.8807 9 11.5ZM11.5 7C9.01472 7 7 9.01472 7 11.5C7 13.9853 9.01472 16 11.5 16C12.3805 16 13.202 15.7471 13.8957 15.31L15.2929 16.7071C15.6834 17.0976 16.3166 17.0976 16.7071 16.7071C17.0976 16.3166 17.0976 15.6834 16.7071 15.2929L15.31 13.8957C15.7471 13.202 16 12.3805 16 11.5C16 9.01472 13.9853 7 11.5 7Z"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ Search2SVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<rect width="24" height="24" fill="none"></rect>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M10 2C5.58172 2 2 5.58172 2 10C2 14.4183 5.58172 18 10 18C11.8487 18 13.551 17.3729 14.9056 16.3199L20.2929 21.7071C20.6834 22.0976 21.3166 22.0976 21.7071 21.7071C22.0976 21.3166 22.0976 20.6834 21.7071 20.2929L16.3199 14.9056C17.3729 13.551 18 11.8487 18 10C18 5.58172 14.4183 2 10 2Z"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ SettingsSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M14.2788 2.15224C13.9085 2 13.439 2 12.5 2C11.561 2 11.0915 2 10.7212 2.15224C10.2274 2.35523 9.83509 2.74458 9.63056 3.23463C9.53719 3.45834 9.50065 3.7185 9.48635 4.09799C9.46534 4.65568 9.17716 5.17189 8.69017 5.45093C8.20318 5.72996 7.60864 5.71954 7.11149 5.45876C6.77318 5.2813 6.52789 5.18262 6.28599 5.15102C5.75609 5.08178 5.22018 5.22429 4.79616 5.5472C4.47814 5.78938 4.24339 6.1929 3.7739 6.99993C3.30441 7.80697 3.06967 8.21048 3.01735 8.60491C2.94758 9.1308 3.09118 9.66266 3.41655 10.0835C3.56506 10.2756 3.77377 10.437 4.0977 10.639C4.57391 10.936 4.88032 11.4419 4.88029 12C4.88026 12.5581 4.57386 13.0639 4.0977 13.3608C3.77372 13.5629 3.56497 13.7244 3.41645 13.9165C3.09108 14.3373 2.94749 14.8691 3.01725 15.395C3.06957 15.7894 3.30432 16.193 3.7738 17C4.24329 17.807 4.47804 18.2106 4.79606 18.4527C5.22008 18.7756 5.75599 18.9181 6.28589 18.8489C6.52778 18.8173 6.77305 18.7186 7.11133 18.5412C7.60852 18.2804 8.2031 18.27 8.69012 18.549C9.17714 18.8281 9.46533 19.3443 9.48635 19.9021C9.50065 20.2815 9.53719 20.5417 9.63056 20.7654C9.83509 21.2554 10.2274 21.6448 10.7212 21.8478C11.0915 22 11.561 22 12.5 22C13.439 22 13.9085 22 14.2788 21.8478C14.7726 21.6448 15.1649 21.2554 15.3694 20.7654C15.4628 20.5417 15.4994 20.2815 15.5137 19.902C15.5347 19.3443 15.8228 18.8281 16.3098 18.549C16.7968 18.2699 17.3914 18.2804 17.8886 18.5412C18.2269 18.7186 18.4721 18.8172 18.714 18.8488C19.2439 18.9181 19.7798 18.7756 20.2038 18.4527C20.5219 18.2105 20.7566 17.807 21.2261 16.9999C21.6956 16.1929 21.9303 15.7894 21.9827 15.395C22.0524 14.8691 21.9088 14.3372 21.5835 13.9164C21.4349 13.7243 21.2262 13.5628 20.9022 13.3608C20.4261 13.0639 20.1197 12.558 20.1197 11.9999C20.1197 11.4418 20.4261 10.9361 20.9022 10.6392C21.2263 10.4371 21.435 10.2757 21.5836 10.0835C21.9089 9.66273 22.0525 9.13087 21.9828 8.60497C21.9304 8.21055 21.6957 7.80703 21.2262 7C20.7567 6.19297 20.522 5.78945 20.2039 5.54727C19.7799 5.22436 19.244 5.08185 18.7141 5.15109C18.4722 5.18269 18.2269 5.28136 17.8887 5.4588C17.3915 5.71959 16.7969 5.73002 16.3099 5.45096C15.8229 5.17191 15.5347 4.65566 15.5136 4.09794C15.4993 3.71848 15.4628 3.45833 15.3694 3.23463C15.1649 2.74458 14.7726 2.35523 14.2788 2.15224ZM12.5 15C14.1695 15 15.5228 13.6569 15.5228 12C15.5228 10.3431 14.1695 9 12.5 9C10.8305 9 9.47716 10.3431 9.47716 12C9.47716 13.6569 10.8305 15 12.5 15Z"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ UploadSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12 15.75C12.4142 15.75 12.75 15.4142 12.75 15V4.02744L14.4306 5.98809C14.7001 6.30259 15.1736 6.33901 15.4881 6.06944C15.8026 5.79988 15.839 5.3264 15.5694 5.01191L12.5694 1.51191C12.427 1.34567 12.2189 1.25 12 1.25C11.7811 1.25 11.573 1.34567 11.4306 1.51191L8.43056 5.01191C8.16099 5.3264 8.19741 5.79988 8.51191 6.06944C8.8264 6.33901 9.29988 6.30259 9.56944 5.98809L11.25 4.02744L11.25 15C11.25 15.4142 11.5858 15.75 12 15.75Z"
|
||||
></path>
|
||||
<path
|
||||
d="M16 9C15.2978 9 14.9467 9 14.6945 9.16851C14.5853 9.24148 14.4915 9.33525 14.4186 9.44446C14.25 9.69667 14.25 10.0478 14.25 10.75L14.25 15C14.25 16.2426 13.2427 17.25 12 17.25C10.7574 17.25 9.75004 16.2426 9.75004 15L9.75004 10.75C9.75004 10.0478 9.75004 9.69664 9.58149 9.4444C9.50854 9.33523 9.41481 9.2415 9.30564 9.16855C9.05341 9 8.70227 9 8 9C5.17157 9 3.75736 9 2.87868 9.87868C2 10.7574 2 12.1714 2 14.9998V15.9998C2 18.8282 2 20.2424 2.87868 21.1211C3.75736 21.9998 5.17157 21.9998 8 21.9998H16C18.8284 21.9998 20.2426 21.9998 21.1213 21.1211C22 20.2424 22 18.8282 22 15.9998V14.9998C22 12.1714 22 10.7574 21.1213 9.87868C20.2426 9 18.8284 9 16 9Z"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ UserSVG(className string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ className }
|
||||
fill="currentColor"
|
||||
viewBox="0 0 1792 1792"
|
||||
>
|
||||
<path
|
||||
d="M1523 1339q-22-155-87.5-257.5t-184.5-118.5q-67 74-159.5 115.5t-195.5 41.5-195.5-41.5-159.5-115.5q-119 16-184.5 118.5t-87.5 257.5q106 150 271 237.5t356 87.5 356-87.5 271-237.5zm-243-699q0-159-112.5-271.5t-271.5-112.5-271.5 112.5-112.5 271.5 112.5 271.5 271.5 112.5 271.5-112.5 112.5-271.5zm512 256q0 182-71 347.5t-190.5 286-285.5 191.5-349 71q-182 0-348-71t-286-191-191-286-71-348 71-348 191-286 286-191 348-71 348 71 286 191 191 286 71 348z"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
904
ngtemplates/components/svgs_templ.go
Normal file
904
ngtemplates/components/svgs_templ.go
Normal file
@@ -0,0 +1,904 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.819
|
||||
package components
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
func ActivitySVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var2 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var2).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path d=\"M9.5 2C8.67157 2 8 2.67157 8 3.5V4.5C8 5.32843 8.67157 6 9.5 6H14.5C15.3284 6 16 5.32843 16 4.5V3.5C16 2.67157 15.3284 2 14.5 2H9.5Z\"></path> <path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M6.5 4.03662C5.24209 4.10719 4.44798 4.30764 3.87868 4.87694C3 5.75562 3 7.16983 3 9.99826V15.9983C3 18.8267 3 20.2409 3.87868 21.1196C4.75736 21.9983 6.17157 21.9983 9 21.9983H15C17.8284 21.9983 19.2426 21.9983 20.1213 21.1196C21 20.2409 21 18.8267 21 15.9983V9.99826C21 7.16983 21 5.75562 20.1213 4.87694C19.552 4.30764 18.7579 4.10719 17.5 4.03662V4.5C17.5 6.15685 16.1569 7.5 14.5 7.5H9.5C7.84315 7.5 6.5 6.15685 6.5 4.5V4.03662ZM7 9.75C6.58579 9.75 6.25 10.0858 6.25 10.5C6.25 10.9142 6.58579 11.25 7 11.25H7.5C7.91421 11.25 8.25 10.9142 8.25 10.5C8.25 10.0858 7.91421 9.75 7.5 9.75H7ZM10.5 9.75C10.0858 9.75 9.75 10.0858 9.75 10.5C9.75 10.9142 10.0858 11.25 10.5 11.25H17C17.4142 11.25 17.75 10.9142 17.75 10.5C17.75 10.0858 17.4142 9.75 17 9.75H10.5ZM7 13.25C6.58579 13.25 6.25 13.5858 6.25 14C6.25 14.4142 6.58579 14.75 7 14.75H7.5C7.91421 14.75 8.25 14.4142 8.25 14C8.25 13.5858 7.91421 13.25 7.5 13.25H7ZM10.5 13.25C10.0858 13.25 9.75 13.5858 9.75 14C9.75 14.4142 10.0858 14.75 10.5 14.75H17C17.4142 14.75 17.75 14.4142 17.75 14C17.75 13.5858 17.4142 13.25 17 13.25H10.5ZM7 16.75C6.58579 16.75 6.25 17.0858 6.25 17.5C6.25 17.9142 6.58579 18.25 7 18.25H7.5C7.91421 18.25 8.25 17.9142 8.25 17.5C8.25 17.0858 7.91421 16.75 7.5 16.75H7ZM10.5 16.75C10.0858 16.75 9.75 17.0858 9.75 17.5C9.75 17.9142 10.0858 18.25 10.5 18.25H17C17.4142 18.25 17.75 17.9142 17.75 17.5C17.75 17.0858 17.4142 16.75 17 16.75H10.5Z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func AddSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var4 == nil {
|
||||
templ_7745c5c3_Var4 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var5 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var5...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var5).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22ZM12 8.25C12.4142 8.25 12.75 8.58579 12.75 9V11.25H15C15.4142 11.25 15.75 11.5858 15.75 12C15.75 12.4142 15.4142 12.75 15 12.75H12.75L12.75 15C12.75 15.4142 12.4142 15.75 12 15.75C11.5858 15.75 11.25 15.4142 11.25 15V12.75H9C8.58579 12.75 8.25 12.4142 8.25 12C8.25 11.5858 8.58579 11.25 9 11.25H11.25L11.25 9C11.25 8.58579 11.5858 8.25 12 8.25Z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func ClockSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var7 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var7 == nil {
|
||||
templ_7745c5c3_Var7 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var8 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var8).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path d=\"M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z\"></path> <path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M12 7.25C12.4142 7.25 12.75 7.58579 12.75 8V11.6893L15.0303 13.9697C15.3232 14.2626 15.3232 14.7374 15.0303 15.0303C14.7374 15.3232 14.2626 15.3232 13.9697 15.0303L11.4697 12.5303C11.329 12.3897 11.25 12.1989 11.25 12V8C11.25 7.58579 11.5858 7.25 12 7.25Z\" fill=\"white\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func DeleteSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var10 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var10 == nil {
|
||||
templ_7745c5c3_Var10 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var11 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var11...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var11).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M3 6.52381C3 6.12932 3.32671 5.80952 3.72973 5.80952H8.51787C8.52437 4.9683 8.61554 3.81504 9.45037 3.01668C10.1074 2.38839 11.0081 2 12 2C12.9919 2 13.8926 2.38839 14.5496 3.01668C15.3844 3.81504 15.4756 4.9683 15.4821 5.80952H20.2703C20.6733 5.80952 21 6.12932 21 6.52381C21 6.9183 20.6733 7.2381 20.2703 7.2381H3.72973C3.32671 7.2381 3 6.9183 3 6.52381Z\"></path> <path d=\"M11.6066 22H12.3935C15.101 22 16.4547 22 17.3349 21.1368C18.2151 20.2736 18.3052 18.8576 18.4853 16.0257L18.7448 11.9452C18.8425 10.4086 18.8913 9.64037 18.4498 9.15352C18.0082 8.66667 17.2625 8.66667 15.7712 8.66667H8.22884C6.7375 8.66667 5.99183 8.66667 5.55026 9.15352C5.1087 9.64037 5.15756 10.4086 5.25528 11.9452L5.51479 16.0257C5.69489 18.8576 5.78494 20.2736 6.66513 21.1368C7.54532 22 8.89906 22 11.6066 22Z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func DocumentSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var13 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var13 == nil {
|
||||
templ_7745c5c3_Var13 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var14 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var14...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var14).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M6.27103 2.11151C5.46135 2.21816 5.03258 2.41324 4.72718 2.71244C4.42179 3.01165 4.22268 3.43172 4.11382 4.225C4.00176 5.04159 4 6.12387 4 7.67568V16.2442C4.38867 15.9781 4.82674 15.7756 5.29899 15.6517C5.82716 15.513 6.44305 15.5132 7.34563 15.5135L20 15.5135V7.67568C20 6.12387 19.9982 5.04159 19.8862 4.22499C19.7773 3.43172 19.5782 3.01165 19.2728 2.71244C18.9674 2.41324 18.5387 2.21816 17.729 2.11151C16.8955 2.00172 15.7908 2 14.2069 2H9.7931C8.2092 2 7.10452 2.00172 6.27103 2.11151ZM6.75862 6.59459C6.75862 6.1468 7.12914 5.78378 7.58621 5.78378H16.4138C16.8709 5.78378 17.2414 6.1468 17.2414 6.59459C17.2414 7.04239 16.8709 7.40541 16.4138 7.40541H7.58621C7.12914 7.40541 6.75862 7.04239 6.75862 6.59459ZM7.58621 9.56757C7.12914 9.56757 6.75862 9.93058 6.75862 10.3784C6.75862 10.8262 7.12914 11.1892 7.58621 11.1892H13.1034C13.5605 11.1892 13.931 10.8262 13.931 10.3784C13.931 9.93058 13.5605 9.56757 13.1034 9.56757H7.58621Z\"></path> <path d=\"M7.47341 17.1351H8.68965H13.1034H19.9991C19.9956 18.2657 19.9776 19.1088 19.8862 19.775C19.7773 20.5683 19.5782 20.9884 19.2728 21.2876C18.9674 21.5868 18.5387 21.7818 17.729 21.8885C16.8955 21.9983 15.7908 22 14.2069 22H9.7931C8.2092 22 7.10452 21.9983 6.27103 21.8885C5.46135 21.7818 5.03258 21.5868 4.72718 21.2876C4.42179 20.9884 4.22268 20.5683 4.11382 19.775C4.07259 19.4746 4.0463 19.1382 4.02952 18.7558C4.30088 18.0044 4.93365 17.4264 5.72738 17.218C6.01657 17.1421 6.39395 17.1351 7.47341 17.1351Z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func DownloadSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var16 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var16 == nil {
|
||||
templ_7745c5c3_Var16 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var17 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var17...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var17).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12ZM12 6.25C12.4142 6.25 12.75 6.58579 12.75 7V12.1893L14.4697 10.4697C14.7626 10.1768 15.2374 10.1768 15.5303 10.4697C15.8232 10.7626 15.8232 11.2374 15.5303 11.5303L12.5303 14.5303C12.3897 14.671 12.1989 14.75 12 14.75C11.8011 14.75 11.6103 14.671 11.4697 14.5303L8.46967 11.5303C8.17678 11.2374 8.17678 10.7626 8.46967 10.4697C8.76256 10.1768 9.23744 10.1768 9.53033 10.4697L11.25 12.1893V7C11.25 6.58579 11.5858 6.25 12 6.25ZM8 16.25C7.58579 16.25 7.25 16.5858 7.25 17C7.25 17.4142 7.58579 17.75 8 17.75H16C16.4142 17.75 16.75 17.4142 16.75 17C16.75 16.5858 16.4142 16.25 16 16.25H8Z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func DropdownSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var19 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var19 == nil {
|
||||
templ_7745c5c3_Var19 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var20 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var20...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var20).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" viewBox=\"0 0 1792 1792\" fill=\"currentColor\"><path d=\"M1408 704q0 26-19 45l-448 448q-19 19-45 19t-45-19l-448-448q-19-19-19-45t19-45 45-19h896q26 0 45 19t19 45z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func EditSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var22 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var22 == nil {
|
||||
templ_7745c5c3_Var22 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var23 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var23...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var24 string
|
||||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var23).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path d=\"M21.1938 2.80624C22.2687 3.88124 22.2687 5.62415 21.1938 6.69914L20.6982 7.19469C20.5539 7.16345 20.3722 7.11589 20.1651 7.04404C19.6108 6.85172 18.8823 6.48827 18.197 5.803C17.5117 5.11774 17.1483 4.38923 16.956 3.8349C16.8841 3.62781 16.8366 3.44609 16.8053 3.30179L17.3009 2.80624C18.3759 1.73125 20.1188 1.73125 21.1938 2.80624Z\"></path> <path d=\"M14.5801 13.3128C14.1761 13.7168 13.9741 13.9188 13.7513 14.0926C13.4886 14.2975 13.2043 14.4732 12.9035 14.6166C12.6485 14.7381 12.3775 14.8284 11.8354 15.0091L8.97709 15.9619C8.71035 16.0508 8.41626 15.9814 8.21744 15.7826C8.01862 15.5837 7.9492 15.2897 8.03811 15.0229L8.99089 12.1646C9.17157 11.6225 9.26191 11.3515 9.38344 11.0965C9.52679 10.7957 9.70249 10.5114 9.90743 10.2487C10.0812 10.0259 10.2832 9.82394 10.6872 9.41993L15.6033 4.50385C15.867 5.19804 16.3293 6.05663 17.1363 6.86366C17.9434 7.67069 18.802 8.13296 19.4962 8.39674L14.5801 13.3128Z\"></path> <path d=\"M20.5355 20.5355C22 19.0711 22 16.714 22 12C22 10.4517 22 9.15774 21.9481 8.0661L15.586 14.4283C15.2347 14.7797 14.9708 15.0437 14.6738 15.2753C14.3252 15.5473 13.948 15.7804 13.5488 15.9706C13.2088 16.1327 12.8546 16.2506 12.3833 16.4076L9.45143 17.3849C8.64568 17.6535 7.75734 17.4438 7.15678 16.8432C6.55621 16.2427 6.34651 15.3543 6.61509 14.5486L7.59235 11.6167C7.74936 11.1454 7.86732 10.7912 8.02935 10.4512C8.21958 10.052 8.45272 9.6748 8.72466 9.32615C8.9563 9.02918 9.22032 8.76528 9.57173 8.41404L15.9339 2.05188C14.8423 2 13.5483 2 12 2C7.28595 2 4.92893 2 3.46447 3.46447C2 4.92893 2 7.28595 2 12C2 16.714 2 19.0711 3.46447 20.5355C4.92893 22 7.28595 22 12 22C16.714 22 19.0711 22 20.5355 20.5355Z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func GitSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var25 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var25 == nil {
|
||||
templ_7745c5c3_Var25 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var26 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var26...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var27 string
|
||||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var26).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" fill=\"currentColor\" viewBox=\"0 0 219 92\"><defs><clipPath id=\"a\"><path d=\"M159 .79h25V69h-25Zm0 0\"></path></clipPath> <clipPath id=\"b\"><path d=\"M183 9h35.371v60H183Zm0 0\"></path></clipPath> <clipPath id=\"c\"><path d=\"M0 .79h92V92H0Zm0 0\"></path></clipPath></defs> <path style=\"stroke: none; fill-rule: nonzero; fill-opacity: 1\" d=\"M130.871 31.836c-4.785 0-8.351 2.352-8.351 8.008 0 4.261 2.347 7.222 8.093 7.222 4.871 0 8.18-2.867 8.18-7.398 0-5.133-2.961-7.832-7.922-7.832Zm-9.57 39.95c-1.133 1.39-2.262 2.87-2.262 4.612 0 3.48 4.434 4.524 10.527 4.524 5.051 0 11.926-.352 11.926-5.043 0-2.793-3.308-2.965-7.488-3.227Zm25.761-39.688c1.563 2.004 3.22 4.789 3.22 8.793 0 9.656-7.571 15.316-18.536 15.316-2.789 0-5.312-.348-6.879-.785l-2.87 4.613 8.526.52c15.059.96 23.934 1.398 23.934 12.968 0 10.008-8.789 15.665-23.934 15.665-15.75 0-21.757-4.004-21.757-10.88 0-3.917 1.742-6 4.789-8.878-2.875-1.211-3.828-3.387-3.828-5.739 0-1.914.953-3.656 2.523-5.312 1.566-1.652 3.305-3.305 5.395-5.219-4.262-2.09-7.485-6.617-7.485-13.058 0-10.008 6.613-16.88 19.93-16.88 3.742 0 6.004.344 8.008.872h16.972v7.394l-8.007.61\"></path> <g clip-path=\"url(#a)\"><path style=\"stroke: none; fill-rule: nonzero; fill-opacity: 1\" d=\"M170.379 16.281c-4.961 0-7.832-2.87-7.832-7.836 0-4.957 2.871-7.656 7.832-7.656 5.05 0 7.922 2.7 7.922 7.656 0 4.965-2.871 7.836-7.922 7.836Zm-11.227 52.305V61.71l4.438-.606c1.219-.175 1.394-.437 1.394-1.746V33.773c0-.953-.261-1.566-1.132-1.824l-4.7-1.656.957-7.047h18.016V59.36c0 1.399.086 1.57 1.395 1.746l4.437.606v6.875h-24.805\"></path></g> <g clip-path=\"url(#b)\"><path style=\"stroke: none; fill-rule: nonzero; fill-opacity: 1\" d=\"M218.371 65.21c-3.742 1.825-9.223 3.481-14.187 3.481-10.356 0-14.27-4.175-14.27-14.015V31.879c0-.524 0-.871-.7-.871h-6.093v-7.746c7.664-.871 10.707-4.703 11.664-14.188h8.27v12.36c0 .609 0 .87.695.87h12.27v8.704h-12.965v20.797c0 5.136 1.218 7.136 5.918 7.136 2.437 0 4.96-.609 7.047-1.39l2.351 7.66\"></path></g> <g clip-path=\"url(#c)\"><path style=\"stroke: none; fill-rule: nonzero; fill-opacity: 1\" d=\"M89.422 42.371 49.629 2.582a5.868 5.868 0 0 0-8.3 0l-8.263 8.262 10.48 10.484a6.965 6.965 0 0 1 7.173 1.668 6.98 6.98 0 0 1 1.656 7.215l10.102 10.105a6.963 6.963 0 0 1 7.214 1.657 6.976 6.976 0 0 1 0 9.875 6.98 6.98 0 0 1-9.879 0 6.987 6.987 0 0 1-1.519-7.594l-9.422-9.422v24.793a6.979 6.979 0 0 1 1.848 1.32 6.988 6.988 0 0 1 0 9.88c-2.73 2.726-7.153 2.726-9.875 0a6.98 6.98 0 0 1 0-9.88 6.893 6.893 0 0 1 2.285-1.523V34.398a6.893 6.893 0 0 1-2.285-1.523 6.988 6.988 0 0 1-1.508-7.637L29.004 14.902 1.719 42.187a5.868 5.868 0 0 0 0 8.301l39.793 39.793a5.868 5.868 0 0 0 8.3 0l39.61-39.605a5.873 5.873 0 0 0 0-8.305\"></path></g></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func HomeSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var28 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var28 == nil {
|
||||
templ_7745c5c3_Var28 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var29 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var29...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var30 string
|
||||
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var29).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M2.5192 7.82274C2 8.77128 2 9.91549 2 12.2039V13.725C2 17.6258 2 19.5763 3.17157 20.7881C4.34315 22 6.22876 22 10 22H14C17.7712 22 19.6569 22 20.8284 20.7881C22 19.5763 22 17.6258 22 13.725V12.2039C22 9.91549 22 8.77128 21.4808 7.82274C20.9616 6.87421 20.0131 6.28551 18.116 5.10812L16.116 3.86687C14.1106 2.62229 13.1079 2 12 2C10.8921 2 9.88939 2.62229 7.88403 3.86687L5.88403 5.10813C3.98695 6.28551 3.0384 6.87421 2.5192 7.82274ZM11.25 18C11.25 18.4142 11.5858 18.75 12 18.75C12.4142 18.75 12.75 18.4142 12.75 18V15C12.75 14.5858 12.4142 14.25 12 14.25C11.5858 14.25 11.25 14.5858 11.25 15V18Z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func ImportSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var31 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var31 == nil {
|
||||
templ_7745c5c3_Var31 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var32 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var32...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var33 string
|
||||
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var32).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M2.06935 5.00839C2 5.37595 2 5.81722 2 6.69975V13.75C2 17.5212 2 19.4069 3.17157 20.5784C4.34315 21.75 6.22876 21.75 10 21.75H14C17.7712 21.75 19.6569 21.75 20.8284 20.5784C22 19.4069 22 17.5212 22 13.75V11.5479C22 8.91554 22 7.59935 21.2305 6.74383C21.1598 6.66514 21.0849 6.59024 21.0062 6.51946C20.1506 5.75 18.8345 5.75 16.2021 5.75H15.8284C14.6747 5.75 14.0979 5.75 13.5604 5.59678C13.2651 5.5126 12.9804 5.39471 12.7121 5.24543C12.2237 4.97367 11.8158 4.56578 11 3.75L10.4497 3.19975C10.1763 2.92633 10.0396 2.78961 9.89594 2.67051C9.27652 2.15704 8.51665 1.84229 7.71557 1.76738C7.52976 1.75 7.33642 1.75 6.94975 1.75C6.06722 1.75 5.62595 1.75 5.25839 1.81935C3.64031 2.12464 2.37464 3.39031 2.06935 5.00839ZM12 11C12.4142 11 12.75 11.3358 12.75 11.75V13H14C14.4142 13 14.75 13.3358 14.75 13.75C14.75 14.1642 14.4142 14.5 14 14.5H12.75V15.75C12.75 16.1642 12.4142 16.5 12 16.5C11.5858 16.5 11.25 16.1642 11.25 15.75V14.5H10C9.58579 14.5 9.25 14.1642 9.25 13.75C9.25 13.3358 9.58579 13 10 13H11.25V11.75C11.25 11.3358 11.5858 11 12 11Z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func InfoSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var34 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var34 == nil {
|
||||
templ_7745c5c3_Var34 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var35 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var35...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var36 string
|
||||
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var35).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22ZM12 17.75C12.4142 17.75 12.75 17.4142 12.75 17V11C12.75 10.5858 12.4142 10.25 12 10.25C11.5858 10.25 11.25 10.5858 11.25 11V17C11.25 17.4142 11.5858 17.75 12 17.75ZM12 7C12.5523 7 13 7.44772 13 8C13 8.55228 12.5523 9 12 9C11.4477 9 11 8.55228 11 8C11 7.44772 11.4477 7 12 7Z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func LoadingSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var37 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var37 == nil {
|
||||
templ_7745c5c3_Var37 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var38 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var38...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var39 string
|
||||
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var38).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><style>\n\t\t .spinner_l9ve {\n\t\t animation: spinner_rcyq 1.2s cubic-bezier(0.52, 0.6, 0.25, 0.99) infinite;\n\t\t }\n\t\t .spinner_cMYp {\n\t\t animation-delay: 0.4s;\n\t\t }\n\t\t .spinner_gHR3 {\n\t\t animation-delay: 0.8s;\n\t\t }\n\t\t @keyframes spinner_rcyq {\n\t\t 0% {\n\t\t transform: translate(12px, 12px) scale(0);\n\t\t opacity: 1;\n\t\t }\n\t\t 100% {\n\t\t transform: translate(0, 0) scale(1);\n\t\t opacity: 0;\n\t\t }\n\t\t }\n\t\t</style><path class=\"spinner_l9ve\" d=\"M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,20a9,9,0,1,1,9-9A9,9,0,0,1,12,21Z\" transform=\"translate(12, 12) scale(0)\"></path> <path class=\"spinner_l9ve spinner_cMYp\" d=\"M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,20a9,9,0,1,1,9-9A9,9,0,0,1,12,21Z\" transform=\"translate(12, 12) scale(0)\"></path> <path class=\"spinner_l9ve spinner_gHR3\" d=\"M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,20a9,9,0,1,1,9-9A9,9,0,0,1,12,21Z\" transform=\"translate(12, 12) scale(0)\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func PasswordSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var40 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var40 == nil {
|
||||
templ_7745c5c3_Var40 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var41 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var41...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var42 string
|
||||
templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var41).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" fill=\"currentColor\" viewBox=\"0 0 1792 1792\"><path d=\"M1376 768q40 0 68 28t28 68v576q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-576q0-40 28-68t68-28h32v-320q0-185 131.5-316.5t316.5-131.5 316.5 131.5 131.5 316.5q0 26-19 45t-45 19h-64q-26 0-45-19t-19-45q0-106-75-181t-181-75-181 75-75 181v320h736z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func SearchSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var43 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var43 == nil {
|
||||
templ_7745c5c3_Var43 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var44 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var44...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var45 string
|
||||
templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var44).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12ZM9 11.5C9 10.1193 10.1193 9 11.5 9C12.8807 9 14 10.1193 14 11.5C14 12.8807 12.8807 14 11.5 14C10.1193 14 9 12.8807 9 11.5ZM11.5 7C9.01472 7 7 9.01472 7 11.5C7 13.9853 9.01472 16 11.5 16C12.3805 16 13.202 15.7471 13.8957 15.31L15.2929 16.7071C15.6834 17.0976 16.3166 17.0976 16.7071 16.7071C17.0976 16.3166 17.0976 15.6834 16.7071 15.2929L15.31 13.8957C15.7471 13.202 16 12.3805 16 11.5C16 9.01472 13.9853 7 11.5 7Z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func Search2SVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var46 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var46 == nil {
|
||||
templ_7745c5c3_Var46 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var47 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var47...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var48 string
|
||||
templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var47).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><rect width=\"24\" height=\"24\" fill=\"none\"></rect> <path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M10 2C5.58172 2 2 5.58172 2 10C2 14.4183 5.58172 18 10 18C11.8487 18 13.551 17.3729 14.9056 16.3199L20.2929 21.7071C20.6834 22.0976 21.3166 22.0976 21.7071 21.7071C22.0976 21.3166 22.0976 20.6834 21.7071 20.2929L16.3199 14.9056C17.3729 13.551 18 11.8487 18 10C18 5.58172 14.4183 2 10 2Z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func SettingsSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var49 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var49 == nil {
|
||||
templ_7745c5c3_Var49 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var50 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var50...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var51 string
|
||||
templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var50).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M14.2788 2.15224C13.9085 2 13.439 2 12.5 2C11.561 2 11.0915 2 10.7212 2.15224C10.2274 2.35523 9.83509 2.74458 9.63056 3.23463C9.53719 3.45834 9.50065 3.7185 9.48635 4.09799C9.46534 4.65568 9.17716 5.17189 8.69017 5.45093C8.20318 5.72996 7.60864 5.71954 7.11149 5.45876C6.77318 5.2813 6.52789 5.18262 6.28599 5.15102C5.75609 5.08178 5.22018 5.22429 4.79616 5.5472C4.47814 5.78938 4.24339 6.1929 3.7739 6.99993C3.30441 7.80697 3.06967 8.21048 3.01735 8.60491C2.94758 9.1308 3.09118 9.66266 3.41655 10.0835C3.56506 10.2756 3.77377 10.437 4.0977 10.639C4.57391 10.936 4.88032 11.4419 4.88029 12C4.88026 12.5581 4.57386 13.0639 4.0977 13.3608C3.77372 13.5629 3.56497 13.7244 3.41645 13.9165C3.09108 14.3373 2.94749 14.8691 3.01725 15.395C3.06957 15.7894 3.30432 16.193 3.7738 17C4.24329 17.807 4.47804 18.2106 4.79606 18.4527C5.22008 18.7756 5.75599 18.9181 6.28589 18.8489C6.52778 18.8173 6.77305 18.7186 7.11133 18.5412C7.60852 18.2804 8.2031 18.27 8.69012 18.549C9.17714 18.8281 9.46533 19.3443 9.48635 19.9021C9.50065 20.2815 9.53719 20.5417 9.63056 20.7654C9.83509 21.2554 10.2274 21.6448 10.7212 21.8478C11.0915 22 11.561 22 12.5 22C13.439 22 13.9085 22 14.2788 21.8478C14.7726 21.6448 15.1649 21.2554 15.3694 20.7654C15.4628 20.5417 15.4994 20.2815 15.5137 19.902C15.5347 19.3443 15.8228 18.8281 16.3098 18.549C16.7968 18.2699 17.3914 18.2804 17.8886 18.5412C18.2269 18.7186 18.4721 18.8172 18.714 18.8488C19.2439 18.9181 19.7798 18.7756 20.2038 18.4527C20.5219 18.2105 20.7566 17.807 21.2261 16.9999C21.6956 16.1929 21.9303 15.7894 21.9827 15.395C22.0524 14.8691 21.9088 14.3372 21.5835 13.9164C21.4349 13.7243 21.2262 13.5628 20.9022 13.3608C20.4261 13.0639 20.1197 12.558 20.1197 11.9999C20.1197 11.4418 20.4261 10.9361 20.9022 10.6392C21.2263 10.4371 21.435 10.2757 21.5836 10.0835C21.9089 9.66273 22.0525 9.13087 21.9828 8.60497C21.9304 8.21055 21.6957 7.80703 21.2262 7C20.7567 6.19297 20.522 5.78945 20.2039 5.54727C19.7799 5.22436 19.244 5.08185 18.7141 5.15109C18.4722 5.18269 18.2269 5.28136 17.8887 5.4588C17.3915 5.71959 16.7969 5.73002 16.3099 5.45096C15.8229 5.17191 15.5347 4.65566 15.5136 4.09794C15.4993 3.71848 15.4628 3.45833 15.3694 3.23463C15.1649 2.74458 14.7726 2.35523 14.2788 2.15224ZM12.5 15C14.1695 15 15.5228 13.6569 15.5228 12C15.5228 10.3431 14.1695 9 12.5 9C10.8305 9 9.47716 10.3431 9.47716 12C9.47716 13.6569 10.8305 15 12.5 15Z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func UploadSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var52 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var52 == nil {
|
||||
templ_7745c5c3_Var52 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var53 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var53...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var54 string
|
||||
templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var53).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M12 15.75C12.4142 15.75 12.75 15.4142 12.75 15V4.02744L14.4306 5.98809C14.7001 6.30259 15.1736 6.33901 15.4881 6.06944C15.8026 5.79988 15.839 5.3264 15.5694 5.01191L12.5694 1.51191C12.427 1.34567 12.2189 1.25 12 1.25C11.7811 1.25 11.573 1.34567 11.4306 1.51191L8.43056 5.01191C8.16099 5.3264 8.19741 5.79988 8.51191 6.06944C8.8264 6.33901 9.29988 6.30259 9.56944 5.98809L11.25 4.02744L11.25 15C11.25 15.4142 11.5858 15.75 12 15.75Z\"></path> <path d=\"M16 9C15.2978 9 14.9467 9 14.6945 9.16851C14.5853 9.24148 14.4915 9.33525 14.4186 9.44446C14.25 9.69667 14.25 10.0478 14.25 10.75L14.25 15C14.25 16.2426 13.2427 17.25 12 17.25C10.7574 17.25 9.75004 16.2426 9.75004 15L9.75004 10.75C9.75004 10.0478 9.75004 9.69664 9.58149 9.4444C9.50854 9.33523 9.41481 9.2415 9.30564 9.16855C9.05341 9 8.70227 9 8 9C5.17157 9 3.75736 9 2.87868 9.87868C2 10.7574 2 12.1714 2 14.9998V15.9998C2 18.8282 2 20.2424 2.87868 21.1211C3.75736 21.9998 5.17157 21.9998 8 21.9998H16C18.8284 21.9998 20.2426 21.9998 21.1213 21.1211C22 20.2424 22 18.8282 22 15.9998V14.9998C22 12.1714 22 10.7574 21.1213 9.87868C20.2426 9 18.8284 9 16 9Z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func UserSVG(className string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var55 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var55 == nil {
|
||||
templ_7745c5c3_Var55 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var56 = []any{className}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var56...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var57 string
|
||||
templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var56).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/svgs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\" fill=\"currentColor\" viewBox=\"0 0 1792 1792\"><path d=\"M1523 1339q-22-155-87.5-257.5t-184.5-118.5q-67 74-159.5 115.5t-195.5 41.5-195.5-41.5-159.5-115.5q-119 16-184.5 118.5t-87.5 257.5q106 150 271 237.5t356 87.5 356-87.5 271-237.5zm-243-699q0-159-112.5-271.5t-271.5-112.5-271.5 112.5-112.5 271.5 112.5 271.5 271.5 112.5 271.5-112.5 112.5-271.5zm512 256q0 182-71 347.5t-190.5 286-285.5 191.5-349 71q-182 0-348-71t-286-191-191-286-71-348 71-348 191-286 286-191 348-71 348 71 286 191 191 286 71 348z\"></path></svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
49
ngtemplates/components/table.templ
Normal file
49
ngtemplates/components/table.templ
Normal file
@@ -0,0 +1,49 @@
|
||||
package components
|
||||
|
||||
type TableCellFormatter[T any] func(data T) templ.Component
|
||||
|
||||
type TableColumn[T any] struct {
|
||||
Name string // Column Name
|
||||
Getter func(T) string // Data Getter
|
||||
Formatter TableCellFormatter[T] // Data Formatter
|
||||
}
|
||||
|
||||
templ (c *TableColumn[T]) getCell(d T) {
|
||||
if c.Formatter != nil {
|
||||
@c.Formatter(d)
|
||||
} else if c.Getter != nil {
|
||||
{ c.Getter(d) }
|
||||
} else {
|
||||
"Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
templ Table[T any](columns []TableColumn[T], rows []T) {
|
||||
<table class="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm">
|
||||
<thead class="text-gray-800 dark:text-gray-400">
|
||||
<tr>
|
||||
for _, column := range columns {
|
||||
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">
|
||||
{ column.Name }
|
||||
</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-black dark:text-white">
|
||||
if len(rows) == 0 {
|
||||
<tr>
|
||||
<td class="text-center p-3" colspan="4">No Results</td>
|
||||
</tr>
|
||||
}
|
||||
for _, row := range rows {
|
||||
<tr>
|
||||
for _, column := range columns {
|
||||
<td class="p-3 border-b border-gray-200">
|
||||
@column.getCell(row)
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
151
ngtemplates/components/table_templ.go
Normal file
151
ngtemplates/components/table_templ.go
Normal file
@@ -0,0 +1,151 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.819
|
||||
package components
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
type TableCellFormatter[T any] func(data T) templ.Component
|
||||
|
||||
type TableColumn[T any] struct {
|
||||
Name string // Column Name
|
||||
Getter func(T) string // Data Getter
|
||||
Formatter TableCellFormatter[T] // Data Formatter
|
||||
}
|
||||
|
||||
func (c *TableColumn[T]) getCell(d T) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
if c.Formatter != nil {
|
||||
templ_7745c5c3_Err = c.Formatter(d).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else if c.Getter != nil {
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(c.Getter(d))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/table.templ`, Line: 15, Col: 15}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "\"Unknown\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func Table[T any](columns []TableColumn[T], rows []T) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var3 == nil {
|
||||
templ_7745c5c3_Var3 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<table class=\"min-w-full leading-normal bg-white dark:bg-gray-700 text-sm\"><thead class=\"text-gray-800 dark:text-gray-400\"><tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, column := range columns {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<th class=\"p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(column.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ngtemplates/components/table.templ`, Line: 27, Col: 19}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</th>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</tr></thead> <tbody class=\"text-black dark:text-white\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<tr><td class=\"text-center p-3\" colspan=\"4\">No Results</td></tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
for _, row := range rows {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, column := range columns {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<td class=\"p-3 border-b border-gray-200\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = column.getCell(row).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</td>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</tbody></table>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
51
ngtemplates/pages/activity.templ
Normal file
51
ngtemplates/pages/activity.templ
Normal file
@@ -0,0 +1,51 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reichard.io/antholume/database"
|
||||
"reichard.io/antholume/ngtemplates/common"
|
||||
"reichard.io/antholume/ngtemplates/components"
|
||||
)
|
||||
|
||||
var columns = []components.TableColumn[database.GetActivityRow]{
|
||||
{
|
||||
Name: "Document",
|
||||
Formatter: documentFormatter,
|
||||
},
|
||||
{
|
||||
Name: "Time",
|
||||
Getter: func(r database.GetActivityRow) string {
|
||||
return fmt.Sprint(r.StartTime)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Duration",
|
||||
Getter: func(r database.GetActivityRow) string {
|
||||
return fmt.Sprint(r.Duration)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Percent",
|
||||
Formatter: percentageFormatter,
|
||||
},
|
||||
}
|
||||
|
||||
templ documentFormatter(row database.GetActivityRow) {
|
||||
<a href={ templ.SafeURL(fmt.Sprintf("./documents/%s", row.DocumentID)) }>
|
||||
{ fmt.Sprintf("%s - %s", *row.Author, *row.Title) }
|
||||
</a>
|
||||
}
|
||||
|
||||
templ percentageFormatter(row database.GetActivityRow) {
|
||||
{ fmt.Sprintf("%.2f%%", row.EndPercentage) }
|
||||
}
|
||||
|
||||
templ Activity(settings common.Settings, rows []database.GetActivityRow) {
|
||||
@layout(settings, "Activity") {
|
||||
<div class="overflow-x-auto">
|
||||
<div class="inline-block min-w-full overflow-hidden rounded shadow">
|
||||
@components.Table(columns, rows)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user