[add] docker instructions, [add] metadata gathering, [add] screenshots

This commit is contained in:
Evan Reichard 2023-09-26 18:09:02 -04:00
parent c22154ed77
commit df7f806834
28 changed files with 837 additions and 537 deletions

View File

@ -1,19 +1,35 @@
FROM alpine:edge AS build
RUN apk add --no-cache --update go gcc g++
WORKDIR /app
COPY . /app
# Certificate Store
FROM alpine as certs
RUN apk update && apk add ca-certificates
# Copy Resources
# Build Image
FROM --platform=$BUILDPLATFORM golang:1.20 AS build
# Install Dependencies
RUN apt-get update -y
RUN apt install -y gcc-x86-64-linux-gnu
# Create Package Directory
WORKDIR /src
RUN mkdir -p /opt/bookmanager
RUN cp -a ./templates /opt/bookmanager/templates
RUN cp -a ./assets /opt/bookmanager/assets
# Download Dependencies & Compile
RUN go mod download
RUN CGO_ENABLED=1 CGO_CFLAGS="-D_LARGEFILE64_SOURCE" go build -o /opt/bookmanager/server
# Cache Dependencies & Compile
ARG TARGETOS
ARG TARGETARCH
RUN --mount=target=. \
--mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg \
if [ "$TARGETARCH" = "amd64" ]; then \
GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=1 CGO_CFLAGS="-D_LARGEFILE64_SOURCE" CC=x86_64-linux-gnu-gcc go build -o /opt/bookmanager/server; \
else \
GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=1 CGO_CFLAGS="-D_LARGEFILE64_SOURCE" go build -o /opt/bookmanager/server; \
fi; \
cp -a ./templates /opt/bookmanager/templates; \
cp -a ./assets /opt/bookmanager/assets;
# Create Image
FROM alpine:3.18
FROM busybox:1.36
COPY --from=certs /etc/ssl/certs /etc/ssl/certs
COPY --from=build /opt/bookmanager /opt/bookmanager
WORKDIR /opt/bookmanager
EXPOSE 8585

View File

@ -14,9 +14,13 @@ docker_build_local:
docker_build_release_beta:
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t gitea.va.reichard.io/reichard/bookmanager:beta --push .
-t gitea.va.reichard.io/reichard/bookmanager:beta \
-t gitea.va.reichard.io/evan/bookmanager:beta \
--push .
docker_build_release_latest:
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t gitea.va.reichard.io/reichard/bookmanager:latest --push .
-t gitea.va.reichard.io/reichard/bookmanager:latest \
-t gitea.va.reichard.io/evan/bookmanager:latest \
--push .

View File

