[add] docker instructions, [add] metadata gathering, [add] screenshots
38
Dockerfile
@ -1,19 +1,35 @@
|
|||||||
FROM alpine:edge AS build
|
# Certificate Store
|
||||||
RUN apk add --no-cache --update go gcc g++
|
FROM alpine as certs
|
||||||
WORKDIR /app
|
RUN apk update && apk add ca-certificates
|
||||||
COPY . /app
|
|
||||||
|
|
||||||
# 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 mkdir -p /opt/bookmanager
|
||||||
RUN cp -a ./templates /opt/bookmanager/templates
|
|
||||||
RUN cp -a ./assets /opt/bookmanager/assets
|
|
||||||
|
|
||||||
# Download Dependencies & Compile
|
# Cache Dependencies & Compile
|
||||||
RUN go mod download
|
ARG TARGETOS
|
||||||
RUN CGO_ENABLED=1 CGO_CFLAGS="-D_LARGEFILE64_SOURCE" go build -o /opt/bookmanager/server
|
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
|
# 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
|
COPY --from=build /opt/bookmanager /opt/bookmanager
|
||||||
WORKDIR /opt/bookmanager
|
WORKDIR /opt/bookmanager
|
||||||
EXPOSE 8585
|
EXPOSE 8585
|
||||||
|
8
Makefile
@ -14,9 +14,13 @@ docker_build_local:
|
|||||||
docker_build_release_beta:
|
docker_build_release_beta:
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--platform linux/amd64,linux/arm64 \
|
--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_build_release_latest:
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--platform linux/amd64,linux/arm64 \
|
--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 .
|
||||||
|
51
README.md
@ -1,14 +1,20 @@
|
|||||||
# Book Manager
|
# Book Manager
|
||||||
|
|
||||||
<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/BookManager/raw/branch/master/screenshots/pwa/login.png">
|
||||||
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web_login.png" width="30%">
|
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/login.png" width="18%">
|
||||||
</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/BookManager/raw/branch/master/screenshots/pwa/home.png">
|
||||||
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web_home.png" width="30%">
|
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/home.png" width="18%">
|
||||||
</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/BookManager/raw/branch/master/screenshots/pwa/documents.png">
|
||||||
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/web_documents.png" width="30%">
|
<img src="https://gitea.va.reichard.io/evan/BookManager/raw/branch/master/screenshots/pwa/documents.png" width="18%">
|
||||||
|
</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="18%">
|
||||||
|
</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="18%">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -24,11 +30,30 @@ In additional to the compatible KOSync API's, we add:
|
|||||||
|
|
||||||
- Additional APIs to automatically upload reading statistics
|
- Additional APIs to automatically upload reading statistics
|
||||||
- Automatically upload documents to the server (can download in the "Documents" view)
|
- 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.
|
- No JavaScript! All information is rendered server side.
|
||||||
|
|
||||||
# Server
|
# 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
|
## Configuration
|
||||||
|
|
||||||
| Environment Variable | Default Value | Description |
|
| Environment Variable | Default Value | Description |
|
||||||
@ -50,22 +75,22 @@ See documentation in the `client` subfolder: [SyncNinja](https://gitea.va.reicha
|
|||||||
|
|
||||||
SQLC Generation (v1.21.0):
|
SQLC Generation (v1.21.0):
|
||||||
|
|
||||||
```
|
```bash
|
||||||
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
|
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
|
||||||
~/go/bin/sqlc generate
|
~/go/bin/sqlc generate
|
||||||
```
|
```
|
||||||
|
|
||||||
Run Development:
|
Run Development:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
CONFIG_PATH=./data DATA_PATH=./data go run cmd/main.go serve
|
CONFIG_PATH=./data DATA_PATH=./data go run main.go serve
|
||||||
```
|
```
|
||||||
|
|
||||||
# Building
|
# Building
|
||||||
|
|
||||||
The `Dockerfile` and `Makefile` contain the build information:
|
The `Dockerfile` and `Makefile` contain the build information:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
# Build Local Docker Image
|
# Build Local Docker Image
|
||||||
make docker_build_local
|
make docker_build_local
|
||||||
|
|
||||||
@ -75,12 +100,12 @@ make docker_build_release_latest
|
|||||||
|
|
||||||
If manually building, you must enable CGO:
|
If manually building, you must enable CGO:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
# Download Dependencies
|
# Download Dependencies
|
||||||
go mod download
|
go mod download
|
||||||
|
|
||||||
# Compile (Binary `./bookmanager`)
|
# 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
|
## Notes
|
||||||
|
@ -21,8 +21,11 @@ type requestDocumentEdit struct {
|
|||||||
Title *string `form:"title"`
|
Title *string `form:"title"`
|
||||||
Author *string `form:"author"`
|
Author *string `form:"author"`
|
||||||
Description *string `form:"description"`
|
Description *string `form:"description"`
|
||||||
|
ISBN10 *string `form:"isbn_10"`
|
||||||
|
ISBN13 *string `form:"isbn_13"`
|
||||||
RemoveCover *string `form:"remove_cover"`
|
RemoveCover *string `form:"remove_cover"`
|
||||||
CoverFile *multipart.FileHeader `form:"cover"`
|
CoverGBID *string `form:"cover_gbid"`
|
||||||
|
CoverFile *multipart.FileHeader `form:"cover_file"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type requestDocumentIdentify struct {
|
type requestDocumentIdentify struct {
|
||||||
@ -101,6 +104,7 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
templateVars["RelBase"] = "../"
|
||||||
templateVars["Data"] = document
|
templateVars["Data"] = document
|
||||||
} else if routeName == "activity" {
|
} else if routeName == "activity" {
|
||||||
activityFilter := database.GetActivityParams{
|
activityFilter := database.GetActivityParams{
|
||||||
@ -131,7 +135,7 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("[createAppResourcesRoute] GetUserWindowStreaks DB Error:", err)
|
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()
|
start_time = time.Now()
|
||||||
|
|
||||||
daily_streak, err := api.DB.Queries.GetUserWindowStreaks(api.DB.Ctx, database.GetUserWindowStreaksParams{
|
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 {
|
if err != nil {
|
||||||
log.Warn("[createAppResourcesRoute] GetUserWindowStreaks DB Error:", err)
|
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()
|
start_time = time.Now()
|
||||||
database_info, _ := api.DB.Queries.GetDatabaseInfo(api.DB.Ctx, rUser.(string))
|
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()
|
start_time = time.Now()
|
||||||
read_graph_data, _ := api.DB.Queries.GetDailyReadStats(api.DB.Ctx, rUser.(string))
|
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{
|
templateVars["Data"] = gin.H{
|
||||||
"DailyStreak": daily_streak,
|
"DailyStreak": daily_streak,
|
||||||
@ -218,7 +222,7 @@ func (api *API) getDocumentCover(c *gin.Context) {
|
|||||||
firstResult := metadataResults[0]
|
firstResult := metadataResults[0]
|
||||||
|
|
||||||
// Save Cover
|
// Save Cover
|
||||||
fileName, err := metadata.SaveCover(*firstResult.GBID, coverDir, document.ID)
|
fileName, err := metadata.SaveCover(*firstResult.GBID, coverDir, document.ID, false)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
coverFile = *fileName
|
coverFile = *fileName
|
||||||
}
|
}
|
||||||
@ -275,8 +279,11 @@ func (api *API) editDocument(c *gin.Context) {
|
|||||||
if rDocEdit.Author == nil &&
|
if rDocEdit.Author == nil &&
|
||||||
rDocEdit.Title == nil &&
|
rDocEdit.Title == nil &&
|
||||||
rDocEdit.Description == nil &&
|
rDocEdit.Description == nil &&
|
||||||
rDocEdit.CoverFile == nil &&
|
rDocEdit.ISBN10 == nil &&
|
||||||
rDocEdit.RemoveCover == nil {
|
rDocEdit.ISBN13 == nil &&
|
||||||
|
rDocEdit.RemoveCover == nil &&
|
||||||
|
rDocEdit.CoverGBID == nil &&
|
||||||
|
rDocEdit.CoverFile == nil {
|
||||||
log.Error("[createAppResourcesRoute] Missing Form Values")
|
log.Error("[createAppResourcesRoute] Missing Form Values")
|
||||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||||
return
|
return
|
||||||
@ -288,7 +295,6 @@ func (api *API) editDocument(c *gin.Context) {
|
|||||||
s := "UNKNOWN"
|
s := "UNKNOWN"
|
||||||
coverFileName = &s
|
coverFileName = &s
|
||||||
} else if rDocEdit.CoverFile != nil {
|
} else if rDocEdit.CoverFile != nil {
|
||||||
|
|
||||||
// Validate Type & Derive Extension on MIME
|
// Validate Type & Derive Extension on MIME
|
||||||
uploadedFile, err := rDocEdit.CoverFile.Open()
|
uploadedFile, err := rDocEdit.CoverFile.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -325,6 +331,14 @@ func (api *API) editDocument(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
coverFileName = &fileName
|
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
|
// Update Document
|
||||||
@ -333,6 +347,8 @@ func (api *API) editDocument(c *gin.Context) {
|
|||||||
Title: api.sanitizeInput(rDocEdit.Title),
|
Title: api.sanitizeInput(rDocEdit.Title),
|
||||||
Author: api.sanitizeInput(rDocEdit.Author),
|
Author: api.sanitizeInput(rDocEdit.Author),
|
||||||
Description: api.sanitizeInput(rDocEdit.Description),
|
Description: api.sanitizeInput(rDocEdit.Description),
|
||||||
|
Isbn10: api.sanitizeInput(rDocEdit.ISBN10),
|
||||||
|
Isbn13: api.sanitizeInput(rDocEdit.ISBN13),
|
||||||
Coverfile: coverFileName,
|
Coverfile: coverFileName,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Error("[createAppResourcesRoute] UpsertDocument DB Error:", err)
|
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) {
|
func (api *API) identifyDocument(c *gin.Context) {
|
||||||
|
rUser, _ := c.Get("AuthorizedUser")
|
||||||
|
|
||||||
var rDocID requestDocumentID
|
var rDocID requestDocumentID
|
||||||
if err := c.ShouldBindUri(&rDocID); err != nil {
|
if err := c.ShouldBindUri(&rDocID); err != nil {
|
||||||
log.Error("[identifyDocument] Invalid URI Bind")
|
log.Error("[identifyDocument] Invalid URI Bind")
|
||||||
@ -399,36 +417,52 @@ func (api *API) identifyDocument(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Template Variables
|
||||||
|
templateVars := gin.H{
|
||||||
|
"RelBase": "../../",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Metadata
|
||||||
metadataResults, err := metadata.GetMetadata(metadata.MetadataInfo{
|
metadataResults, err := metadata.GetMetadata(metadata.MetadataInfo{
|
||||||
Title: rDocIdentify.Title,
|
Title: rDocIdentify.Title,
|
||||||
Author: rDocIdentify.Author,
|
Author: rDocIdentify.Author,
|
||||||
ISBN10: rDocIdentify.ISBN,
|
ISBN10: rDocIdentify.ISBN,
|
||||||
ISBN13: rDocIdentify.ISBN,
|
ISBN13: rDocIdentify.ISBN,
|
||||||
})
|
})
|
||||||
if err != nil || len(metadataResults) == 0 {
|
if err == nil && len(metadataResults) > 0 {
|
||||||
log.Error("[identifyDocument] Metadata Error")
|
firstResult := metadataResults[0]
|
||||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Metadata Error"})
|
|
||||||
|
// 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
templateVars["Data"] = document
|
||||||
firstResult := metadataResults[0]
|
|
||||||
|
|
||||||
if firstResult.Title != nil {
|
c.HTML(http.StatusOK, "document", templateVars)
|
||||||
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, "/")
|
|
||||||
}
|
}
|
||||||
|
@ -85,6 +85,7 @@ func (api *API) authFormLogin(c *gin.Context) {
|
|||||||
|
|
||||||
if username == "" || rawPassword == "" {
|
if username == "" || rawPassword == "" {
|
||||||
c.HTML(http.StatusUnauthorized, "login", gin.H{
|
c.HTML(http.StatusUnauthorized, "login", gin.H{
|
||||||
|
"RegistrationEnabled": api.Config.RegistrationEnabled,
|
||||||
"Error": "Invalid Credentials",
|
"Error": "Invalid Credentials",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@ -93,6 +94,7 @@ func (api *API) authFormLogin(c *gin.Context) {
|
|||||||
|
|
||||||
if authorized := api.authorizeCredentials(username, password); authorized != true {
|
if authorized := api.authorizeCredentials(username, password); authorized != true {
|
||||||
c.HTML(http.StatusUnauthorized, "login", gin.H{
|
c.HTML(http.StatusUnauthorized, "login", gin.H{
|
||||||
|
"RegistrationEnabled": api.Config.RegistrationEnabled,
|
||||||
"Error": "Invalid Credentials",
|
"Error": "Invalid Credentials",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
@ -380,8 +380,12 @@ current_streak AS (
|
|||||||
end_date AS current_streak_end_date
|
end_date AS current_streak_end_date
|
||||||
FROM streaks
|
FROM streaks
|
||||||
WHERE CASE
|
WHERE CASE
|
||||||
WHEN ?2 = "WEEK" THEN DATE('now', time_offset, 'weekday 0', '-7 day') = current_streak_end_date
|
WHEN ?2 = "WEEK" THEN
|
||||||
WHEN ?2 = "DAY" THEN DATE('now', time_offset, '-1 day') = current_streak_end_date OR DATE('now', time_offset) = current_streak_end_date
|
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
|
END
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
)
|
)
|
||||||
|
@ -1006,8 +1006,12 @@ current_streak AS (
|
|||||||
end_date AS current_streak_end_date
|
end_date AS current_streak_end_date
|
||||||
FROM streaks
|
FROM streaks
|
||||||
WHERE CASE
|
WHERE CASE
|
||||||
WHEN ?2 = "WEEK" THEN DATE('now', time_offset, 'weekday 0', '-7 day') = current_streak_end_date
|
WHEN ?2 = "WEEK" THEN
|
||||||
WHEN ?2 = "DAY" THEN DATE('now', time_offset, '-1 day') = current_streak_end_date OR DATE('now', time_offset) = current_streak_end_date
|
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
|
END
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
)
|
)
|
||||||
|
@ -129,7 +129,7 @@ func GetMetadata(metadataSearch MetadataInfo) ([]MetadataInfo, error) {
|
|||||||
return allMetadata, nil
|
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
|
// Google Books -> JPG
|
||||||
coverFile := "." + filepath.Clean(fmt.Sprintf("/%s.jpg", documentID))
|
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
|
// Validate File Doesn't Exists
|
||||||
_, err := os.Stat(coverFilePath)
|
_, err := os.Stat(coverFilePath)
|
||||||
if err == nil {
|
if err == nil && overwrite == false {
|
||||||
log.Warn("[SaveCover] File Alreads Exists")
|
log.Warn("[SaveCover] File Alreads Exists")
|
||||||
return &coverFile, nil
|
return &coverFile, nil
|
||||||
}
|
}
|
||||||
|
11
screenshots/README.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Screenshots
|
||||||
|
|
||||||
|
## Process Images
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Resize
|
||||||
|
sips -Z 1500 *.png
|
||||||
|
|
||||||
|
# Crop Top & Bottom
|
||||||
|
sips --cropOffset 85 1 -c 1385 693 *.png
|
||||||
|
```
|
BIN
screenshots/pwa/activity.png
Normal file
After Width: | Height: | Size: 229 KiB |
BIN
screenshots/pwa/document.png
Normal file
After Width: | Height: | Size: 586 KiB |
BIN
screenshots/pwa/documents.png
Normal file
After Width: | Height: | Size: 490 KiB |
BIN
screenshots/pwa/home.png
Normal file
After Width: | Height: | Size: 144 KiB |
BIN
screenshots/pwa/login.png
Normal file
After Width: | Height: | Size: 60 KiB |
BIN
screenshots/pwa/metadata.png
Normal file
After Width: | Height: | Size: 335 KiB |
BIN
screenshots/pwa/navigation.png
Normal file
After Width: | Height: | Size: 78 KiB |
BIN
screenshots/web/activity.png
Normal file
After Width: | Height: | Size: 245 KiB |
BIN
screenshots/web/document.png
Normal file
After Width: | Height: | Size: 639 KiB |
BIN
screenshots/web/documents.png
Normal file
After Width: | Height: | Size: 791 KiB |
BIN
screenshots/web/home.png
Normal file
After Width: | Height: | Size: 131 KiB |
BIN
screenshots/web/login.png
Normal file
After Width: | Height: | Size: 790 KiB |
BIN
screenshots/web/metadata.png
Normal file
After Width: | Height: | Size: 357 KiB |
Before Width: | Height: | Size: 1.8 MiB |
Before Width: | Height: | Size: 362 KiB |
Before Width: | Height: | Size: 2.8 MiB |
@ -1,9 +1,10 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<link rel="manifest" href="./manifest.json" />
|
<link rel="manifest" href="{{ .RelBase }}./manifest.json" />
|
||||||
<meta charset="utf-8" />
|
<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>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<title>Book Manager - {{block "title" .}}{{end}}</title>
|
<title>Book Manager - {{block "title" .}}{{end}}</title>
|
||||||
</head>
|
</head>
|
||||||
@ -11,33 +12,18 @@
|
|||||||
<main
|
<main
|
||||||
class="relative h-screen overflow-hidden bg-gray-100 dark:bg-gray-800"
|
class="relative h-screen overflow-hidden bg-gray-100 dark:bg-gray-800"
|
||||||
>
|
>
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex items-center justify-between w-full h-16">
|
||||||
<input type="checkbox" id="mobile-nav-button" class="hidden"/>
|
<div id="mobile-nav-button" class="flex flex-col z-40 relative ml-6">
|
||||||
<div class="fixed -left-64 duration-500 transition-all w-56 z-50 h-screen shadow-lg lg:left-0 lg:block lg:relative">
|
<input type="checkbox" class="absolute lg:hidden z-50 -top-2 w-7 h-7 flex cursor-pointer opacity-0 w-12 h-12" />
|
||||||
<div class="h-full bg-white dark:bg-gray-700">
|
<span class="lg:hidden w-7 h-0.5 z-40 mt-0.5 bg-white"></span>
|
||||||
<div class="flex items-center justify-center gap-4 h-16">
|
<span class="lg:hidden w-7 h-0.5 z-40 mt-1 bg-white"></span>
|
||||||
<label
|
<span class="lg:hidden w-7 h-0.5 z-40 mt-1 bg-white"></span>
|
||||||
id="mobile-nav-close-button"
|
|
||||||
for="mobile-nav-button"
|
<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">
|
||||||
class="flex block items-center p-2 text-gray-500 bg-white rounded-full shadow text-md cursor-pointer lg:hidden"
|
<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>
|
||||||
<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>
|
</div>
|
||||||
<nav class="mt-6">
|
<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="/"
|
||||||
@ -114,30 +100,8 @@
|
|||||||
<span class="mx-4 text-sm font-normal"> Graphs </span>
|
<span class="mx-4 text-sm font-normal"> Graphs </span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<h1 class="text-xl font-bold dark:text-white px-6">{{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"
|
||||||
@ -159,7 +123,7 @@
|
|||||||
<input type="checkbox" id="user-dropdown-button" class="hidden"/>
|
<input type="checkbox" id="user-dropdown-button" class="hidden"/>
|
||||||
<div
|
<div
|
||||||
id="user-dropdown"
|
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
|
<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"
|
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>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</div>
|
||||||
<div class="h-screen px-4 pb-24 overflow-auto md:px-6">
|
|
||||||
|
<div class="h-screen px-4 pb-24 overflow-auto md:px-6 lg:ml-48">
|
||||||
{{block "content" .}}{{end}}
|
{{block "content" .}}{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Custom Animation CSS -->
|
<!-- Custom Animation CSS -->
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
/* ----------------------------- */
|
|
||||||
/* ------ Navigation Slide ----- */
|
|
||||||
/* ----------------------------- */
|
|
||||||
#mobile-nav-button:checked + div {
|
|
||||||
left: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ----------------------------- */
|
/* ----------------------------- */
|
||||||
/* ------- User Dropdown ------- */
|
/* ------- User Dropdown ------- */
|
||||||
/* ----------------------------- */
|
/* ----------------------------- */
|
||||||
@ -232,6 +188,55 @@
|
|||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
opacity: 0;
|
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>
|
</style>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -3,33 +3,35 @@
|
|||||||
{{define "title"}}Documents{{end}}
|
{{define "title"}}Documents{{end}}
|
||||||
|
|
||||||
{{define "header"}}
|
{{define "header"}}
|
||||||
<a href="../documents">Documents</a>
|
<a href="{{ .RelBase }}./documents">Documents</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "content"}}
|
{{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="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">
|
<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-20 cursor-pointer" for="edit-cover-button">
|
<label class="z-10 cursor-pointer" for="edit-cover-button">
|
||||||
<img class="rounded object-fill h-full" src="../documents/{{.Data.ID}}/cover"></img>
|
<img class="rounded object-fill w-full" src="{{ .RelBase }}./documents/{{.Data.ID}}/cover"></img>
|
||||||
</label>
|
</label>
|
||||||
<div class="flex flex-wrap-reverse justify-between z-40 gap-2 relative">
|
<div class="flex flex-wrap-reverse justify-between z-20 gap-2 relative">
|
||||||
<div class="mr-2 min-w-[50%]">
|
<div class="min-w-[50%] md:mr-2">
|
||||||
<div class="flex gap-1 text-sm">
|
<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">
|
<p class="font-medium">
|
||||||
{{ or .Data.Isbn10 "N/A" }}
|
{{ or .Data.Isbn10 "N/A" }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-1 text-sm">
|
<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">
|
<p class="font-medium">
|
||||||
{{ or .Data.Isbn13 "N/A" }}
|
{{ or .Data.Isbn13 "N/A" }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</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"/>
|
<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
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
enctype="multipart/form-data"
|
enctype="multipart/form-data"
|
||||||
@ -38,10 +40,13 @@
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
id="cover"
|
id="cover_file"
|
||||||
name="cover"
|
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>
|
||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
@ -49,7 +54,10 @@
|
|||||||
class="flex flex-col gap-2 w-72 text-black dark:text-white text-sm"
|
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" />
|
<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>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -72,13 +80,16 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
<input type="checkbox" id="delete-button" class="hidden css-button"/>
|
<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
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="./{{ .Data.ID }}/delete"
|
action="./{{ .Data.ID }}/delete"
|
||||||
class="text-black dark:text-white text-sm"
|
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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -113,7 +124,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
<input type="checkbox" id="edit-button" class="hidden css-button"/>
|
<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
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="./{{ .Data.ID }}/identify"
|
action="./{{ .Data.ID }}/identify"
|
||||||
@ -124,23 +135,29 @@
|
|||||||
id="title"
|
id="title"
|
||||||
name="title"
|
name="title"
|
||||||
placeholder="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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="author"
|
id="author"
|
||||||
name="author"
|
name="author"
|
||||||
placeholder="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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="isbn"
|
id="isbn"
|
||||||
name="isbn"
|
name="isbn"
|
||||||
placeholder="ISBN 10 / ISBN 13"
|
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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -180,9 +197,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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="relative">
|
||||||
<div class="text-gray-400 inline-flex gap-2 relative">
|
<div class="text-gray-500 inline-flex gap-2 relative">
|
||||||
<p>Title</p>
|
<p>Title</p>
|
||||||
<label class="my-auto" for="edit-title-button">
|
<label class="my-auto" for="edit-title-button">
|
||||||
<svg
|
<svg
|
||||||
@ -205,7 +222,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
<input type="checkbox" id="edit-title-button" class="hidden css-button"/>
|
<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
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="./{{ .Data.ID }}/edit"
|
action="./{{ .Data.ID }}/edit"
|
||||||
@ -216,9 +233,12 @@
|
|||||||
id="title"
|
id="title"
|
||||||
name="title"
|
name="title"
|
||||||
value="{{ or .Data.Title "N/A" }}"
|
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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -227,7 +247,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative">
|
<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>
|
<p>Author</p>
|
||||||
<label class="my-auto" for="edit-author-button">
|
<label class="my-auto" for="edit-author-button">
|
||||||
<svg
|
<svg
|
||||||
@ -251,7 +271,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<input type="checkbox" id="edit-author-button" class="hidden css-button"/>
|
<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
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="./{{ .Data.ID }}/edit"
|
action="./{{ .Data.ID }}/edit"
|
||||||
@ -262,9 +282,12 @@
|
|||||||
id="author"
|
id="author"
|
||||||
name="author"
|
name="author"
|
||||||
value="{{ or .Data.Author "N/A" }}"
|
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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -273,26 +296,26 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-gray-400">Time Read</p>
|
<p class="text-gray-500">Time Read</p>
|
||||||
<p class="font-medium text-lg">
|
<p class="font-medium text-lg">
|
||||||
{{ .Data.TotalTimeMinutes }} Minutes
|
{{ .Data.TotalTimeMinutes }} Minutes
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-gray-400">Progress</p>
|
<p class="text-gray-500">Progress</p>
|
||||||
<p class="font-medium text-lg">
|
<p class="font-medium text-lg">
|
||||||
{{ .Data.CurrentPage }} / {{ .Data.TotalPages }} ({{ .Data.Percentage }}%)
|
{{ .Data.CurrentPage }} / {{ .Data.TotalPages }} ({{ .Data.Percentage }}%)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<!--
|
<!--
|
||||||
<div>
|
<div>
|
||||||
<p class="text-gray-400">ISBN 10</p>
|
<p class="text-gray-500">ISBN 10</p>
|
||||||
<p class="font-medium text-lg">
|
<p class="font-medium text-lg">
|
||||||
{{ or .Data.Isbn10 "N/A" }}
|
{{ or .Data.Isbn10 "N/A" }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-gray-400">ISBN 13</p>
|
<p class="text-gray-500">ISBN 13</p>
|
||||||
<p class="font-medium text-lg">
|
<p class="font-medium text-lg">
|
||||||
{{ or .Data.Isbn13 "N/A" }}
|
{{ or .Data.Isbn13 "N/A" }}
|
||||||
</p>
|
</p>
|
||||||
@ -301,7 +324,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative">
|
<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>
|
<p>Description</p>
|
||||||
<label class="my-auto" for="edit-description-button">
|
<label class="my-auto" for="edit-description-button">
|
||||||
<svg
|
<svg
|
||||||
@ -328,9 +351,9 @@
|
|||||||
<div class="relative font-medium text-justify hyphens-auto">
|
<div class="relative font-medium text-justify hyphens-auto">
|
||||||
<input type="checkbox" id="edit-description-button" class="hidden css-button"/>
|
<input type="checkbox" id="edit-description-button" class="hidden css-button"/>
|
||||||
<div
|
<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
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="./{{ .Data.ID }}/edit"
|
action="./{{ .Data.ID }}/edit"
|
||||||
@ -340,14 +363,122 @@
|
|||||||
type="text"
|
type="text"
|
||||||
id="description"
|
id="description"
|
||||||
name="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>
|
>{{ 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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<p>{{ or .Data.Description "N/A" }}</p>
|
<p>{{ or .Data.Description "N/A" }}</p>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<style>
|
||||||
.css-button:checked + div {
|
.css-button:checked + div {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
|