Compare commits
17 Commits
425f469097
...
0.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| c3410b7833 | |||
| 1403bae036 | |||
| af41946a65 | |||
| 243ae2a001 | |||
| d94e79f39c | |||
| 856bc7e2e6 | |||
| 5cc1e2d71c | |||
| ffc5462326 | |||
| 3cbe4b1c0d | |||
| c213b3b09f | |||
| 7d45bb0253 | |||
| a8bcd0f588 | |||
| bc3e9cbaf0 | |||
| e6ad51ed70 | |||
| cce0ef2de1 | |||
| 71898c39e7 | |||
| 985b6e0851 |
@@ -24,7 +24,7 @@ steps:
|
|||||||
- name: publish_docker
|
- name: publish_docker
|
||||||
image: plugins/docker
|
image: plugins/docker
|
||||||
settings:
|
settings:
|
||||||
repo: gitea.va.reichard.io/evan/bookmanager
|
repo: gitea.va.reichard.io/evan/antholume
|
||||||
registry: gitea.va.reichard.io
|
registry: gitea.va.reichard.io
|
||||||
tags:
|
tags:
|
||||||
- dev
|
- dev
|
||||||
|
|||||||
14
Dockerfile
14
Dockerfile
@@ -10,17 +10,17 @@ WORKDIR /src
|
|||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Create Package Directory
|
# Create Package Directory
|
||||||
RUN mkdir -p /opt/bookmanager
|
RUN mkdir -p /opt/antholume
|
||||||
|
|
||||||
# Compile
|
# Compile
|
||||||
RUN go build -o /opt/bookmanager/server; \
|
RUN go build -o /opt/antholume/server; \
|
||||||
cp -a ./templates /opt/bookmanager/templates; \
|
cp -a ./templates /opt/antholume/templates; \
|
||||||
cp -a ./assets /opt/bookmanager/assets;
|
cp -a ./assets /opt/antholume/assets;
|
||||||
|
|
||||||
# Create Image
|
# Create Image
|
||||||
FROM busybox:1.36
|
FROM busybox:1.36
|
||||||
COPY --from=certs /etc/ssl/certs /etc/ssl/certs
|
COPY --from=certs /etc/ssl/certs /etc/ssl/certs
|
||||||
COPY --from=build /opt/bookmanager /opt/bookmanager
|
COPY --from=build /opt/antholume /opt/antholume
|
||||||
WORKDIR /opt/bookmanager
|
WORKDIR /opt/antholume
|
||||||
EXPOSE 8585
|
EXPOSE 8585
|
||||||
ENTRYPOINT ["/opt/bookmanager/server", "serve"]
|
ENTRYPOINT ["/opt/antholume/server", "serve"]
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ FROM --platform=$BUILDPLATFORM golang:1.20 AS build
|
|||||||
|
|
||||||
# Create Package Directory
|
# Create Package Directory
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
RUN mkdir -p /opt/bookmanager
|
RUN mkdir -p /opt/antholume
|
||||||
|
|
||||||
# Cache Dependencies & Compile
|
# Cache Dependencies & Compile
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
@@ -15,14 +15,14 @@ ARG TARGETARCH
|
|||||||
RUN --mount=target=. \
|
RUN --mount=target=. \
|
||||||
--mount=type=cache,target=/root/.cache/go-build \
|
--mount=type=cache,target=/root/.cache/go-build \
|
||||||
--mount=type=cache,target=/go/pkg \
|
--mount=type=cache,target=/go/pkg \
|
||||||
GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /opt/bookmanager/server; \
|
GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /opt/antholume/server; \
|
||||||
cp -a ./templates /opt/bookmanager/templates; \
|
cp -a ./templates /opt/antholume/templates; \
|
||||||
cp -a ./assets /opt/bookmanager/assets;
|
cp -a ./assets /opt/antholume/assets;
|
||||||
|
|
||||||
# Create Image
|
# Create Image
|
||||||
FROM busybox:1.36
|
FROM busybox:1.36
|
||||||
COPY --from=certs /etc/ssl/certs /etc/ssl/certs
|
COPY --from=certs /etc/ssl/certs /etc/ssl/certs
|
||||||
COPY --from=build /opt/bookmanager /opt/bookmanager
|
COPY --from=build /opt/antholume /opt/antholume
|
||||||
WORKDIR /opt/bookmanager
|
WORKDIR /opt/antholume
|
||||||
EXPOSE 8585
|
EXPOSE 8585
|
||||||
ENTRYPOINT ["/opt/bookmanager/server", "serve"]
|
ENTRYPOINT ["/opt/antholume/server", "serve"]
|
||||||
|
|||||||
10
Makefile
10
Makefile
@@ -11,25 +11,25 @@ build_local: build_tailwind
|
|||||||
env GOOS=darwin GOARCH=amd64 go build -o ./build/server_darwin_amd64
|
env GOOS=darwin GOARCH=amd64 go build -o ./build/server_darwin_amd64
|
||||||
|
|
||||||
docker_build_local: build_tailwind
|
docker_build_local: build_tailwind
|
||||||
docker build -t bookmanager:latest .
|
docker build -t antholume:latest .
|
||||||
|
|
||||||
docker_build_release_dev: build_tailwind
|
docker_build_release_dev: build_tailwind
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--platform linux/amd64,linux/arm64 \
|
--platform linux/amd64,linux/arm64 \
|
||||||
-t gitea.va.reichard.io/evan/bookmanager:dev \
|
-t gitea.va.reichard.io/evan/antholume:dev \
|
||||||
-f Dockerfile-BuildKit \
|
-f Dockerfile-BuildKit \
|
||||||
--push .
|
--push .
|
||||||
|
|
||||||
docker_build_release_latest: build_tailwind
|
docker_build_release_latest: build_tailwind
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--platform linux/amd64,linux/arm64 \
|
--platform linux/amd64,linux/arm64 \
|
||||||
-t gitea.va.reichard.io/evan/bookmanager:latest \
|
-t gitea.va.reichard.io/evan/antholume:latest \
|
||||||
-t gitea.va.reichard.io/evan/bookmanager:`git describe --tags` \
|
-t gitea.va.reichard.io/evan/antholume:`git describe --tags` \
|
||||||
-f Dockerfile-BuildKit \
|
-f Dockerfile-BuildKit \
|
||||||
--push .
|
--push .
|
||||||
|
|
||||||
build_tailwind:
|
build_tailwind:
|
||||||
tailwind build -o ./assets/style.css
|
tailwind build -o ./assets/style.css --minify
|
||||||
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
|
|||||||
91
README.md
91
README.md
@@ -1,101 +1,90 @@
|
|||||||
# Book Manager
|
<p><img align="center" src="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/banner.png"></p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/login.png">
|
<a href="https://gitea.va.reichard.io/evan/AnthoLume/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%">
|
<img src="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/pwa/login.png" width="19%">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/home.png">
|
<a href="https://gitea.va.reichard.io/evan/AnthoLume/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%">
|
<img src="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/pwa/home.png" width="19%">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/documents.png">
|
<a href="https://gitea.va.reichard.io/evan/AnthoLume/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%">
|
<img src="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/pwa/documents.png" width="19%">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/document.png">
|
<a href="https://gitea.va.reichard.io/evan/AnthoLume/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%">
|
<img src="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/pwa/document.png" width="19%">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/metadata.png">
|
<a href="https://gitea.va.reichard.io/evan/AnthoLume/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%">
|
<img src="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/pwa/metadata.png" width="19%">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">Screenshots</p>
|
|
||||||
<p align="center">
|
<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>
|
||||||
|
<p align="center"><strong>user:</strong> demo • <strong>pass:</strong> demo</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://drone.va.reichard.io/evan/BookManager" target="_blank">
|
<a href="https://drone.va.reichard.io/evan/AnthoLume" target="_blank">
|
||||||
<img src="https://drone.va.reichard.io/api/badges/evan/BookManager/status.svg">
|
<img src="https://drone.va.reichard.io/api/badges/evan/AnthoLume/status.svg">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</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`
|
- OPDS API Endpoint
|
||||||
- Password: `demo`
|
- 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)
|
## Server
|
||||||
- [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
|
|
||||||
|
|
||||||
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
|
### Local / Offline Reader
|
||||||
- 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
|
|
||||||
|
|
||||||
# Server
|
The Local / Offline reader allows you to use any AnthoLume server as a standalone offline accessible reading app! Some features:
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
- Add local EPUB documents
|
- Add local EPUB documents
|
||||||
- Read both local and any cached server documents
|
- Read both local and any cached server documents
|
||||||
- Maintains progress for all types of documents (server / local)
|
- Maintains progress for all types of documents (server / local)
|
||||||
- Uploads any progress or activity for cached server documents once the internet is accessible
|
- 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`
|
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`
|
The OPDS API endpoint is located at: `http(s)://<SERVER>/api/opds`
|
||||||
|
|
||||||
## Quick Start
|
### Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Make Data Directory
|
# Make Data Directory
|
||||||
mkdir -p bookmanager_data
|
mkdir -p antholume_data
|
||||||
|
|
||||||
# Run Server
|
# Run Server
|
||||||
docker run \
|
docker run \
|
||||||
-p 8585:8585 \
|
-p 8585:8585 \
|
||||||
-e REGISTRATION_ENABLED=true \
|
-e REGISTRATION_ENABLED=true \
|
||||||
-v ./bookmanager_data:/config \
|
-v ./antholume_data:/config \
|
||||||
-v ./bookmanager_data:/data \
|
-v ./antholume_data:/data \
|
||||||
gitea.va.reichard.io/evan/bookmanager:latest
|
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.
|
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 |
|
| Environment Variable | Default Value | Description |
|
||||||
| -------------------- | ------------- | ------------------------------------------------------------------- |
|
| -------------------- | ------------- | ------------------------------------------------------------------- |
|
||||||
| DATABASE_TYPE | SQLite | Currently only "SQLite" is supported |
|
| DATABASE_TYPE | SQLite | Currently only "SQLite" is supported |
|
||||||
| DATABASE_NAME | book_manager | The database name, or in SQLite's case, the filename |
|
| DATABASE_NAME | antholume | The database name, or in SQLite's case, the filename |
|
||||||
| CONFIG_PATH | /config | Directory where to store SQLite's DB |
|
| CONFIG_PATH | /config | Directory where to store SQLite's DB |
|
||||||
| DATA_PATH | /data | Directory where to store the documents and cover metadata |
|
| DATA_PATH | /data | Directory where to store the documents and cover metadata |
|
||||||
| LISTEN_PORT | 8585 | Port the server listens at |
|
| LISTEN_PORT | 8585 | Port the server listens at |
|
||||||
@@ -118,11 +107,11 @@ 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:
|
- 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
|
- 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.21.0):
|
||||||
|
|
||||||
@@ -137,7 +126,7 @@ Run Development:
|
|||||||
CONFIG_PATH=./data DATA_PATH=./data REGISTRATION_ENABLED=true go run main.go serve
|
CONFIG_PATH=./data DATA_PATH=./data REGISTRATION_ENABLED=true go run main.go serve
|
||||||
```
|
```
|
||||||
|
|
||||||
# Building
|
## Building
|
||||||
|
|
||||||
The `Dockerfile` and `Makefile` contain the build information:
|
The `Dockerfile` and `Makefile` contain the build information:
|
||||||
|
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ func (api *API) registerOPDSRoutes(apiGroup *gin.RouterGroup) {
|
|||||||
opdsGroup := apiGroup.Group("/opds")
|
opdsGroup := apiGroup.Group("/opds")
|
||||||
|
|
||||||
// OPDS Routes
|
// OPDS Routes
|
||||||
|
opdsGroup.GET("", api.authOPDSMiddleware, api.opdsDocuments)
|
||||||
opdsGroup.GET("/", api.authOPDSMiddleware, api.opdsDocuments)
|
opdsGroup.GET("/", api.authOPDSMiddleware, api.opdsDocuments)
|
||||||
opdsGroup.GET("/documents/:document/cover", api.authOPDSMiddleware, api.getDocumentCover)
|
opdsGroup.GET("/documents/:document/cover", api.authOPDSMiddleware, api.getDocumentCover)
|
||||||
opdsGroup.GET("/documents/:document/file", api.authOPDSMiddleware, api.downloadDocument)
|
opdsGroup.GET("/documents/:document/file", api.authOPDSMiddleware, api.downloadDocument)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -26,6 +27,7 @@ import (
|
|||||||
type queryParams struct {
|
type queryParams struct {
|
||||||
Page *int64 `form:"page"`
|
Page *int64 `form:"page"`
|
||||||
Limit *int64 `form:"limit"`
|
Limit *int64 `form:"limit"`
|
||||||
|
Search *string `form:"search"`
|
||||||
Document *string `form:"document"`
|
Document *string `form:"document"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,8 +113,15 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any
|
|||||||
qParams := bindQueryParams(c)
|
qParams := bindQueryParams(c)
|
||||||
|
|
||||||
if routeName == "documents" {
|
if routeName == "documents" {
|
||||||
|
var query *string
|
||||||
|
if qParams.Search != nil && *qParams.Search != "" {
|
||||||
|
search := "%" + *qParams.Search + "%"
|
||||||
|
query = &search
|
||||||
|
}
|
||||||
|
|
||||||
documents, err := api.DB.Queries.GetDocumentsWithStats(api.DB.Ctx, database.GetDocumentsWithStatsParams{
|
documents, err := api.DB.Queries.GetDocumentsWithStats(api.DB.Ctx, database.GetDocumentsWithStatsParams{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
|
Query: query,
|
||||||
Offset: (*qParams.Page - 1) * *qParams.Limit,
|
Offset: (*qParams.Page - 1) * *qParams.Limit,
|
||||||
Limit: *qParams.Limit,
|
Limit: *qParams.Limit,
|
||||||
})
|
})
|
||||||
@@ -122,10 +131,30 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
length, err := api.DB.Queries.GetDocumentsSize(api.DB.Ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("[createAppResourcesRoute] GetDocumentsSize DB Error:", err)
|
||||||
|
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocumentsSize DB Error: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err = api.getDocumentsWordCount(documents); err != nil {
|
if err = api.getDocumentsWordCount(documents); err != nil {
|
||||||
log.Error("[createAppResourcesRoute] Unable to Get Word Counts: ", err)
|
log.Error("[createAppResourcesRoute] Unable to Get Word Counts: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
totalPages := int64(math.Ceil(float64(length) / float64(*qParams.Limit)))
|
||||||
|
nextPage := *qParams.Page + 1
|
||||||
|
previousPage := *qParams.Page - 1
|
||||||
|
|
||||||
|
if nextPage <= totalPages {
|
||||||
|
templateVars["NextPage"] = nextPage
|
||||||
|
}
|
||||||
|
|
||||||
|
if previousPage >= 0 {
|
||||||
|
templateVars["PreviousPage"] = previousPage
|
||||||
|
}
|
||||||
|
|
||||||
|
templateVars["PageLimit"] = *qParams.Limit
|
||||||
templateVars["Data"] = documents
|
templateVars["Data"] = documents
|
||||||
} else if routeName == "document" {
|
} else if routeName == "document" {
|
||||||
var rDocID requestDocumentID
|
var rDocID requestDocumentID
|
||||||
@@ -975,7 +1004,7 @@ func bindQueryParams(c *gin.Context) queryParams {
|
|||||||
c.BindQuery(&qParams)
|
c.BindQuery(&qParams)
|
||||||
|
|
||||||
if qParams.Limit == nil {
|
if qParams.Limit == nil {
|
||||||
var defaultValue int64 = 50
|
var defaultValue int64 = 9
|
||||||
qParams.Limit = &defaultValue
|
qParams.Limit = &defaultValue
|
||||||
} else if *qParams.Limit < 0 {
|
} else if *qParams.Limit < 0 {
|
||||||
var zeroValue int64 = 0
|
var zeroValue int64 = 0
|
||||||
@@ -983,7 +1012,7 @@ func bindQueryParams(c *gin.Context) queryParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if qParams.Page == nil || *qParams.Page < 1 {
|
if qParams.Page == nil || *qParams.Page < 1 {
|
||||||
var oneValue int64 = 0
|
var oneValue int64 = 1
|
||||||
qParams.Page = &oneValue
|
qParams.Page = &oneValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -169,6 +169,13 @@ func (api *API) setProgress(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update Statistic
|
||||||
|
log.Info("[setProgress] UpdateDocumentUserStatistic Running...")
|
||||||
|
if err := api.DB.UpdateDocumentUserStatistic(rPosition.DocumentID, rUser.(string)); err != nil {
|
||||||
|
log.Error("[setProgress] UpdateDocumentUserStatistic Error:", err)
|
||||||
|
}
|
||||||
|
log.Info("[setProgress] UpdateDocumentUserStatistic Complete")
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"document": progress.DocumentID,
|
"document": progress.DocumentID,
|
||||||
"timestamp": progress.CreatedAt,
|
"timestamp": progress.CreatedAt,
|
||||||
@@ -285,6 +292,15 @@ func (api *API) addActivities(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update Statistic
|
||||||
|
for _, doc := range allDocuments {
|
||||||
|
log.Info("[addActivities] UpdateDocumentUserStatistic Running...")
|
||||||
|
if err := api.DB.UpdateDocumentUserStatistic(doc, rUser.(string)); err != nil {
|
||||||
|
log.Error("[addActivities] UpdateDocumentUserStatistic Error:", err)
|
||||||
|
}
|
||||||
|
log.Info("[addActivities] UpdateDocumentUserStatistic Complete")
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"added": len(rActivity.Activity),
|
"added": len(rActivity.Activity),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -55,15 +55,30 @@ func (api *API) opdsDocuments(c *gin.Context) {
|
|||||||
splitFilepath := strings.Split(*doc.Filepath, ".")
|
splitFilepath := strings.Split(*doc.Filepath, ".")
|
||||||
fileType := splitFilepath[len(splitFilepath)-1]
|
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{
|
item := opds.Entry{
|
||||||
Title: fmt.Sprintf("[%3d%%] %s", int(doc.Percentage), *doc.Title),
|
Title: fmt.Sprintf("[%3d%%] %s", int(doc.Percentage), title),
|
||||||
Author: []opds.Author{
|
Author: []opds.Author{
|
||||||
{
|
{
|
||||||
Name: *doc.Author,
|
Name: author,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Content: &opds.Content{
|
Content: &opds.Content{
|
||||||
Content: *doc.Description,
|
Content: description,
|
||||||
ContentType: "text",
|
ContentType: "text",
|
||||||
},
|
},
|
||||||
Links: []opds.Link{
|
Links: []opds.Link{
|
||||||
@@ -91,7 +106,7 @@ func (api *API) opdsDocuments(c *gin.Context) {
|
|||||||
// TODO
|
// TODO
|
||||||
// Links: []opds.Link{
|
// Links: []opds.Link{
|
||||||
// {
|
// {
|
||||||
// Title: "Search Book Manager",
|
// Title: "Search AnthoLume",
|
||||||
// Rel: "search",
|
// Rel: "search",
|
||||||
// TypeLink: "application/opensearchdescription+xml",
|
// TypeLink: "application/opensearchdescription+xml",
|
||||||
// Href: "search.xml",
|
// Href: "search.xml",
|
||||||
@@ -105,8 +120,8 @@ func (api *API) opdsDocuments(c *gin.Context) {
|
|||||||
|
|
||||||
func (api *API) opdsSearchDescription(c *gin.Context) {
|
func (api *API) opdsSearchDescription(c *gin.Context) {
|
||||||
rawXML := `<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
|
rawXML := `<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
|
||||||
<ShortName>Search Book Manager</ShortName>
|
<ShortName>Search AnthoLume</ShortName>
|
||||||
<Description>Search Book Manager</Description>
|
<Description>Search AnthoLume</Description>
|
||||||
<Url type="application/atom+xml;profile=opds-catalog;kind=acquisition" template="./search?query={searchTerms}"/>
|
<Url type="application/atom+xml;profile=opds-catalog;kind=acquisition" template="./search?query={searchTerms}"/>
|
||||||
</OpenSearchDescription>`
|
</OpenSearchDescription>`
|
||||||
c.Data(http.StatusOK, "application/xml", []byte(rawXML))
|
c.Data(http.StatusOK, "application/xml", []byte(rawXML))
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* Custom Service Worker Convenience Functions Wrapper
|
||||||
|
**/
|
||||||
const SW = (function () {
|
const SW = (function () {
|
||||||
// Helper Function
|
// Helper Function
|
||||||
function randomID() {
|
function randomID() {
|
||||||
@@ -63,3 +66,57 @@ const SW = (function () {
|
|||||||
|
|
||||||
return { install, send };
|
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/icon512.png
Normal file
BIN
assets/icons/icon512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
@@ -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})}));
|
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
1
assets/lib/platform.min.js
vendored
Normal file
1
assets/lib/platform.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -22,15 +22,19 @@
|
|||||||
media="(prefers-color-scheme: dark)"
|
media="(prefers-color-scheme: dark)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<title>Book Manager - Local</title>
|
<title>AnthoLume - Local</title>
|
||||||
|
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<link rel="stylesheet" href="/assets/style.css" />
|
<link rel="stylesheet" href="/assets/style.css" />
|
||||||
|
|
||||||
|
<!-- Libraries -->
|
||||||
<script src="/assets/lib/jszip.min.js"></script>
|
<script src="/assets/lib/jszip.min.js"></script>
|
||||||
<script src="/assets/lib/epub.min.js"></script>
|
<script src="/assets/lib/epub.min.js"></script>
|
||||||
<script src="/assets/lib/idb-keyval.js"></script>
|
<script src="/assets/lib/idb-keyval.min.js"></script>
|
||||||
<script src="/assets/lib/sw-helper.js"></script>
|
<script src="/assets/lib/sw-helper.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Local -->
|
||||||
|
<script src="/assets/common.js"></script>
|
||||||
<script src="/assets/index.js"></script>
|
<script src="/assets/index.js"></script>
|
||||||
<script src="/assets/local/index.js"></script>
|
<script src="/assets/local/index.js"></script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
{
|
{
|
||||||
"short_name": "Book Manager",
|
"name": "AnthoLume",
|
||||||
"name": "Book Manager",
|
"short_name": "AnthoLume",
|
||||||
|
"lang": "en-US",
|
||||||
"theme_color": "#1F2937",
|
"theme_color": "#1F2937",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"scope": "/",
|
"scope": "/",
|
||||||
"start_url": "/"
|
"start_url": "/",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"purpose": "any",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"src": "/assets/icons/icon512.png",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,24 +14,27 @@
|
|||||||
/>
|
/>
|
||||||
<meta name="theme-color" content="#D2B48C" />
|
<meta name="theme-color" content="#D2B48C" />
|
||||||
|
|
||||||
<title>Book Manager - Reader</title>
|
<title>AnthoLume - Reader</title>
|
||||||
|
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<link rel="stylesheet" href="/assets/style.css" />
|
<link rel="stylesheet" href="/assets/style.css" />
|
||||||
|
|
||||||
<!-- Libraries -->
|
<!-- Libraries -->
|
||||||
<script src="/assets/lib/platform.js"></script>
|
<script src="/assets/lib/platform.min.js"></script>
|
||||||
<script src="/assets/lib/jszip.min.js"></script>
|
<script src="/assets/lib/jszip.min.js"></script>
|
||||||
<script src="/assets/lib/epub.min.js"></script>
|
<script src="/assets/lib/epub.min.js"></script>
|
||||||
<script src="/assets/lib/no-sleep.js"></script>
|
<script src="/assets/lib/no-sleep.min.js"></script>
|
||||||
<script src="/assets/lib/idb-keyval.js"></script>
|
<script src="/assets/lib/idb-keyval.min.js"></script>
|
||||||
<script src="/assets/lib/sw-helper.js"></script>
|
|
||||||
|
|
||||||
<!-- Reader -->
|
<!-- Reader -->
|
||||||
|
<script src="/assets/common.js"></script>
|
||||||
<script src="/assets/index.js"></script>
|
<script src="/assets/index.js"></script>
|
||||||
<script src="/assets/reader/index.js"></script>
|
<script src="/assets/reader/index.js"></script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
/* ----------------------------- */
|
||||||
|
/* -------- PWA Styling -------- */
|
||||||
|
/* ----------------------------- */
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
overscroll-behavior-y: none;
|
overscroll-behavior-y: none;
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ function populateMetadata(data) {
|
|||||||
**/
|
**/
|
||||||
class EBookReader {
|
class EBookReader {
|
||||||
bookState = {
|
bookState = {
|
||||||
currentWord: 0,
|
|
||||||
pages: 0,
|
pages: 0,
|
||||||
percentage: 0,
|
percentage: 0,
|
||||||
progress: "",
|
progress: "",
|
||||||
@@ -115,27 +114,11 @@ class EBookReader {
|
|||||||
* Load progress and generate locations
|
* Load progress and generate locations
|
||||||
**/
|
**/
|
||||||
async setupReader() {
|
async setupReader() {
|
||||||
// Get Word Count (If Needed)
|
// Get Word Count
|
||||||
if (this.bookState.words == 0)
|
this.bookState.words = await this.countWords();
|
||||||
this.bookState.words = await this.countWords();
|
|
||||||
|
|
||||||
// Load Progress
|
// Load Progress
|
||||||
let { cfi } = await this.getCFIFromXPath(this.bookState.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
|
// Update Position
|
||||||
await this.setPosition(cfi);
|
await this.setPosition(cfi);
|
||||||
@@ -143,8 +126,14 @@ class EBookReader {
|
|||||||
// Highlight Element - DOM Has Element
|
// Highlight Element - DOM Has Element
|
||||||
let { element } = await this.getCFIFromXPath(this.bookState.progress);
|
let { element } = await this.getCFIFromXPath(this.bookState.progress);
|
||||||
|
|
||||||
|
// Set Progress Element & Highlight
|
||||||
this.bookState.progressElement = element;
|
this.bookState.progressElement = element;
|
||||||
this.highlightPositionMarker();
|
this.highlightPositionMarker();
|
||||||
|
|
||||||
|
// Update Stats & Page Start
|
||||||
|
let stats = await this.getBookStats();
|
||||||
|
this.updateBookStatElements(stats);
|
||||||
|
this.bookState.pageStart = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
initDevice() {
|
initDevice() {
|
||||||
@@ -660,7 +649,7 @@ class EBookReader {
|
|||||||
this.bookState.pageStart = Date.now();
|
this.bookState.pageStart = Date.now();
|
||||||
|
|
||||||
// Update Stats
|
// Update Stats
|
||||||
let stats = this.getBookStats();
|
let stats = await this.getBookStats();
|
||||||
this.updateBookStatElements(stats);
|
this.updateBookStatElements(stats);
|
||||||
|
|
||||||
// Create Progress
|
// Create Progress
|
||||||
@@ -674,16 +663,11 @@ class EBookReader {
|
|||||||
// Render Previous Page
|
// Render Previous Page
|
||||||
await this.rendition.prev();
|
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
|
// Reset Read Timer
|
||||||
this.bookState.pageStart = Date.now();
|
this.bookState.pageStart = Date.now();
|
||||||
|
|
||||||
// Update Stats
|
// Update Stats
|
||||||
let stats = this.getBookStats();
|
let stats = await this.getBookStats();
|
||||||
this.updateBookStatElements(stats);
|
this.updateBookStatElements(stats);
|
||||||
|
|
||||||
// Create Progress
|
// Create Progress
|
||||||
@@ -719,9 +703,8 @@ class EBookReader {
|
|||||||
|
|
||||||
// Update Current Word
|
// Update Current Word
|
||||||
let pageWords = await this.getVisibleWordCount();
|
let pageWords = await this.getVisibleWordCount();
|
||||||
let startingWord = this.bookState.currentWord;
|
let currentWord = await this.getBookWordPosition();
|
||||||
let percentRead = pageWords / this.bookState.words;
|
let percentRead = pageWords / this.bookState.words;
|
||||||
this.bookState.currentWord += pageWords;
|
|
||||||
|
|
||||||
let pageWPM = pageWords / (elapsedTime / 60000);
|
let pageWPM = pageWords / (elapsedTime / 60000);
|
||||||
console.log("[createActivity] Page WPM:", pageWPM);
|
console.log("[createActivity] Page WPM:", pageWPM);
|
||||||
@@ -740,10 +723,10 @@ class EBookReader {
|
|||||||
|
|
||||||
// Exclude 0 Pages
|
// Exclude 0 Pages
|
||||||
if (totalPages == 0)
|
if (totalPages == 0)
|
||||||
return console.log("[createActivity] Invalid Total Pages (0)");
|
return console.warn("[createActivity] Invalid Total Pages (0)");
|
||||||
|
|
||||||
let currentPage = Math.round(
|
let currentPage = Math.round(
|
||||||
(startingWord * totalPages) / this.bookState.words
|
(currentWord * totalPages) / this.bookState.words
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create Activity Event
|
// Create Activity Event
|
||||||
@@ -805,6 +788,8 @@ class EBookReader {
|
|||||||
// Update Pointers
|
// Update Pointers
|
||||||
let currentCFI = await this.rendition.currentLocation();
|
let currentCFI = await this.rendition.currentLocation();
|
||||||
let { element, xpath } = await this.getXPathFromCFI(currentCFI.start.cfi);
|
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.progress = xpath;
|
||||||
this.bookState.progressElement = element;
|
this.bookState.progressElement = element;
|
||||||
|
|
||||||
@@ -814,9 +799,7 @@ class EBookReader {
|
|||||||
device_id: this.readerSettings.deviceID,
|
device_id: this.readerSettings.deviceID,
|
||||||
device: this.readerSettings.deviceName,
|
device: this.readerSettings.deviceName,
|
||||||
percentage:
|
percentage:
|
||||||
Math.round(
|
Math.round((currentWord / this.bookState.words) * 100000) / 100000,
|
||||||
(this.bookState.currentWord / this.bookState.words) * 100000
|
|
||||||
) / 100000,
|
|
||||||
progress: this.bookState.progress,
|
progress: this.bookState.progress,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -885,12 +868,13 @@ class EBookReader {
|
|||||||
/**
|
/**
|
||||||
* Get chapter pages, name and progress percentage
|
* Get chapter pages, name and progress percentage
|
||||||
**/
|
**/
|
||||||
getBookStats() {
|
async getBookStats() {
|
||||||
let currentProgress = this.sectionProgress();
|
let currentProgress = this.sectionProgress();
|
||||||
if (!currentProgress) return;
|
if (!currentProgress) return;
|
||||||
let { sectionPages, sectionCurrentPage } = currentProgress;
|
let { sectionPages, sectionCurrentPage } = currentProgress;
|
||||||
|
|
||||||
let currentLocation = this.rendition.currentLocation();
|
let currentLocation = this.rendition.currentLocation();
|
||||||
|
let currentWord = await this.getBookWordPosition();
|
||||||
|
|
||||||
let currentTOC = this.book.navigation.toc.find(
|
let currentTOC = this.book.navigation.toc.find(
|
||||||
(item) => item.href == currentLocation.start.href
|
(item) => item.href == currentLocation.start.href
|
||||||
@@ -901,9 +885,7 @@ class EBookReader {
|
|||||||
sectionTotalPages: sectionPages,
|
sectionTotalPages: sectionPages,
|
||||||
chapterName: currentTOC ? currentTOC.label.trim() : "N/A",
|
chapterName: currentTOC ? currentTOC.label.trim() : "N/A",
|
||||||
percentage:
|
percentage:
|
||||||
Math.round(
|
Math.round((currentWord / this.bookState.words) * 10000) / 100,
|
||||||
(this.bookState.currentWord / this.bookState.words) * 10000
|
|
||||||
) / 100,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -928,7 +910,7 @@ class EBookReader {
|
|||||||
* Get XPath from current location
|
* Get XPath from current location
|
||||||
**/
|
**/
|
||||||
async getXPathFromCFI(cfi) {
|
async getXPathFromCFI(cfi) {
|
||||||
// Get DocFragment (current book spline index)
|
// Get DocFragment (Spine Index)
|
||||||
let startCFI = cfi.replace("epubcfi(", "");
|
let startCFI = cfi.replace("epubcfi(", "");
|
||||||
let docFragmentIndex =
|
let docFragmentIndex =
|
||||||
this.book.spine.spineItems.find((item) =>
|
this.book.spine.spineItems.find((item) =>
|
||||||
@@ -936,58 +918,62 @@ class EBookReader {
|
|||||||
).index + 1;
|
).index + 1;
|
||||||
|
|
||||||
// Base Progress
|
// 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 contents = this.rendition.getContents()[0];
|
||||||
let node = contents.range(cfi).startContainer;
|
let currentNode = contents.range(cfi).startContainer;
|
||||||
let element = null;
|
let element =
|
||||||
|
currentNode.nodeType == Node.ELEMENT_NODE
|
||||||
|
? currentNode
|
||||||
|
: currentNode.parentElement;
|
||||||
|
|
||||||
// Walk upwards and build progress until body
|
// XPath Reference
|
||||||
let childPos = "";
|
let allPos = "";
|
||||||
while (node.nodeName != "BODY") {
|
|
||||||
let ownValue;
|
|
||||||
|
|
||||||
switch (node.nodeType) {
|
// Walk Upwards
|
||||||
case Node.ELEMENT_NODE:
|
while (currentNode.nodeName != "BODY") {
|
||||||
// Store First Element Node
|
// Get Parent
|
||||||
if (!element) element = node;
|
let parentElement = currentNode.parentElement;
|
||||||
let relativeIndex =
|
|
||||||
Array.from(node.parentNode.children)
|
|
||||||
.filter((item) => item.nodeName == node.nodeName)
|
|
||||||
.indexOf(node) + 1;
|
|
||||||
|
|
||||||
ownValue = node.nodeName.toLowerCase() + "[" + relativeIndex + "]";
|
// Unknown Node -> Update Reference
|
||||||
break;
|
if (currentNode.nodeType != Node.ELEMENT_NODE) {
|
||||||
case Node.ATTRIBUTE_NODE:
|
console.log("[getXPathFromCFI] Unknown Node Type:", currentNode);
|
||||||
ownValue = "@" + node.nodeName;
|
currentNode = parentElement;
|
||||||
break;
|
continue;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepend childPos & Update node reference
|
/**
|
||||||
childPos = "/" + ownValue + childPos;
|
* Exclude A tags. This could potentially be all inline elements:
|
||||||
node = node.parentNode;
|
* 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 };
|
return { xpath, element };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -995,19 +981,13 @@ class EBookReader {
|
|||||||
* Get CFI from current location
|
* Get CFI from current location
|
||||||
**/
|
**/
|
||||||
async getCFIFromXPath(xpath) {
|
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
|
// No XPath
|
||||||
if (!xpath || xpath == "") return {};
|
if (!xpath || xpath == "") return {};
|
||||||
|
|
||||||
// Match Document Fragment Index
|
// Match Document Fragment Index
|
||||||
let fragMatch = xpath.match(/^\/body\/DocFragment\[(\d+)\]/);
|
let fragMatch = xpath.match(/^\/body\/DocFragment\[(\d+)\]/);
|
||||||
if (!fragMatch) {
|
if (!fragMatch) {
|
||||||
console.warn("No XPath Match");
|
console.warn("[getCFIFromXPath] No XPath Match");
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1039,7 +1019,7 @@ class EBookReader {
|
|||||||
.find((item) => item.sectionIndex == spinePosition)?.document ||
|
.find((item) => item.sectionIndex == spinePosition)?.document ||
|
||||||
sectionItem.document;
|
sectionItem.document;
|
||||||
|
|
||||||
// Derive XPath & Namespace
|
// Derive Namespace & XPath
|
||||||
let namespaceURI = docItem.documentElement.namespaceURI;
|
let namespaceURI = docItem.documentElement.namespaceURI;
|
||||||
let remainingXPath = xpath
|
let remainingXPath = xpath
|
||||||
// Replace with new base
|
// Replace with new base
|
||||||
@@ -1049,6 +1029,26 @@ class EBookReader {
|
|||||||
// Remove potential trailing `text()`
|
// Remove potential trailing `text()`
|
||||||
.replace(/\/text\(\)(\[\d+\])?$/, "");
|
.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
|
// Validate Namespace
|
||||||
if (namespaceURI) remainingXPath = remainingXPath.replaceAll("/", "/ns:");
|
if (namespaceURI) remainingXPath = remainingXPath.replaceAll("/", "/ns:");
|
||||||
|
|
||||||
@@ -1065,8 +1065,25 @@ class EBookReader {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// 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);
|
let cfi = sectionItem.cfiFromElement(element);
|
||||||
|
|
||||||
return { cfi, element };
|
return { cfi, element };
|
||||||
@@ -1076,6 +1093,43 @@ class EBookReader {
|
|||||||
* Get visible word count - used for reading stats
|
* Get visible word count - used for reading stats
|
||||||
**/
|
**/
|
||||||
async getVisibleWordCount() {
|
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)
|
// Force Expand & Resize (Race Condition Issue)
|
||||||
this.rendition.manager.visible().forEach((item) => item.expand());
|
this.rendition.manager.visible().forEach((item) => item.expand());
|
||||||
|
|
||||||
@@ -1092,7 +1146,7 @@ class EBookReader {
|
|||||||
let visibleText = textRange.toString();
|
let visibleText = textRange.toString();
|
||||||
|
|
||||||
// Split on Whitespace
|
// Split on Whitespace
|
||||||
return visibleText.trim().split(/\s+/).length;
|
return visibleText;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1147,14 +1201,17 @@ class EBookReader {
|
|||||||
* of progress percentage. Implementation returns the same number as the
|
* of progress percentage. Implementation returns the same number as the
|
||||||
* server side implementation.
|
* server side implementation.
|
||||||
**/
|
**/
|
||||||
countWords() {
|
async countWords() {
|
||||||
// Iterate over each item in the spine, render, and count words.
|
let spineWC = await Promise.all(
|
||||||
return this.book.spine.spineItems.reduce(async (totalCount, item) => {
|
this.book.spine.spineItems.map(async (item) => {
|
||||||
let currentCount = await totalCount;
|
let newDoc = await item.load(this.book.load.bind(this.book));
|
||||||
let newDoc = await item.load(this.book.load.bind(this.book));
|
let spineWords = newDoc.innerText.trim().split(/\s+/).length;
|
||||||
let itemCount = newDoc.innerText.trim().split(/\s+/).length;
|
item.wordCount = spineWords;
|
||||||
return currentCount + itemCount;
|
return spineWords;
|
||||||
}, 0);
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return spineWC.reduce((totalCount, itemCount) => totalCount + itemCount, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
2140
assets/style.css
2140
assets/style.css
File diff suppressed because one or more lines are too long
@@ -63,6 +63,7 @@ const PRECACHE_ASSETS = [
|
|||||||
"/reader",
|
"/reader",
|
||||||
"/assets/local/index.js",
|
"/assets/local/index.js",
|
||||||
"/assets/reader/index.js",
|
"/assets/reader/index.js",
|
||||||
|
"/assets/icons/icon512.png",
|
||||||
"/assets/images/no-cover.jpg",
|
"/assets/images/no-cover.jpg",
|
||||||
"/assets/reader/readerThemes.css",
|
"/assets/reader/readerThemes.css",
|
||||||
|
|
||||||
@@ -70,14 +71,14 @@ const PRECACHE_ASSETS = [
|
|||||||
"/manifest.json",
|
"/manifest.json",
|
||||||
"/assets/index.js",
|
"/assets/index.js",
|
||||||
"/assets/style.css",
|
"/assets/style.css",
|
||||||
|
"/assets/common.js",
|
||||||
|
|
||||||
// Library Assets
|
// Library Assets
|
||||||
"/assets/lib/platform.js",
|
"/assets/lib/platform.min.js",
|
||||||
"/assets/lib/jszip.min.js",
|
"/assets/lib/jszip.min.js",
|
||||||
"/assets/lib/epub.min.js",
|
"/assets/lib/epub.min.js",
|
||||||
"/assets/lib/no-sleep.js",
|
"/assets/lib/no-sleep.min.js",
|
||||||
"/assets/lib/idb-keyval.js",
|
"/assets/lib/idb-keyval.min.js",
|
||||||
"/assets/lib/sw-helper.js",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// ------------------------------------------------------- //
|
// ------------------------------------------------------- //
|
||||||
|
|||||||
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
|
- Syncing read activity
|
||||||
- Uploading documents
|
- Uploading documents
|
||||||
@@ -12,10 +12,10 @@ Copy the `syncninja.koplugin` directory to the `plugins` directory for your KORe
|
|||||||
|
|
||||||
## Configuration
|
## 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
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -853,8 +853,8 @@ function SyncNinja:getLocalDocumentMetadata()
|
|||||||
docsettings:saveSetting("partial_md5_checksum", pmd5)
|
docsettings:saveSetting("partial_md5_checksum", pmd5)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Get Document Props
|
-- Get Document Props & Ensure Not Nil
|
||||||
local doc_props = docsettings:readSetting("doc_props")
|
local doc_props = docsettings:readSetting("doc_props") or {}
|
||||||
local fdoc = bookinfo_books[v.file] or {}
|
local fdoc = bookinfo_books[v.file] or {}
|
||||||
|
|
||||||
-- Update or Create
|
-- Update or Create
|
||||||
|
|||||||
@@ -31,9 +31,9 @@ type Config struct {
|
|||||||
|
|
||||||
func Load() *Config {
|
func Load() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
Version: "0.0.2",
|
Version: "0.0.1",
|
||||||
DBType: trimLowerString(getEnv("DATABASE_TYPE", "SQLite")),
|
DBType: trimLowerString(getEnv("DATABASE_TYPE", "SQLite")),
|
||||||
DBName: trimLowerString(getEnv("DATABASE_NAME", "book_manager")),
|
DBName: trimLowerString(getEnv("DATABASE_NAME", "antholume")),
|
||||||
ConfigPath: getEnv("CONFIG_PATH", "/config"),
|
ConfigPath: getEnv("CONFIG_PATH", "/config"),
|
||||||
DataPath: getEnv("DATA_PATH", "/data"),
|
DataPath: getEnv("DATA_PATH", "/data"),
|
||||||
ListenPort: getEnv("LISTEN_PORT", "8585"),
|
ListenPort: getEnv("LISTEN_PORT", "8585"),
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ var ddl string
|
|||||||
//go:embed update_temp_tables.sql
|
//go:embed update_temp_tables.sql
|
||||||
var tsql string
|
var tsql string
|
||||||
|
|
||||||
|
//go:embed update_document_user_statistics.sql
|
||||||
|
var doc_user_stat_sql string
|
||||||
|
|
||||||
func NewMgr(c *config.Config) *DBManager {
|
func NewMgr(c *config.Config) *DBManager {
|
||||||
// Create Manager
|
// Create Manager
|
||||||
dbm := &DBManager{
|
dbm := &DBManager{
|
||||||
@@ -60,6 +63,21 @@ func (dbm *DBManager) Shutdown() error {
|
|||||||
return dbm.DB.Close()
|
return dbm.DB.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (dbm *DBManager) UpdateDocumentUserStatistic(documentID string, userID string) error {
|
||||||
|
// Prepare Statement
|
||||||
|
stmt, err := dbm.DB.PrepareContext(dbm.Ctx, doc_user_stat_sql)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
if _, err := stmt.ExecContext(dbm.Ctx, documentID, userID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (dbm *DBManager) CacheTempTables() error {
|
func (dbm *DBManager) CacheTempTables() error {
|
||||||
if _, err := dbm.DB.ExecContext(dbm.Ctx, tsql); err != nil {
|
if _, err := dbm.DB.ExecContext(dbm.Ctx, tsql); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -182,6 +182,16 @@ ORDER BY created_at DESC
|
|||||||
LIMIT $limit
|
LIMIT $limit
|
||||||
OFFSET $offset;
|
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
|
-- name: GetDocumentsWithStats :many
|
||||||
SELECT
|
SELECT
|
||||||
docs.id,
|
docs.id,
|
||||||
@@ -217,7 +227,13 @@ LEFT JOIN users ON users.id = $user_id
|
|||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
document_user_statistics AS dus
|
document_user_statistics AS dus
|
||||||
ON dus.document_id = docs.id AND dus.user_id = $user_id
|
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
|
ORDER BY dus.last_read DESC, docs.created_at DESC
|
||||||
LIMIT $limit
|
LIMIT $limit
|
||||||
OFFSET $offset;
|
OFFSET $offset;
|
||||||
|
|||||||
@@ -596,6 +596,24 @@ func (q *Queries) GetDocuments(ctx context.Context, arg GetDocumentsParams) ([]D
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getDocumentsSize = `-- name: GetDocumentsSize :one
|
||||||
|
SELECT
|
||||||
|
COUNT(rowid) AS length
|
||||||
|
FROM documents AS docs
|
||||||
|
WHERE ?1 IS NULL OR (
|
||||||
|
docs.title LIKE ?1 OR
|
||||||
|
docs.author LIKE ?1
|
||||||
|
)
|
||||||
|
LIMIT 1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetDocumentsSize(ctx context.Context, query interface{}) (int64, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getDocumentsSize, query)
|
||||||
|
var length int64
|
||||||
|
err := row.Scan(&length)
|
||||||
|
return length, err
|
||||||
|
}
|
||||||
|
|
||||||
const getDocumentsWithStats = `-- name: GetDocumentsWithStats :many
|
const getDocumentsWithStats = `-- name: GetDocumentsWithStats :many
|
||||||
SELECT
|
SELECT
|
||||||
docs.id,
|
docs.id,
|
||||||
@@ -631,16 +649,23 @@ LEFT JOIN users ON users.id = ?1
|
|||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
document_user_statistics AS dus
|
document_user_statistics AS dus
|
||||||
ON dus.document_id = docs.id AND dus.user_id = ?1
|
ON dus.document_id = docs.id AND dus.user_id = ?1
|
||||||
WHERE docs.deleted = false
|
WHERE
|
||||||
|
docs.deleted = false AND (
|
||||||
|
?2 IS NULL OR (
|
||||||
|
docs.title LIKE ?2 OR
|
||||||
|
docs.author LIKE ?2
|
||||||
|
)
|
||||||
|
)
|
||||||
ORDER BY dus.last_read DESC, docs.created_at DESC
|
ORDER BY dus.last_read DESC, docs.created_at DESC
|
||||||
LIMIT ?3
|
LIMIT ?4
|
||||||
OFFSET ?2
|
OFFSET ?3
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetDocumentsWithStatsParams struct {
|
type GetDocumentsWithStatsParams struct {
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
Offset int64 `json:"offset"`
|
Query interface{} `json:"query"`
|
||||||
Limit int64 `json:"limit"`
|
Offset int64 `json:"offset"`
|
||||||
|
Limit int64 `json:"limit"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetDocumentsWithStatsRow struct {
|
type GetDocumentsWithStatsRow struct {
|
||||||
@@ -661,7 +686,12 @@ type GetDocumentsWithStatsRow struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetDocumentsWithStats(ctx context.Context, arg GetDocumentsWithStatsParams) ([]GetDocumentsWithStatsRow, error) {
|
func (q *Queries) GetDocumentsWithStats(ctx context.Context, arg GetDocumentsWithStatsParams) ([]GetDocumentsWithStatsRow, error) {
|
||||||
rows, err := q.db.QueryContext(ctx, getDocumentsWithStats, arg.UserID, arg.Offset, arg.Limit)
|
rows, err := q.db.QueryContext(ctx, getDocumentsWithStats,
|
||||||
|
arg.UserID,
|
||||||
|
arg.Query,
|
||||||
|
arg.Offset,
|
||||||
|
arg.Limit,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,7 +136,9 @@ CREATE TEMPORARY TABLE IF NOT EXISTS document_user_statistics (
|
|||||||
read_percentage REAL NOT NULL,
|
read_percentage REAL NOT NULL,
|
||||||
percentage REAL NOT NULL,
|
percentage REAL NOT NULL,
|
||||||
words_read INTEGER NOT NULL,
|
words_read INTEGER NOT NULL,
|
||||||
wpm REAL NOT NULL
|
wpm REAL NOT NULL,
|
||||||
|
|
||||||
|
UNIQUE(document_id, user_id) ON CONFLICT REPLACE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
@@ -325,6 +327,8 @@ current_progress AS (
|
|||||||
WHERE
|
WHERE
|
||||||
dp.user_id = iga.user_id
|
dp.user_id = iga.user_id
|
||||||
AND dp.document_id = iga.document_id
|
AND dp.document_id = iga.document_id
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 1
|
||||||
), end_percentage) AS percentage
|
), end_percentage) AS percentage
|
||||||
FROM intermediate_ga AS iga
|
FROM intermediate_ga AS iga
|
||||||
GROUP BY user_id, document_id
|
GROUP BY user_id, document_id
|
||||||
|
|||||||
77
database/update_document_user_statistics.sql
Normal file
77
database/update_document_user_statistics.sql
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
INSERT INTO document_user_statistics
|
||||||
|
WITH intermediate_ga AS (
|
||||||
|
SELECT
|
||||||
|
ga1.id AS row_id,
|
||||||
|
ga1.user_id,
|
||||||
|
ga1.document_id,
|
||||||
|
ga1.duration,
|
||||||
|
ga1.start_time,
|
||||||
|
ga1.start_percentage,
|
||||||
|
ga1.end_percentage,
|
||||||
|
|
||||||
|
-- Find Overlapping Events (Assign Unique ID)
|
||||||
|
(
|
||||||
|
SELECT MIN(id)
|
||||||
|
FROM activity AS ga2
|
||||||
|
WHERE
|
||||||
|
ga1.document_id = ga2.document_id
|
||||||
|
AND ga1.user_id = ga2.user_id
|
||||||
|
AND ga1.start_percentage <= ga2.end_percentage
|
||||||
|
AND ga1.end_percentage >= ga2.start_percentage
|
||||||
|
) AS group_leader
|
||||||
|
FROM activity AS ga1
|
||||||
|
WHERE
|
||||||
|
document_id = ?
|
||||||
|
AND user_id = ?
|
||||||
|
),
|
||||||
|
grouped_activity AS (
|
||||||
|
SELECT
|
||||||
|
user_id,
|
||||||
|
document_id,
|
||||||
|
MAX(start_time) AS start_time,
|
||||||
|
MIN(start_percentage) AS start_percentage,
|
||||||
|
MAX(end_percentage) AS end_percentage,
|
||||||
|
MAX(end_percentage) - MIN(start_percentage) AS read_percentage,
|
||||||
|
SUM(duration) AS duration
|
||||||
|
FROM intermediate_ga
|
||||||
|
GROUP BY group_leader
|
||||||
|
),
|
||||||
|
current_progress AS (
|
||||||
|
SELECT
|
||||||
|
user_id,
|
||||||
|
document_id,
|
||||||
|
COALESCE((
|
||||||
|
SELECT percentage
|
||||||
|
FROM document_progress AS dp
|
||||||
|
WHERE
|
||||||
|
dp.user_id = iga.user_id
|
||||||
|
AND dp.document_id = iga.document_id
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
), end_percentage) AS percentage
|
||||||
|
FROM intermediate_ga AS iga
|
||||||
|
GROUP BY user_id, document_id
|
||||||
|
HAVING MAX(start_time)
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
ga.document_id,
|
||||||
|
ga.user_id,
|
||||||
|
MAX(start_time) AS last_read,
|
||||||
|
SUM(duration) AS total_time_seconds,
|
||||||
|
SUM(read_percentage) AS read_percentage,
|
||||||
|
cp.percentage,
|
||||||
|
|
||||||
|
(CAST(COALESCE(d.words, 0.0) AS REAL) * SUM(read_percentage))
|
||||||
|
AS words_read,
|
||||||
|
|
||||||
|
(CAST(COALESCE(d.words, 0.0) AS REAL) * SUM(read_percentage))
|
||||||
|
/ (SUM(duration) / 60.0) AS 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 d.id = ga.document_id
|
||||||
|
GROUP BY ga.document_id, ga.user_id
|
||||||
|
ORDER BY wpm DESC;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
services:
|
services:
|
||||||
bookmanager:
|
antholume:
|
||||||
environment:
|
environment:
|
||||||
- CONFIG_PATH=/data
|
- CONFIG_PATH=/data
|
||||||
- DATA_PATH=/data
|
- DATA_PATH=/data
|
||||||
|
|||||||
@@ -101,9 +101,6 @@ func getSVGBezierOpposedLine(pointA SVGGraphPoint, pointB SVGGraphPoint) SVGBezi
|
|||||||
Length: int(math.Sqrt(math.Pow(lengthX, 2) + math.Pow(lengthY, 2))),
|
Length: int(math.Sqrt(math.Pow(lengthX, 2) + math.Pow(lengthY, 2))),
|
||||||
Angle: int(math.Atan2(lengthY, lengthX)),
|
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 {
|
func getSVGBezierControlPoint(currentPoint *SVGGraphPoint, prevPoint *SVGGraphPoint, nextPoint *SVGGraphPoint, isReverse bool) SVGGraphPoint {
|
||||||
|
|||||||
6
main.go
6
main.go
@@ -24,13 +24,13 @@ func main() {
|
|||||||
log.SetFormatter(UTCFormatter{&log.TextFormatter{FullTimestamp: true}})
|
log.SetFormatter(UTCFormatter{&log.TextFormatter{FullTimestamp: true}})
|
||||||
|
|
||||||
app := &cli.App{
|
app := &cli.App{
|
||||||
Name: "Book Manager",
|
Name: "AnthoLume",
|
||||||
Usage: "A self hosted e-book progress tracker.",
|
Usage: "A self hosted e-book progress tracker.",
|
||||||
Commands: []*cli.Command{
|
Commands: []*cli.Command{
|
||||||
{
|
{
|
||||||
Name: "serve",
|
Name: "serve",
|
||||||
Aliases: []string{"s"},
|
Aliases: []string{"s"},
|
||||||
Usage: "Start Book Manager web server.",
|
Usage: "Start AnthoLume web server.",
|
||||||
Action: cmdServer,
|
Action: cmdServer,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -42,7 +42,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cmdServer(ctx *cli.Context) error {
|
func cmdServer(ctx *cli.Context) error {
|
||||||
log.Info("Starting Book Manager Server")
|
log.Info("Starting AnthoLume Server")
|
||||||
|
|
||||||
// Create Channel
|
// Create Channel
|
||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
|
|||||||
16
notes/README.md
Normal file
16
notes/README.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Notes
|
||||||
|
|
||||||
|
This folder consists of various notes / files that I want to save and may come back to at some point.
|
||||||
|
|
||||||
|
# Ideas / To Do
|
||||||
|
|
||||||
|
- Rename!
|
||||||
|
- Google Fonts -> SW Cache and/or Local
|
||||||
|
- Search Documents
|
||||||
|
- Title, Author, Description
|
||||||
|
- Change Device Name / Assume Device
|
||||||
|
- Hide Document per User (Another Table?)
|
||||||
|
- Admin User?
|
||||||
|
- Reset Passwords
|
||||||
|
- Actually Delete Documents
|
||||||
|
- Document & Activity Pagination
|
||||||
@@ -1,25 +1,25 @@
|
|||||||
# PWA Screenshots
|
# PWA Screenshots
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/login.png">
|
<a href="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/pwa/login.png">
|
||||||
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/login.png" width="32%">
|
<img src="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/pwa/login.png" width="32%">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/home.png">
|
<a href="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/pwa/home.png">
|
||||||
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/home.png" width="32%">
|
<img src="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/pwa/home.png" width="32%">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/activity.png">
|
<a href="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/pwa/activity.png">
|
||||||
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/activity.png" width="32%">
|
<img src="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/pwa/activity.png" width="32%">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/documents.png">
|
<a href="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/pwa/documents.png">
|
||||||
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/documents.png" width="32%">
|
<img src="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/pwa/documents.png" width="32%">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/document.png">
|
<a href="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/pwa/document.png">
|
||||||
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/document.png" width="32%">
|
<img src="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/pwa/document.png" width="32%">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/metadata.png">
|
<a href="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/pwa/metadata.png">
|
||||||
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/metadata.png" width="32%">
|
<img src="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/pwa/metadata.png" width="32%">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
# Web Screenshots
|
# Web Screenshots
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web/login.png">
|
<a href="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/web/login.png">
|
||||||
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web/login.png" width="49%">
|
<img src="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/web/login.png" width="49%">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web/home.png">
|
<a href="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/web/home.png">
|
||||||
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web/home.png" width="49%">
|
<img src="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/web/home.png" width="49%">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web/activity.png">
|
<a href="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/web/activity.png">
|
||||||
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web/activity.png" width="49%">
|
<img src="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/web/activity.png" width="49%">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web/documents.png">
|
<a href="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/web/documents.png">
|
||||||
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web/documents.png" width="49%">
|
<img src="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/web/documents.png" width="49%">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web/document.png">
|
<a href="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/web/document.png">
|
||||||
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web/document.png" width="49%">
|
<img src="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/web/document.png" width="49%">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web/metadata.png">
|
<a href="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/web/metadata.png">
|
||||||
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web/metadata.png" width="49%">
|
<img src="https://gitea.va.reichard.io/evan/AnthoLume/raw/branch/master/screenshots/web/metadata.png" width="49%">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ pkgs.mkShell {
|
|||||||
packages = with pkgs; [
|
packages = with pkgs; [
|
||||||
go
|
go
|
||||||
nodePackages.tailwindcss
|
nodePackages.tailwindcss
|
||||||
|
python311Packages.grip
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,31 +3,19 @@
|
|||||||
{{end}} {{define "content"}}
|
{{end}} {{define "content"}}
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<div class="inline-block min-w-full overflow-hidden rounded shadow">
|
<div class="inline-block min-w-full overflow-hidden rounded shadow">
|
||||||
<table class="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm md:text-sm">
|
<table class="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm">
|
||||||
<thead class="text-gray-800 dark:text-gray-400">
|
<thead class="text-gray-800 dark:text-gray-400">
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">
|
||||||
scope="col"
|
|
||||||
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
|
||||||
>
|
|
||||||
Document
|
Document
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">
|
||||||
scope="col"
|
|
||||||
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
|
||||||
>
|
|
||||||
Time
|
Time
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">
|
||||||
scope="col"
|
|
||||||
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
|
||||||
>
|
|
||||||
Duration
|
Duration
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">
|
||||||
scope="col"
|
|
||||||
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
|
||||||
>
|
|
||||||
Percent
|
Percent
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -38,7 +26,6 @@
|
|||||||
<td class="text-center p-3" colspan="4">No Results</td>
|
<td class="text-center p-3" colspan="4">No Results</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{range $activity := .Data }}
|
{{range $activity := .Data }}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="p-3 border-b border-gray-200">
|
<td class="p-3 border-b border-gray-200">
|
||||||
|
|||||||
@@ -9,14 +9,14 @@
|
|||||||
<meta name="theme-color" content="#F3F4F6" media="(prefers-color-scheme: light)">
|
<meta name="theme-color" content="#F3F4F6" media="(prefers-color-scheme: light)">
|
||||||
<meta name="theme-color" content="#1F2937" media="(prefers-color-scheme: dark)">
|
<meta name="theme-color" content="#1F2937" media="(prefers-color-scheme: dark)">
|
||||||
|
|
||||||
<title>Book Manager - {{block "title" .}}{{end}}</title>
|
<title>AnthoLume - {{block "title" .}}{{end}}</title>
|
||||||
|
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<link rel="stylesheet" href="/assets/style.css">
|
<link rel="stylesheet" href="/assets/style.css">
|
||||||
|
|
||||||
<!-- Service Worker / Offline Cache Flush -->
|
<!-- Service Worker / Offline Cache Flush -->
|
||||||
<script src="/assets/lib/idb-keyval.js"></script>
|
<script src="/assets/lib/idb-keyval.min.js"></script>
|
||||||
<script src="/assets/lib/sw-helper.js"></script>
|
<script src="/assets/common.js"></script>
|
||||||
<script src="/assets/index.js"></script>
|
<script src="/assets/index.js"></script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -127,16 +127,16 @@
|
|||||||
<body class="bg-gray-100 dark:bg-gray-800">
|
<body class="bg-gray-100 dark:bg-gray-800">
|
||||||
<div class="flex items-center justify-between w-full h-16">
|
<div class="flex items-center justify-between w-full h-16">
|
||||||
<div id="mobile-nav-button" class="flex flex-col z-40 relative ml-6">
|
<div id="mobile-nav-button" class="flex flex-col z-40 relative ml-6">
|
||||||
<input type="checkbox" class="absolute lg:hidden z-50 -top-2 w-7 h-7 flex cursor-pointer opacity-0 w-12 h-12" />
|
<input type="checkbox" class="absolute lg:hidden z-50 -top-2 w-7 h-7 flex cursor-pointer opacity-0" />
|
||||||
<span class="lg:hidden bg-black w-7 h-0.5 z-40 mt-0.5 dark:bg-white"></span>
|
<span class="lg:hidden bg-black w-7 h-0.5 z-40 mt-0.5 dark:bg-white"></span>
|
||||||
<span class="lg:hidden bg-black w-7 h-0.5 z-40 mt-1 dark:bg-white"></span>
|
<span class="lg:hidden bg-black w-7 h-0.5 z-40 mt-1 dark:bg-white"></span>
|
||||||
<span class="lg:hidden bg-black w-7 h-0.5 z-40 mt-1 dark:bg-white"></span>
|
<span class="lg:hidden bg-black w-7 h-0.5 z-40 mt-1 dark:bg-white"></span>
|
||||||
|
|
||||||
<div id="menu" class="fixed -ml-6 h-full w-56 lg:w-48 bg-white dark:bg-gray-700 shadow-lg">
|
<div id="menu" class="fixed -ml-6 h-full w-56 lg:w-48 bg-white dark:bg-gray-700 shadow-lg">
|
||||||
<div class="h-16 flex justify-end lg:justify-around">
|
<div class="h-16 flex justify-end lg:justify-around">
|
||||||
<p class="text-xl font-bold dark:text-white text-right my-auto pr-4 lg:pr-0">Book Manager</p>
|
<p class="text-xl font-bold dark:text-white text-right my-auto pr-8 lg:pr-0">AnthoLume</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-6">
|
<div>
|
||||||
<a
|
<a
|
||||||
class="flex items-center justify-start w-full p-2 pl-6 my-2 transition-colors duration-200 border-l-4 {{if eq .RouteName "home"}}border-purple-500 dark:text-white{{else}}border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{end}}"
|
class="flex items-center justify-start w-full p-2 pl-6 my-2 transition-colors duration-200 border-l-4 {{if eq .RouteName "home"}}border-purple-500 dark:text-white{{else}}border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{end}}"
|
||||||
href="/"
|
href="/"
|
||||||
@@ -232,12 +232,47 @@
|
|||||||
</a>
|
</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
|
<a class="flex justify-center items-center p-6 w-full absolute bottom-0" target="_blank" href="https://gitea.va.reichard.io/evan/AnthoLume">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="text-black dark:text-white"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 219 92"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="a"><path d="M159 .79h25V69h-25Zm0 0" /></clipPath>
|
||||||
|
<clipPath id="b"><path d="M183 9h35.371v60H183Zm0 0" /></clipPath>
|
||||||
|
<clipPath id="c"><path d="M0 .79h92V92H0Zm0 0" /></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"
|
||||||
|
/>
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-xl font-bold dark:text-white px-6 lg:ml-44">{{block "header" .}}{{end}}</h1>
|
<h1 class="text-xl font-bold dark:text-white px-6 lg:ml-44">{{block "header" .}}{{end}}</h1>
|
||||||
<div
|
<div class="relative flex items-center justify-end w-full p-4 space-x-4">
|
||||||
class="relative flex items-center justify-end w-full p-4 space-x-4"
|
|
||||||
>
|
|
||||||
<a href="#" class="relative block">
|
<a href="#" class="relative block">
|
||||||
<svg
|
<svg
|
||||||
width="20"
|
width="20"
|
||||||
@@ -268,7 +303,7 @@
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="/settings"
|
href="/settings"
|
||||||
class="block block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600"
|
class="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
>
|
>
|
||||||
<span class="flex flex-col">
|
<span class="flex flex-col">
|
||||||
@@ -277,7 +312,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="/logout"
|
href="/logout"
|
||||||
class="block block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600"
|
class="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
>
|
>
|
||||||
<span class="flex flex-col">
|
<span class="flex flex-col">
|
||||||
@@ -309,9 +344,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main
|
<main class="relative overflow-hidden">
|
||||||
class="relative overflow-hidden"
|
|
||||||
>
|
|
||||||
<div id="container" class="h-[100dvh] px-4 overflow-auto md:px-6 lg:ml-48">
|
<div id="container" class="h-[100dvh] px-4 overflow-auto md:px-6 lg:ml-48">
|
||||||
{{block "content" .}}{{end}}
|
{{block "content" .}}{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,166 +0,0 @@
|
|||||||
{{template "base.html" .}}
|
|
||||||
|
|
||||||
{{define "title"}}Documents{{end}}
|
|
||||||
|
|
||||||
{{define "header"}}
|
|
||||||
<a href="../documents">Documents</a>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{define "content"}}
|
|
||||||
<div class="h-full w-full overflow-scroll bg-white shadow-lg dark:bg-gray-700 rounded dark:text-white p-6">
|
|
||||||
<div class="flex flex-col gap-2 float-left mr-6 mb-6">
|
|
||||||
<img class="rounded w-40 md:w-60 lg:w-80 object-fill h-full" src="../documents/{{.Data.ID}}/cover"></img>
|
|
||||||
<div class="flex gap-2 justify-end text-gray-500 dark:text-gray-400">
|
|
||||||
<div class="relative">
|
|
||||||
<label for="delete-button">
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
class="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<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
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</label>
|
|
||||||
<input type="checkbox" id="delete-button" class="hidden css-button"/>
|
|
||||||
<form
|
|
||||||
method="POST"
|
|
||||||
action="./{{ .Data.ID }}/delete"
|
|
||||||
class="absolute bottom-7 left-5 text-black bg-gray-200 transition-all duration-200 rounded shadow-inner shadow-lg shadow-gray-500 dark:text-white dark:shadow-gray-900 dark:bg-gray-600 text-sm p-3"
|
|
||||||
>
|
|
||||||
<p class="font-medium w-24 pb-2">Are you sure?</p>
|
|
||||||
<button class="font-medium w-full px-2 py-1 text-white bg-gray-500 dark:text-gray-800 dark:bg-gray-400 hover:bg-gray-800 dark:hover:bg-gray-100" type="submit">Delete</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<a href="../activity?document={{ .Data.ID }}">
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
class="hover:text-gray-800 dark:hover:text-gray-100"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<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 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"/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<div class="relative">
|
|
||||||
<label for="edit-button">
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
class="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<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
|
|
||||||
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
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</label>
|
|
||||||
<input type="checkbox" id="edit-button" class="hidden css-button"/>
|
|
||||||
<form
|
|
||||||
method="POST"
|
|
||||||
action="./{{ .Data.ID }}/edit"
|
|
||||||
class="absolute bottom-7 left-5 text-black bg-gray-200 transition-all duration-200 rounded shadow-inner shadow-lg shadow-gray-500 dark:text-white dark:shadow-gray-900 dark:bg-gray-600 text-sm p-3"
|
|
||||||
>
|
|
||||||
<label class="font-medium" for="isbn">ISBN</label>
|
|
||||||
<input class="mt-1 mb-2 p-1 bg-gray-400 text-black dark:bg-gray-700 dark:text-white" type="text" id="isbn" name="ISBN"><br>
|
|
||||||
<button class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 dark:bg-gray-400 hover:bg-gray-800 dark:hover:bg-gray-100" type="submit">Search Metadata</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{{ if .Data.Filepath }}
|
|
||||||
<a href="./{{.Data.ID}}/file">
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
class="hover:text-gray-800 dark:hover:text-gray-100"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
{{ else }}
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
class="text-gray-200 dark:text-gray-600"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap justify-between gap-6 pb-6">
|
|
||||||
<div>
|
|
||||||
<p class="text-gray-400">Title</p>
|
|
||||||
<!-- <input type="text" class="font-medium bg-transparent" value="{{ or .Data.Title "Unknown" }}"/> -->
|
|
||||||
<p class="font-medium text-lg">
|
|
||||||
{{ or .Data.Title "N/A" }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-gray-400">Author</p>
|
|
||||||
<p class="font-medium text-lg">
|
|
||||||
{{ or .Data.Author "N/A" }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-gray-400">Progress</p>
|
|
||||||
<p class="font-medium text-lg">
|
|
||||||
{{ .Data.Page }} / {{ .Data.Pages }} ({{ .Data.Percentage }}%)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-gray-400">Time Read</p>
|
|
||||||
<p class="font-medium text-lg">
|
|
||||||
{{ .Data.TotalTimeMinutes }} Minutes
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="text-gray-400">Description</p>
|
|
||||||
<p class="font-medium text-justify hyphens-auto">
|
|
||||||
{{ or .Data.Description "N/A" }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<style>
|
|
||||||
.css-button:checked + form {
|
|
||||||
visibility: visible;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.css-button + form {
|
|
||||||
visibility: hidden;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{{end}}
|
|
||||||
@@ -67,7 +67,6 @@
|
|||||||
type="submit"
|
type="submit"
|
||||||
>Remove Cover</button>
|
>Remove Cover</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<label for="delete-button">
|
<label for="delete-button">
|
||||||
@@ -323,7 +322,6 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
<input type="checkbox" id="progress-info-button" class="hidden css-button"/>
|
<input type="checkbox" id="progress-info-button" class="hidden css-button"/>
|
||||||
|
|
||||||
<div class="absolute z-30 top-7 right-0 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600">
|
<div class="absolute z-30 top-7 right-0 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600">
|
||||||
<div class="text-xs flex">
|
<div class="text-xs flex">
|
||||||
<p class="text-gray-400 w-32">Seconds / Percent</p>
|
<p class="text-gray-400 w-32">Seconds / Percent</p>
|
||||||
@@ -355,20 +353,6 @@
|
|||||||
{{ .Data.Percentage }}%
|
{{ .Data.Percentage }}%
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<!--
|
|
||||||
<div>
|
|
||||||
<p class="text-gray-500">ISBN 10</p>
|
|
||||||
<p class="font-medium text-lg">
|
|
||||||
{{ or .Data.Isbn10 "N/A" }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-gray-500">ISBN 13</p>
|
|
||||||
<p class="font-medium text-lg">
|
|
||||||
{{ or .Data.Isbn13 "N/A" }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
-->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
|
|||||||
@@ -7,6 +7,49 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
|
<div
|
||||||
|
class="flex flex-col gap-2 grow p-4 mb-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
||||||
|
>
|
||||||
|
<form class="flex gap-4 flex-col lg:flex-row" action="./documents" method="GET">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<rect width="24" height="24" fill="none" />
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="search"
|
||||||
|
name="search"
|
||||||
|
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="Search Author / Title"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
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">Search</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{{range $doc := .Data }}
|
{{range $doc := .Data }}
|
||||||
<div class="w-full relative">
|
<div class="w-full relative">
|
||||||
@@ -104,6 +147,16 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full flex gap-4 justify-center mt-4 text-black dark:text-white">
|
||||||
|
{{ if .PreviousPage }}
|
||||||
|
<a href="./documents?page={{ .PreviousPage }}&limit={{ .PageLimit }}" class="bg-white shadow-lg dark:bg-gray-600 hover:bg-gray-400 font-medium rounded text-sm text-center p-2 w-24 dark:hover:bg-gray-700 focus:outline-none">◄</a>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ if .NextPage }}
|
||||||
|
<a href="./documents?page={{ .NextPage }}&limit={{ .PageLimit }}" class="bg-white shadow-lg dark:bg-gray-600 hover:bg-gray-400 font-medium rounded text-sm text-center p-2 w-24 dark:hover:bg-gray-700 focus:outline-none">►</a>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="fixed bottom-6 right-6 rounded-full flex items-center justify-center">
|
<div class="fixed bottom-6 right-6 rounded-full flex items-center justify-center">
|
||||||
<input type="checkbox" id="upload-file-button" class="hidden css-button"/>
|
<input type="checkbox" id="upload-file-button" class="hidden css-button"/>
|
||||||
<div class="rounded p-4 bg-gray-800 dark:bg-gray-200 text-white dark:text-black w-72 text-sm flex flex-col gap-2">
|
<div class="rounded p-4 bg-gray-800 dark:bg-gray-200 text-white dark:text-black w-72 text-sm flex flex-col gap-2">
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
media="(prefers-color-scheme: dark)"
|
media="(prefers-color-scheme: dark)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<title>Book Manager - Error</title>
|
<title>AnthoLume - Error</title>
|
||||||
|
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<link rel="stylesheet" href="/assets/style.css" />
|
<link rel="stylesheet" href="/assets/style.css" />
|
||||||
@@ -30,29 +30,27 @@
|
|||||||
<body
|
<body
|
||||||
class="bg-gray-100 dark:bg-gray-800 flex flex-col justify-center h-screen"
|
class="bg-gray-100 dark:bg-gray-800 flex flex-col justify-center h-screen"
|
||||||
>
|
>
|
||||||
<section>
|
<div class="py-8 px-4 mx-auto max-w-screen-xl lg:py-16 lg:px-6">
|
||||||
<div class="py-8 px-4 mx-auto max-w-screen-xl lg:py-16 lg:px-6">
|
<div class="mx-auto max-w-screen-sm text-center">
|
||||||
<div class="mx-auto max-w-screen-sm text-center">
|
<h1
|
||||||
<h1
|
class="mb-4 text-7xl tracking-tight font-extrabold lg:text-9xl text-gray-600 dark:text-gray-500"
|
||||||
class="mb-4 text-7xl tracking-tight font-extrabold lg:text-9xl text-gray-600 dark:text-gray-500"
|
>
|
||||||
>
|
{{ .Status }}
|
||||||
{{ .Status }}
|
</h1>
|
||||||
</h1>
|
<p
|
||||||
<p
|
class="mb-4 text-3xl tracking-tight font-bold text-gray-900 md:text-4xl dark:text-white"
|
||||||
class="mb-4 text-3xl tracking-tight font-bold text-gray-900 md:text-4xl dark:text-white"
|
>
|
||||||
>
|
{{ .Error }}
|
||||||
{{ .Error }}
|
</p>
|
||||||
</p>
|
<p class="mb-8 text-lg font-light text-gray-500 dark:text-gray-400">
|
||||||
<p class="mb-8 text-lg font-light text-gray-500 dark:text-gray-400">
|
{{ .Message }}
|
||||||
{{ .Message }}
|
</p>
|
||||||
</p>
|
<a
|
||||||
<a
|
href="/"
|
||||||
href="/"
|
class="rounded text-center font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||||
class="rounded text-center font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
>Back to Homepage</a
|
||||||
>Back to Homepage</a
|
>
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -22,14 +22,14 @@
|
|||||||
media="(prefers-color-scheme: dark)"
|
media="(prefers-color-scheme: dark)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<title>Book Manager - {{if .Register}}Register{{else}}Login{{end}}</title>
|
<title>AnthoLume - {{if .Register}}Register{{else}}Login{{end}}</title>
|
||||||
|
|
||||||
<link rel="manifest" href="./manifest.json" />
|
<link rel="manifest" href="./manifest.json" />
|
||||||
<link rel="stylesheet" href="./assets/style.css" />
|
<link rel="stylesheet" href="./assets/style.css" />
|
||||||
|
|
||||||
<!-- Service Worker / Offline Cache Flush -->
|
<!-- Service Worker / Offline Cache Flush -->
|
||||||
<script src="/assets/lib/idb-keyval.js"></script>
|
<script src="/assets/lib/idb-keyval.min.js"></script>
|
||||||
<script src="/assets/lib/sw-helper.js"></script>
|
<script src="/assets/common.js"></script>
|
||||||
<script src="/assets/index.js"></script>
|
<script src="/assets/index.js"></script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
Reference in New Issue
Block a user