@ -1,14 +1,29 @@
# Book Manager
<p align="center">
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web_login.png">
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web_login.png" width="30%">
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/login.png">
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/login.png" width="19%">
</a>
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web_home.png">
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web_home.png" width="30%">
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/home.png">
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/home.png" width="19%">
</a>
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web_documents.png">
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web_documents.png" width="30%">
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/documents.png">
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/documents.png" width="19%">
</a>
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/document.png">
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/document.png" width="19%">
</a>
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/metadata.png">
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/metadata.png" width="19%">
</a>
</p>
<p align="center">
<a href="https://gitea.va.reichard.io/evan/BookManager/src/branch/master/screenshots/web/README.md">
--- WEB ---
</a>
<a href="https://gitea.va.reichard.io/evan/BookManager/src/branch/master/screenshots/pwa/README.md">
--- PWA ---
</a>
</p>
@ -24,11 +39,30 @@ In additional to the compatible KOSync API's, we add:
- Additional APIs to automatically upload reading statistics
- Automatically upload documents to the server (can download in the "Documents" view)
- Automatic book cover metadata scraping (Thanks [OpenLibrary](https://openlibrary.org/))
- Book metadata scraping (Thanks [OpenLibrary](https://openlibrary.org/) & [Google Books API](https://developers.google.com/books/docs/v1/getting_started))
- No JavaScript! All information is rendered server side.
# Server
Docker Image: `docker pull gitea.va.reichard.io/evan/bookmanager:latest`
## Quick Start
```bash
# Make Data Directory
mkdir -p bookmanager_data
# Run Server
docker run \
-p 8585:8585 \
-e REGISTRATION_ENABLED=true \
-v ./bookmanager_data:/config \
-v ./bookmanager_data:/data \
gitea.va.reichard.io/evan/bookmanager:latest
```
The service is now accessible at: `http://localhost:8585`
## Configuration
| Environment Variable | Default Value | Description |
@ -50,22 +84,22 @@ See documentation in the `client` subfolder: [SyncNinja](https://gitea.va.reicha
SQLC Generation (v1.21.0):
```
```bash
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
~/go/bin/sqlc generate
```
Run Development:
```
CONFIG_PATH=./data DATA_PATH=./data go run cmd/main.go serve
```bash
CONFIG_PATH=./data DATA_PATH=./data go run main.go serve
```
# Building
The `Dockerfile` and `Makefile` contain the build information:
```
```bash
# Build Local Docker Image
make docker_build_local
@ -75,12 +109,12 @@ make docker_build_release_latest
If manually building, you must enable CGO:
```
```bash
# Download Dependencies
go mod download
# Compile (Binary `./bookmanager`)
CGO_ENABLED=1 CGO_CFLAGS="-D_LARGEFILE64_SOURCE" go build -o /bookmanager cmd/main.go
CGO_ENABLED=1 CGO_CFLAGS="-D_LARGEFILE64_SOURCE" go build -o /bookmanager
```
## Notes

View File

@ -21,8 +21,11 @@ type requestDocumentEdit struct {
Title *string `form:"title"`
Author *string `form:"author"`
Description *string `form:"description"`
ISBN10 *string `form:"isbn_10"`
ISBN13 *string `form:"isbn_13"`
RemoveCover *string `form:"remove_cover"`
CoverFile *multipart.FileHeader `form:"cover"`
CoverGBID *string `form:"cover_gbid"`
CoverFile *multipart.FileHeader `form:"cover_file"`
}
type requestDocumentIdentify struct {
@ -101,6 +104,7 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any
return
}
templateVars["RelBase"] = "../"
templateVars["Data"] = document
} else if routeName == "activity" {
activityFilter := database.GetActivityParams{
@ -131,7 +135,7 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any
if err != nil {
log.Warn("[createAppResourcesRoute] GetUserWindowStreaks DB Error:", err)
}
log.Info("GetUserWindowStreaks - WEEK - ", time.Since(start_time))
log.Debug("GetUserWindowStreaks - WEEK - ", time.Since(start_time))
start_time = time.Now()
daily_streak, err := api.DB.Queries.GetUserWindowStreaks(api.DB.Ctx, database.GetUserWindowStreaksParams{
@ -141,15 +145,15 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any
if err != nil {
log.Warn("[createAppResourcesRoute] GetUserWindowStreaks DB Error:", err)
}
log.Info("GetUserWindowStreaks - DAY - ", time.Since(start_time))
log.Debug("GetUserWindowStreaks - DAY - ", time.Since(start_time))
start_time = time.Now()
database_info, _ := api.DB.Queries.GetDatabaseInfo(api.DB.Ctx, rUser.(string))
log.Info("GetDatabaseInfo - ", time.Since(start_time))
log.Debug("GetDatabaseInfo - ", time.Since(start_time))
start_time = time.Now()
read_graph_data, _ := api.DB.Queries.GetDailyReadStats(api.DB.Ctx, rUser.(string))
log.Info("GetDailyReadStats - ", time.Since(start_time))
log.Debug("GetDailyReadStats - ", time.Since(start_time))
templateVars["Data"] = gin.H{
"DailyStreak": daily_streak,
@ -218,7 +222,7 @@ func (api *API) getDocumentCover(c *gin.Context) {
firstResult := metadataResults[0]
// Save Cover
fileName, err := metadata.SaveCover(*firstResult.GBID, coverDir, document.ID)
fileName, err := metadata.SaveCover(*firstResult.GBID, coverDir, document.ID, false)
if err == nil {
coverFile = *fileName
}
@ -275,8 +279,11 @@ func (api *API) editDocument(c *gin.Context) {
if rDocEdit.Author == nil &&
rDocEdit.Title == nil &&
rDocEdit.Description == nil &&
rDocEdit.CoverFile == nil &&
rDocEdit.RemoveCover == nil {
rDocEdit.ISBN10 == nil &&
rDocEdit.ISBN13 == nil &&
rDocEdit.RemoveCover == nil &&
rDocEdit.CoverGBID == nil &&
rDocEdit.CoverFile == nil {
log.Error("[createAppResourcesRoute] Missing Form Values")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
return
@ -288,7 +295,6 @@ func (api *API) editDocument(c *gin.Context) {
s := "UNKNOWN"
coverFileName = &s
} else if rDocEdit.CoverFile != nil {
// Validate Type & Derive Extension on MIME
uploadedFile, err := rDocEdit.CoverFile.Open()
if err != nil {
@ -325,6 +331,14 @@ func (api *API) editDocument(c *gin.Context) {
}
coverFileName = &fileName
} else if rDocEdit.CoverGBID != nil {
// TODO
var coverDir string = filepath.Join(api.Config.DataPath, "covers")
fileName, err := metadata.SaveCover(*rDocEdit.CoverGBID, coverDir, rDocID.DocumentID, true)
if err == nil {
coverFileName = fileName
}
}
// Update Document
@ -333,6 +347,8 @@ func (api *API) editDocument(c *gin.Context) {
Title: api.sanitizeInput(rDocEdit.Title),
Author: api.sanitizeInput(rDocEdit.Author),
Description: api.sanitizeInput(rDocEdit.Description),
Isbn10: api.sanitizeInput(rDocEdit.ISBN10),
Isbn13: api.sanitizeInput(rDocEdit.ISBN13),
Coverfile: coverFileName,
}); err != nil {
log.Error("[createAppResourcesRoute] UpsertDocument DB Error:", err)
@ -367,6 +383,8 @@ func (api *API) deleteDocument(c *gin.Context) {
}
func (api *API) identifyDocument(c *gin.Context) {
rUser, _ := c.Get("AuthorizedUser")
var rDocID requestDocumentID
if err := c.ShouldBindUri(&rDocID); err != nil {
log.Error("[identifyDocument] Invalid URI Bind")
@ -399,36 +417,52 @@ func (api *API) identifyDocument(c *gin.Context) {
return
}
// Template Variables
templateVars := gin.H{
"RelBase": "../../",
}
// Get Metadata
metadataResults, err := metadata.GetMetadata(metadata.MetadataInfo{
Title: rDocIdentify.Title,
Author: rDocIdentify.Author,
ISBN10: rDocIdentify.ISBN,
ISBN13: rDocIdentify.ISBN,
})
if err != nil || len(metadataResults) == 0 {
log.Error("[identifyDocument] Metadata Error")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Metadata Error"})
if err == nil && len(metadataResults) > 0 {
firstResult := metadataResults[0]
// Store First Metadata Result
if _, err = api.DB.Queries.AddMetadata(api.DB.Ctx, database.AddMetadataParams{
DocumentID: rDocID.DocumentID,
Title: firstResult.Title,
Author: firstResult.Author,
Description: firstResult.Description,
Gbid: firstResult.GBID,
Olid: firstResult.OLID,
Isbn10: firstResult.ISBN10,
Isbn13: firstResult.ISBN13,
}); err != nil {
log.Error("[identifyDocument] AddMetadata DB Error:", err)
}
templateVars["Metadata"] = firstResult
} else {
log.Warn("[identifyDocument] Metadata Error")
templateVars["MetadataError"] = "No Metadata Found"
}
document, err := api.DB.Queries.GetDocumentWithStats(api.DB.Ctx, database.GetDocumentWithStatsParams{
UserID: rUser.(string),
DocumentID: rDocID.DocumentID,
})
if err != nil {
log.Error("[identifyDocument] GetDocumentWithStats DB Error:", err)
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
return
}
// TODO
firstResult := metadataResults[0]
templateVars["Data"] = document
if firstResult.Title != nil {
log.Info("Title:", *firstResult.Title)
}
if firstResult.Author != nil {
log.Info("Author:", *firstResult.Author)
}
if firstResult.Description != nil {
log.Info("Description:", *firstResult.Description)
}
if firstResult.ISBN10 != nil {
log.Info("ISBN 10:", *firstResult.ISBN10)
}
if firstResult.ISBN13 != nil {
log.Info("ISBN 13:", *firstResult.ISBN13)
}
c.Redirect(http.StatusFound, "/")
c.HTML(http.StatusOK, "document", templateVars)
}

View File

@ -85,6 +85,7 @@ func (api *API) authFormLogin(c *gin.Context) {
if username == "" || rawPassword == "" {
c.HTML(http.StatusUnauthorized, "login", gin.H{
"RegistrationEnabled": api.Config.RegistrationEnabled,
"Error": "Invalid Credentials",
})
return
@ -93,6 +94,7 @@ func (api *API) authFormLogin(c *gin.Context) {
if authorized := api.authorizeCredentials(username, password); authorized != true {
c.HTML(http.StatusUnauthorized, "login", gin.H{
"RegistrationEnabled": api.Config.RegistrationEnabled,
"Error": "Invalid Credentials",
})
return

View File

@ -380,8 +380,12 @@ current_streak AS (
end_date AS current_streak_end_date
FROM streaks
WHERE CASE
WHEN ?2 = "WEEK" THEN DATE('now', time_offset, 'weekday 0', '-7 day') = current_streak_end_date
WHEN ?2 = "DAY" THEN DATE('now', time_offset, '-1 day') = current_streak_end_date OR DATE('now', time_offset) = current_streak_end_date
WHEN ?2 = "WEEK" THEN
DATE('now', time_offset, 'weekday 0', '-14 day') = current_streak_end_date
OR DATE('now', time_offset, 'weekday 0', '-7 day') = current_streak_end_date
WHEN ?2 = "DAY" THEN
DATE('now', time_offset, '-1 day') = current_streak_end_date
OR DATE('now', time_offset) = current_streak_end_date
END
LIMIT 1
)

View File

@ -1006,8 +1006,12 @@ current_streak AS (
end_date AS current_streak_end_date
FROM streaks
WHERE CASE
WHEN ?2 = "WEEK" THEN DATE('now', time_offset, 'weekday 0', '-7 day') = current_streak_end_date
WHEN ?2 = "DAY" THEN DATE('now', time_offset, '-1 day') = current_streak_end_date OR DATE('now', time_offset) = current_streak_end_date
WHEN ?2 = "WEEK" THEN
DATE('now', time_offset, 'weekday 0', '-14 day') = current_streak_end_date
OR DATE('now', time_offset, 'weekday 0', '-7 day') = current_streak_end_date
WHEN ?2 = "DAY" THEN
DATE('now', time_offset, '-1 day') = current_streak_end_date
OR DATE('now', time_offset) = current_streak_end_date
END
LIMIT 1
)

View File

@ -129,7 +129,7 @@ func GetMetadata(metadataSearch MetadataInfo) ([]MetadataInfo, error) {
return allMetadata, nil
}
func SaveCover(gbid string, coverDir string, documentID string) (*string, error) {
func SaveCover(gbid string, coverDir string, documentID string, overwrite bool) (*string, error) {
// Google Books -> JPG
coverFile := "." + filepath.Clean(fmt.Sprintf("/%s.jpg", documentID))
@ -137,7 +137,7 @@ func SaveCover(gbid string, coverDir string, documentID string) (*string, error)
// Validate File Doesn't Exists
_, err := os.Stat(coverFilePath)
if err == nil {
if err == nil && overwrite == false {
log.Warn("[SaveCover] File Alreads Exists")
return &coverFile, nil
}

13
screenshots/README.md Normal file
View File

@ -0,0 +1,13 @@
# Screenshots
You can find PWA and Web screenshots in their respective folders.
## Process Images
```bash
# Resize
sips -Z 1500 *.png
# Crop Top & Bottom
sips --cropOffset 85 1 -c 1385 688 *.png
```

25
screenshots/pwa/README.md Normal file
View File

@ -0,0 +1,25 @@
# PWA Screenshots
<p align="center">
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/login.png">
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/login.png" width="32%">
</a>
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/home.png">
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/home.png" width="32%">
</a>
<a href="https://gitea.va.reichard.io/evan/BookManager/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%">
</a>
</p>
<p align="center">
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/documents.png">
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/documents.png" width="32%">
</a>
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/document.png">
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/document.png" width="32%">
</a>
<a href="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/metadata.png">
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/metadata.png" width="32%">
</a>
</p>

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 KiB

BIN
screenshots/pwa/home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

BIN
screenshots/pwa/login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

28
screenshots/web/README.md Normal file
View File

@ -0,0 +1,28 @@
# Web Screenshots
<p align="center">
<a href="https://gitea.va.reichard.io/evan/BookManager/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%">
</a>
<a href="https://gitea.va.reichard.io/evan/BookManager/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%">
</a>
</p>
<p align="center">
<a href="https://gitea.va.reichard.io/evan/BookManager/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%">
</a>
<a href="https://gitea.va.reichard.io/evan/BookManager/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%">
</a>
</p>
<p align="center">
<a href="https://gitea.va.reichard.io/evan/BookManager/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%">
</a>
<a href="https://gitea.va.reichard.io/evan/BookManager/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%">
</a>
</p>

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 791 KiB

BIN
screenshots/web/home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

BIN
screenshots/web/login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 MiB

View File

@ -1,9 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="manifest" href="./manifest.json" />
<link rel="manifest" href="{{ .RelBase }}./manifest.json" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<meta name="viewport"
content="width=device-width, initial-scale=0.90, user-scalable=no">
<script src="https://cdn.tailwindcss.com"></script>
<title>Book Manager - {{block "title" .}}{{end}}</title>
</head>
@ -11,33 +12,18 @@
<main
class="relative h-screen overflow-hidden bg-gray-100 dark:bg-gray-800"
>
<div class="flex items-start justify-between">
<input type="checkbox" id="mobile-nav-button" class="hidden"/>
<div class="fixed -left-64 duration-500 transition-all w-56 z-50 h-screen shadow-lg lg:left-0 lg:block lg:relative">
<div class="h-full bg-white dark:bg-gray-700">
<div class="flex items-center justify-center gap-4 h-16">
<label
id="mobile-nav-close-button"
for="mobile-nav-button"
class="flex block items-center p-2 text-gray-500 bg-white rounded-full shadow text-md cursor-pointer lg:hidden"
>
<svg
width="20"
height="20"
class="text-gray-400"
fill="currentColor"
viewBox="0 0 1792 1792"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1664 1344v128q0 26-19 45t-45 19h-1408q-26 0-45-19t-19-45v-128q0-26 19-45t45-19h1408q26 0 45 19t19 45zm0-512v128q0 26-19 45t-45 19h-1408q-26 0-45-19t-19-45v-128q0-26 19-45t45-19h1408q26 0 45 19t19 45zm0-512v128q0 26-19 45t-45 19h-1408q-26 0-45-19t-19-45v-128q0-26 19-45t45-19h1408q26 0 45 19t19 45z"
></path>
</svg>
</label>
<p class="text-xl font-bold dark:text-white">Book Manager</p>
<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">
<input type="checkbox" class="absolute lg:hidden z-50 -top-2 w-7 h-7 flex cursor-pointer opacity-0 w-12 h-12" />
<span class="lg:hidden w-7 h-0.5 z-40 mt-0.5 bg-white"></span>
<span class="lg:hidden w-7 h-0.5 z-40 mt-1 bg-white"></span>
<span class="lg:hidden w-7 h-0.5 z-40 mt-1 bg-white"></span>
<div id="menu" class="fixed -mt-6 -ml-6 lg:-mt-8 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">
<p class="text-xl font-bold dark:text-white text-right my-auto pr-4 lg:pr-0">Book Manager</p>
</div>
<nav class="mt-6">
<div>
<div class="mt-6">
<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}}"
href="/"
@ -114,30 +100,8 @@
<span class="mx-4 text-sm font-normal"> Graphs </span>
</a>
</div>
</nav>
</div>
</div>
<div class="flex flex-col w-full">
<header class="z-40 flex items-center justify-between w-full h-16">
<div class="block ml-6 lg:hidden">
<label
for="mobile-nav-button"
class="flex items-center p-2 text-gray-500 bg-white rounded-full shadow text-md cursor-pointer"
>
<svg
width="20"
height="20"
class="text-gray-400"
fill="currentColor"
viewBox="0 0 1792 1792"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1664 1344v128q0 26-19 45t-45 19h-1408q-26 0-45-19t-19-45v-128q0-26 19-45t45-19h1408q26 0 45 19t19 45zm0-512v128q0 26-19 45t-45 19h-1408q-26 0-45-19t-19-45v-128q0-26 19-45t45-19h1408q26 0 45 19t19 45zm0-512v128q0 26-19 45t-45 19h-1408q-26 0-45-19t-19-45v-128q0-26 19-45t45-19h1408q26 0 45 19t19 45z"
></path>
</svg>
</label>
</div>
<h1 class="text-xl font-bold dark:text-white px-6">{{block "header" .}}{{end}}</h1>
<div
class="relative flex items-center justify-end w-full p-4 space-x-4"
@ -159,7 +123,7 @@
<input type="checkbox" id="user-dropdown-button" class="hidden"/>
<div
id="user-dropdown"
class="transition duration-200 absolute right-4 top-16 pt-4"
class="transition duration-200 z-20 absolute right-4 top-16 pt-4"
>
<div
class="w-56 origin-top-right bg-white rounded-md shadow-lg dark:shadow-gray-800 dark:bg-gray-700 ring-1 ring-black ring-opacity-5"
@ -202,24 +166,16 @@
</div>
</label>
</div>
</header>
<div class="h-screen px-4 pb-24 overflow-auto md:px-6">
</div>
<div class="h-screen px-4 pb-24 overflow-auto md:px-6 lg:ml-48">
{{block "content" .}}{{end}}
</div>
</div>
</div>
</main>
<!-- Custom Animation CSS -->
<style>
/* ----------------------------- */
/* ------ Navigation Slide ----- */
/* ----------------------------- */
#mobile-nav-button:checked + div {
left: 0px;
}
/* ----------------------------- */
/* ------- User Dropdown ------- */
/* ----------------------------- */
@ -232,6 +188,55 @@
visibility: hidden;
opacity: 0;
}
/* ----------------------------- */
/* ----- Mobile Navigation ----- */
/* ----------------------------- */
#mobile-nav-button span {
transform-origin: 5px 0px;
transition: transform 0.5s cubic-bezier(0.77,0.2,0.05,1.0),
background 0.5s cubic-bezier(0.77,0.2,0.05,1.0),
opacity 0.55s ease;
}
#mobile-nav-button span:first-child {
transform-origin: 0% 0%;
}
#mobile-nav-button span:nth-last-child(2) {
transform-origin: 0% 100%;
}
#mobile-nav-button input:checked ~ span {
opacity: 1;
transform: rotate(45deg) translate(2px, -2px);
background: #FFFFFF;
}
#mobile-nav-button input:checked ~ span:nth-last-child(3) {
opacity: 0;
transform: rotate(0deg) scale(0.2, 0.2);
}
#mobile-nav-button input:checked ~ span:nth-last-child(2) {
transform: rotate(-45deg) translate(0, 6px);
}
#mobile-nav-button input:checked ~ div {
transform: none;
}
@media (min-width: 1024px) {
#mobile-nav-button input ~ div {
transform: none;
}
}
#menu {
transform-origin: 0% 0%;
transform: translate(-100%, 0);
transition: transform 0.5s cubic-bezier(0.77,0.2,0.05,1.0);
}
</style>
</body>
</html>

View File

@ -3,33 +3,35 @@
{{define "title"}}Documents{{end}}
{{define "header"}}
<a href="../documents">Documents</a>
<a href="{{ .RelBase }}./documents">Documents</a>
{{end}}
{{define "content"}}
<div class="h-full w-full relative">
<!-- Document Info -->
<div class="h-full w-full overflow-scroll bg-white shadow-lg dark:bg-gray-700 rounded dark:text-white p-4">
<div class="flex flex-col gap-2 float-left w-40 md:w-60 lg:w-80 mr-4 mb-2 relative">
<label class="z-20 cursor-pointer" for="edit-cover-button">
<img class="rounded object-fill h-full" src="../documents/{{.Data.ID}}/cover"></img>
<div class="flex flex-col gap-2 float-left w-44 md:w-60 lg:w-80 mr-4 mb-2 relative">
<label class="z-10 cursor-pointer" for="edit-cover-button">
<img class="rounded object-fill w-full" src="{{ .RelBase }}./documents/{{.Data.ID}}/cover"></img>
</label>
<div class="flex flex-wrap-reverse justify-between z-40 gap-2 relative">
<div class="mr-2 min-w-[50%]">
<div class="flex flex-wrap-reverse justify-between z-20 gap-2 relative">
<div class="min-w-[50%] md:mr-2">
<div class="flex gap-1 text-sm">
<p class="text-gray-400">ISBN 10:</p>
<p class="text-gray-500">ISBN-10:</p>
<p class="font-medium">
{{ or .Data.Isbn10 "N/A" }}
</p>
</div>
<div class="flex gap-1 text-sm">
<p class="text-gray-400">ISBN 13:</p>
<p class="text-gray-500">ISBN-13:</p>
<p class="font-medium">
{{ or .Data.Isbn13 "N/A" }}
</p>
</div>
</div>
<div class="flex grow justify-between my-auto text-gray-500 dark:text-gray-400">
<div class="flex grow justify-between my-auto text-gray-500 dark:text-gray-500">
<input type="checkbox" id="edit-cover-button" class="hidden css-button"/>
<div class="absolute z-50 flex flex-col gap-2 top-0 left-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 flex flex-col gap-2 top-0 left-0 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600">
<form
method="POST"
enctype="multipart/form-data"
@ -38,10 +40,13 @@
>
<input
type="file"
id="cover"
name="cover"
id="cover_file"
name="cover_file"
>
<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">Upload Cover</button>
<button
class="font-medium px-2 py-1 text-white bg-gray-500 dark:bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
type="submit"
>Upload Cover</button>
</form>
<form
method="POST"
@ -49,7 +54,10 @@
class="flex flex-col gap-2 w-72 text-black dark:text-white text-sm"
>
<input type="checkbox" checked id="remove_cover" name="remove_cover" class="hidden" />
<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">Remove Cover</button>
<button
class="font-medium px-2 py-1 text-white bg-gray-500 dark:bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
type="submit"
>Remove Cover</button>
</form>
</div>
@ -72,13 +80,16 @@
</svg>
</label>
<input type="checkbox" id="delete-button" class="hidden css-button"/>
<div class="absolute z-50 bottom-7 left-5 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 bottom-7 left-5 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600">
<form
method="POST"
action="./{{ .Data.ID }}/delete"
class="text-black dark:text-white text-sm"
>
<button class="font-medium w-24 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>
<button
class="font-medium w-24 px-2 py-1 text-white bg-gray-500 dark:bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
type="submit"
>Delete</button>
</form>
</div>
</div>
@ -113,7 +124,7 @@
</svg>
</label>
<input type="checkbox" id="edit-button" class="hidden css-button"/>
<div class="absolute z-50 bottom-7 left-5 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 bottom-7 left-5 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600">
<form
method="POST"
action="./{{ .Data.ID }}/identify"
@ -124,23 +135,29 @@
id="title"
name="title"
placeholder="Title"
class="p-2 bg-gray-400 text-black dark:bg-gray-700 dark:text-white"
value="{{ or .Data.Title nil }}"
class="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white"
>
<input
type="text"
id="author"
name="author"
placeholder="Author"
class="p-2 bg-gray-400 text-black dark:bg-gray-700 dark:text-white"
value="{{ or .Data.Author nil }}"
class="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white"
>
<input
type="text"
id="isbn"
name="isbn"
placeholder="ISBN 10 / ISBN 13"
class="p-2 bg-gray-400 text-black dark:bg-gray-700 dark:text-white"
value="{{ or .Data.Isbn13 (or .Data.Isbn10 nil) }}"
class="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white"
>
<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">Identify</button>
<button
class="font-medium px-2 py-1 text-white bg-gray-500 dark:bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
type="submit"
>Identify</button>
</form>
</div>
</div>
@ -180,9 +197,9 @@
</div>
</div>
</div>
<div class="grid grid-cols-2 justify-between gap-4 pb-4">
<div class="grid sm:grid-cols-2 justify-between gap-4 pb-4">
<div class="relative">
<div class="text-gray-400 inline-flex gap-2 relative">
<div class="text-gray-500 inline-flex gap-2 relative">
<p>Title</p>
<label class="my-auto" for="edit-title-button">
<svg
@ -205,7 +222,7 @@
</svg>
</label>
<input type="checkbox" id="edit-title-button" class="hidden css-button"/>
<div class="absolute z-50 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">
<form
method="POST"
action="./{{ .Data.ID }}/edit"
@ -216,9 +233,12 @@
id="title"
name="title"
value="{{ or .Data.Title "N/A" }}"
class="p-2 bg-gray-400 text-black dark:bg-gray-700 dark:text-white"
class="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white"
>
<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">Save</button>
<button
class="font-medium px-2 py-1 text-white bg-gray-500 dark:bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
type="submit"
>Save</button>
</form>
</div>
</div>
@ -227,7 +247,7 @@
</p>
</div>
<div class="relative">
<div class="text-gray-400 inline-flex gap-2 relative">
<div class="text-gray-500 inline-flex gap-2 relative">
<p>Author</p>
<label class="my-auto" for="edit-author-button">
<svg
@ -251,7 +271,7 @@
</label>
<input type="checkbox" id="edit-author-button" class="hidden css-button"/>
<div class="absolute z-50 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">
<form
method="POST"
action="./{{ .Data.ID }}/edit"
@ -262,9 +282,12 @@
id="author"
name="author"
value="{{ or .Data.Author "N/A" }}"
class="p-2 bg-gray-400 text-black dark:bg-gray-700 dark:text-white"
class="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white"
>
<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">Save</button>
<button
class="font-medium px-2 py-1 text-white bg-gray-500 dark:bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
type="submit"
>Save</button>
</form>
</div>
</div>
@ -273,26 +296,26 @@
</p>
</div>
<div>
<p class="text-gray-400">Time Read</p>
<p class="text-gray-500">Time Read</p>
<p class="font-medium text-lg">
{{ .Data.TotalTimeMinutes }} Minutes
</p>
</div>
<div>
<p class="text-gray-400">Progress</p>
<p class="text-gray-500">Progress</p>
<p class="font-medium text-lg">
{{ .Data.CurrentPage }} / {{ .Data.TotalPages }} ({{ .Data.Percentage }}%)
</p>
</div>
<!--
<div>
<p class="text-gray-400">ISBN 10</p>
<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-400">ISBN 13</p>
<p class="text-gray-500">ISBN 13</p>
<p class="font-medium text-lg">
{{ or .Data.Isbn13 "N/A" }}
</p>
@ -301,7 +324,7 @@
</div>
<div class="relative">
<div class="text-gray-400 inline-flex gap-2 relative">
<div class="text-gray-500 inline-flex gap-2 relative">
<p>Description</p>
<label class="my-auto" for="edit-description-button">
<svg
@ -328,9 +351,9 @@
<div class="relative font-medium text-justify hyphens-auto">
<input type="checkbox" id="edit-description-button" class="hidden css-button"/>
<div
class="absolute h-full w-full min-h-[10em] z-50 top-1 right-0 gap-4 flex transition-all duration-200"
class="absolute h-full w-full min-h-[10em] z-30 top-1 right-0 gap-4 flex transition-all duration-200"
>
<img class="hidden md:block invisible rounded w-40 md:w-60 lg:w-80 object-fill" src="../documents/{{.Data.ID}}/cover"></img>
<img class="hidden md:block invisible rounded w-44 md:w-60 lg:w-80 object-fill" src="{{ .RelBase }}./documents/{{.Data.ID}}/cover"></img>
<form
method="POST"
action="./{{ .Data.ID }}/edit"
@ -340,14 +363,122 @@
type="text"
id="description"
name="description"
class="h-full w-full p-2 bg-gray-400 text-black dark:bg-gray-700 dark:text-white"
class="h-full w-full p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white"
>{{ or .Data.Description "N/A" }}</textarea>
<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">Save</button>
<button
class="font-medium px-2 py-1 text-white bg-gray-500 dark:bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
type="submit"
>Save</button>
</form>
</div>
<p>{{ or .Data.Description "N/A" }}</p>
</div>
</div>
{{ if .MetadataError }}
<div class="absolute top-0 left-0 w-full h-full z-50">
<div class="fixed top-0 left-0 bg-black opacity-50 w-screen h-screen"></div>
<div class="relative flex flex-col gap-4 p-4 max-h-[95%] bg-white dark:bg-gray-800 overflow-scroll -translate-x-2/4 -translate-y-2/4 top-1/2 left-1/2 w-5/6 overflow-hidden shadow rounded">
<div class="text-center">
<h3 class="text-lg font-bold leading-6 dark:text-gray-300">No Metadata Results Found</h3>
</div>
<a href="{{ .RelBase }}./documents/{{ .Data.ID }}"
class="w-full text-center font-medium px-2 py-1 text-white bg-gray-500 dark:bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
type="submit"
>Back to Document</a>
</div>
</div>
{{ end }}
<!-- Metadata Info -->
{{ if .Metadata }}
<div class="absolute top-0 left-0 w-full h-full z-50">
<div class="fixed top-0 left-0 bg-black opacity-50 w-screen h-screen"></div>
<div class="relative max-h-[95%] bg-white dark:bg-gray-800 overflow-scroll -translate-x-2/4 -translate-y-2/4 top-1/2 left-1/2 w-5/6 overflow-hidden shadow rounded">
<div class="py-5 text-center">
<h3 class="text-lg font-bold leading-6 dark:text-gray-300">Metadata Results</h3>
</div>
<form
id="metadata-save"
method="POST"
action="{{ .RelBase }}./documents/{{ .Data.ID }}/edit"
class="text-black dark:text-white border-b dark:border-black"
>
<dl>
<div class="p-3 bg-gray-100 dark:bg-gray-900 grid grid-cols-3 gap-4 sm:px-6">
<dt class="my-auto font-medium text-gray-500">
Cover
</dt>
<dd class="mt-1 text-sm sm:mt-0 sm:col-span-2">
<img class="rounded object-fill h-32" src="https://books.google.com/books/content/images/frontcover/{{ .Metadata.GBID }}?fife=w480-h690"></img>
</dd>
</div>
<div class="p-3 bg-white dark:bg-gray-800 grid grid-cols-3 gap-4 sm:px-6">
<dt class="my-auto font-medium text-gray-500">
Title
</dt>
<dd class="mt-1 text-sm sm:mt-0 sm:col-span-2">
{{ or .Metadata.Title "N/A" }}
</dd>
</div>
<div class="p-3 bg-gray-100 dark:bg-gray-900 grid grid-cols-3 gap-4 sm:px-6">
<dt class="my-auto font-medium text-gray-500">
Author
</dt>
<dd class="mt-1 text-sm sm:mt-0 sm:col-span-2">
{{ or .Metadata.Author "N/A" }}
</dd>
</div>
<div class="p-3 bg-white dark:bg-gray-800 grid grid-cols-3 gap-4 sm:px-6">
<dt class="my-auto font-medium text-gray-500">
ISBN 10
</dt>
<dd class="mt-1 text-sm sm:mt-0 sm:col-span-2">
{{ or .Metadata.ISBN10 "N/A" }}
</dd>
</div>
<div class="p-3 bg-gray-100 dark:bg-gray-900 grid grid-cols-3 gap-4 sm:px-6">
<dt class="my-auto font-medium text-gray-500">
ISBN 13
</dt>
<dd class="mt-1 text-sm sm:mt-0 sm:col-span-2">
{{ or .Metadata.ISBN13 "N/A" }}
</dd>
</div>
<div class="p-3 bg-white dark:bg-gray-800 sm:grid sm:grid-cols-3 sm:gap-4 px-6">
<dt class="my-auto font-medium text-gray-500">
Description
</dt>
<dd class="max-h-[10em] overflow-scroll mt-1 sm:mt-0 sm:col-span-2">
{{ or .Metadata.Description "N/A" }}
</dd>
</div>
</dl>
<div class="hidden">
<input type="text" id="title" name="title" value="{{ .Metadata.Title }}">
<input type="text" id="author" name="author" value="{{ .Metadata.Author }}">
<input type="text" id="description" name="description" value="{{ .Metadata.Description }}">
<input type="text" id="isbn_10" name="isbn_10" value="{{ .Metadata.ISBN10 }}">
<input type="text" id="isbn_13" name="isbn_13" value="{{ .Metadata.ISBN13 }}">
<input type="text" id="cover_gbid" name="cover_gbid" value="{{ .Metadata.GBID }}">
</div>
</form>
<div class="flex justify-end gap-4 m-4">
<a href="{{ .RelBase }}./documents/{{ .Data.ID }}"
class="w-24 text-center font-medium px-2 py-1 text-white bg-gray-500 dark:bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
type="submit"
>Cancel</a>
<button
form="metadata-save"
class="w-24 font-medium px-2 py-1 text-white bg-gray-500 dark:bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
type="submit"
>Save</button>
</div>
</div>
</div>
{{ end }}
</div>
<style>
.css-button:checked + div {
visibility: visible;