[add] editing, deletion, metadata recording

This commit is contained in:
Evan Reichard 2023-09-23 14:14:57 -04:00
parent 3150c89303
commit 9b2b7bb6f0
12 changed files with 920 additions and 163 deletions

View File

@ -25,7 +25,7 @@ type API struct {
func NewApi(db *database.DBManager, c *config.Config) *API { func NewApi(db *database.DBManager, c *config.Config) *API {
api := &API{ api := &API{
HTMLPolicy: bluemonday.StripTagsPolicy(), HTMLPolicy: bluemonday.StrictPolicy(),
Router: gin.Default(), Router: gin.Default(),
Config: c, Config: c,
DB: db, DB: db,
@ -97,6 +97,9 @@ func (api *API) registerWebAppRoutes() {
api.Router.GET("/activity", api.authWebAppMiddleware, api.createAppResourcesRoute("activity")) api.Router.GET("/activity", api.authWebAppMiddleware, api.createAppResourcesRoute("activity"))
api.Router.GET("/documents", api.authWebAppMiddleware, api.createAppResourcesRoute("documents")) api.Router.GET("/documents", api.authWebAppMiddleware, api.createAppResourcesRoute("documents"))
api.Router.GET("/documents/:document", api.authWebAppMiddleware, api.createAppResourcesRoute("document")) api.Router.GET("/documents/:document", api.authWebAppMiddleware, api.createAppResourcesRoute("document"))
api.Router.DELETE("/documents/:document", api.authWebAppMiddleware, api.deleteDocument)
api.Router.POST("/documents/:document/edit", api.authWebAppMiddleware, api.createAppResourcesRoute("document-edit"))
api.Router.POST("/documents/:document/delete", api.authWebAppMiddleware, api.deleteDocument)
api.Router.GET("/documents/:document/file", api.authWebAppMiddleware, api.downloadDocumentFile) api.Router.GET("/documents/:document/file", api.authWebAppMiddleware, api.downloadDocumentFile)
api.Router.GET("/documents/:document/cover", api.authWebAppMiddleware, api.getDocumentCover) api.Router.GET("/documents/:document/cover", api.authWebAppMiddleware, api.getDocumentCover)

View File

@ -2,17 +2,29 @@ package api
import ( import (
"fmt" "fmt"
"mime/multipart"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"time" "time"
"github.com/gabriel-vasile/mimetype"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/exp/slices"
"reichard.io/bbank/database" "reichard.io/bbank/database"
"reichard.io/bbank/metadata" "reichard.io/bbank/metadata"
) )
type requestDocumentEdit struct {
Title *string `form:"title"`
Author *string `form:"author"`
Description *string `form:"description"`
RemoveCover *string `form:"remove_cover"`
CoverFile *multipart.FileHeader `form:"cover"`
}
func baseResourceRoute(template string, args ...map[string]any) func(c *gin.Context) { func baseResourceRoute(template string, args ...map[string]any) func(c *gin.Context) {
variables := gin.H{"RouteName": template} variables := gin.H{"RouteName": template}
if len(args) > 0 { if len(args) > 0 {
@ -84,6 +96,92 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any
} }
templateVars["Data"] = document templateVars["Data"] = document
} else if routeName == "document-edit" {
var rDocID requestDocumentID
if err := c.ShouldBindUri(&rDocID); err != nil {
log.Error("[createAppResourcesRoute] Invalid URI Bind")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
return
}
var rDocEdit requestDocumentEdit
if err := c.ShouldBind(&rDocEdit); err != nil {
log.Error("[createAppResourcesRoute] Invalid Form Bind")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
return
}
// Validate Something Exists
if rDocEdit.Author == nil &&
rDocEdit.Title == nil &&
rDocEdit.Description == nil &&
rDocEdit.CoverFile == nil &&
rDocEdit.RemoveCover == nil {
log.Error("[createAppResourcesRoute] Missing Form Values")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
return
}
// Handle Cover
var coverFileName *string
if rDocEdit.RemoveCover != nil && *rDocEdit.RemoveCover == "on" {
s := "UNKNOWN"
coverFileName = &s
} else if rDocEdit.CoverFile != nil {
// Validate Type & Derive Extension on MIME
uploadedFile, err := rDocEdit.CoverFile.Open()
if err != nil {
log.Error("[createAppResourcesRoute] File Error")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
return
}
fileMime, err := mimetype.DetectReader(uploadedFile)
if err != nil {
log.Error("[createAppResourcesRoute] MIME Error")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
return
}
fileExtension := fileMime.Extension()
// Validate Extension
if !slices.Contains([]string{".jpg", ".png"}, fileExtension) {
log.Error("[uploadDocumentFile] Invalid FileType: ", fileExtension)
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Filetype"})
return
}
// Generate Storage Path
fileName := fmt.Sprintf("%s%s", rDocID.DocumentID, fileExtension)
safePath := filepath.Join(api.Config.DataPath, "covers", fileName)
// Save
err = c.SaveUploadedFile(rDocEdit.CoverFile, safePath)
if err != nil {
log.Error("[createAppResourcesRoute] File Error")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
return
}
coverFileName = &fileName
}
// Update Document
if _, err := api.DB.Queries.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{
ID: rDocID.DocumentID,
Title: api.sanitizeInput(rDocEdit.Title),
Author: api.sanitizeInput(rDocEdit.Author),
Description: api.sanitizeInput(rDocEdit.Description),
Coverfile: coverFileName,
}); err != nil {
log.Error("[createAppResourcesRoute] UpsertDocument DB Error:", err)
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
return
}
c.Redirect(http.StatusFound, "./")
return
} else if routeName == "activity" { } else if routeName == "activity" {
activityFilter := database.GetActivityParams{ activityFilter := database.GetActivityParams{
UserID: rUser.(string), UserID: rUser.(string),
@ -164,19 +262,19 @@ func (api *API) getDocumentCover(c *gin.Context) {
} }
// Handle Identified Document // Handle Identified Document
if document.Olid != nil { if document.Coverfile != nil {
if *document.Olid == "UNKNOWN" { if *document.Coverfile == "UNKNOWN" {
c.File("./assets/no-cover.jpg") c.File("./assets/no-cover.jpg")
return return
} }
// Derive Path // Derive Path
fileName := "." + filepath.Clean(fmt.Sprintf("/%s.jpg", *document.Olid)) safePath := filepath.Join(api.Config.DataPath, "covers", *document.Coverfile)
safePath := filepath.Join(api.Config.DataPath, "covers", fileName)
// Validate File Exists // Validate File Exists
_, err = os.Stat(safePath) _, err = os.Stat(safePath)
if err != nil { if err != nil {
log.Error("[getDocumentCover] File Should But Doesn't Exist:", err)
c.File("./assets/no-cover.jpg") c.File("./assets/no-cover.jpg")
return return
} }
@ -185,59 +283,126 @@ func (api *API) getDocumentCover(c *gin.Context) {
return return
} }
/* // --- Attempt Metadata ---
This is a bit convoluted because we want to ensure we set the OLID to
UNKNOWN if there are any errors. This will ideally prevent us from
hitting the OpenLibrary API multiple times in the future.
*/
var coverID string = "UNKNOWN" var coverDir string = filepath.Join(api.Config.DataPath, "covers")
var coverFilePath string var coverFile string = "UNKNOWN"
// Identify Documents & Save Covers // Identify Documents & Save Covers
bookMetadata := metadata.MetadataInfo{ metadataResults, err := metadata.GetMetadata(metadata.MetadataInfo{
Title: document.Title, Title: document.Title,
Author: document.Author, Author: document.Author,
} })
err = metadata.GetMetadata(&bookMetadata)
if err == nil && bookMetadata.GBID != nil {
// Derive & Sanitize File Name
fileName := "." + filepath.Clean(fmt.Sprintf("/%s.jpg", *bookMetadata.GBID))
// Generate Storage Path if err == nil && len(metadataResults) > 0 && metadataResults[0].GBID != nil {
coverFilePath = filepath.Join(api.Config.DataPath, "covers", fileName) firstResult := metadataResults[0]
err := metadata.SaveCover(*bookMetadata.GBID, coverFilePath) // Save Cover
fileName, err := metadata.SaveCover(*firstResult.GBID, coverDir, document.ID)
if err == nil { if err == nil {
coverID = *bookMetadata.GBID coverFile = *fileName
log.Info("Title:", *bookMetadata.Title)
log.Info("Author:", *bookMetadata.Author)
log.Info("Description:", *bookMetadata.Description)
log.Info("IDs:", bookMetadata.ISBN)
}
} }
// coverIDs, err := metadata.GetCoverOLIDs(document.Title, document.Author) // Store First Metadata Result
// if err == nil && len(coverIDs) > 0 { if _, err = api.DB.Queries.AddMetadata(api.DB.Ctx, database.AddMetadataParams{
// coverFilePath, err = metadata.DownloadAndSaveCover(coverIDs[0], api.Config.DataPath) DocumentID: document.ID,
// if err == nil { Title: firstResult.Title,
// coverID = coverIDs[0] Author: firstResult.Author,
// } Description: firstResult.Description,
// } Gbid: firstResult.GBID,
Olid: firstResult.OLID,
Isbn10: firstResult.ISBN10,
Isbn13: firstResult.ISBN13,
}); err != nil {
log.Error("[getDocumentCover] AddMetadata DB Error:", err)
}
}
// Upsert Document // Upsert Document
if _, err = api.DB.Queries.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{ if _, err = api.DB.Queries.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{
ID: document.ID, ID: document.ID,
Olid: &coverID, Coverfile: &coverFile,
}); err != nil { }); err != nil {
log.Warn("[getDocumentCover] UpsertDocument DB Error:", err) log.Warn("[getDocumentCover] UpsertDocument DB Error:", err)
} }
// Return Unknown Cover // Return Unknown Cover
if coverID == "UNKNOWN" { if coverFile == "UNKNOWN" {
c.File("./assets/no-cover.jpg") c.File("./assets/no-cover.jpg")
return return
} }
coverFilePath := filepath.Join(coverDir, coverFile)
c.File(coverFilePath) c.File(coverFilePath)
} }
// DELETE /api/documents/:document
func (api *API) deleteDocument(c *gin.Context) {
var rDocID requestDocumentID
if err := c.ShouldBindUri(&rDocID); err != nil {
log.Error("[deleteDocument] Invalid URI Bind")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
return
}
changed, err := api.DB.Queries.DeleteDocument(api.DB.Ctx, rDocID.DocumentID)
if err != nil {
log.Error("[deleteDocument] DeleteDocument DB Error")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
return
}
if changed == 0 {
log.Error("[deleteDocument] DeleteDocument DB Error")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Document"})
return
}
c.Redirect(http.StatusFound, "../")
}
// POST /api/documents/:document/identify
func (api *API) identifyDocument(c *gin.Context) {
var rDocID requestDocumentID
if err := c.ShouldBindUri(&rDocID); err != nil {
log.Error("[identifyDocument] Invalid URI Bind")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
return
}
isbn := strings.TrimSpace(c.PostForm("ISBN"))
if isbn == "" {
log.Error("[identifyDocument] Invalid Form")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
return
}
metadataResults, err := metadata.GetMetadata(metadata.MetadataInfo{
ISBN10: &isbn,
ISBN13: &isbn,
})
if err != nil || len(metadataResults) == 0 {
log.Error("[identifyDocument] Metadata Error")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Metadata Error"})
return
}
// TODO
firstResult := metadataResults[0]
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, "/")
}

View File

@ -5,6 +5,7 @@ import (
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
"html"
"io" "io"
"net/http" "net/http"
"os" "os"
@ -72,8 +73,6 @@ type requestDocumentID struct {
DocumentID string `uri:"document" binding:"required"` DocumentID string `uri:"document" binding:"required"`
} }
var allowedExtensions []string = []string{".epub", ".html"}
func (api *API) authorizeUser(c *gin.Context) { func (api *API) authorizeUser(c *gin.Context) {
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"authorized": "OK", "authorized": "OK",
@ -485,7 +484,7 @@ func (api *API) uploadDocumentFile(c *gin.Context) {
fileMime, err := mimetype.DetectReader(uploadedFile) fileMime, err := mimetype.DetectReader(uploadedFile)
fileExtension := fileMime.Extension() fileExtension := fileMime.Extension()
if !slices.Contains(allowedExtensions, fileExtension) { if !slices.Contains([]string{".epub", ".html"}, fileExtension) {
log.Error("[uploadDocumentFile] Invalid FileType:", fileExtension) log.Error("[uploadDocumentFile] Invalid FileType:", fileExtension)
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Filetype"}) c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Filetype"})
return return
@ -609,12 +608,12 @@ func (api *API) sanitizeInput(val any) *string {
switch v := val.(type) { switch v := val.(type) {
case *string: case *string:
if v != nil { if v != nil {
newString := api.HTMLPolicy.Sanitize(string(*v)) newString := html.UnescapeString(api.HTMLPolicy.Sanitize(string(*v)))
return &newString return &newString
} }
case string: case string:
if v != "" { if v != "" {
newString := api.HTMLPolicy.Sanitize(string(v)) newString := html.UnescapeString(api.HTMLPolicy.Sanitize(string(v)))
return &newString return &newString
} }
} }

View File

@ -32,13 +32,17 @@ type Document struct {
ID string `json:"id"` ID string `json:"id"`
Md5 *string `json:"md5"` Md5 *string `json:"md5"`
Filepath *string `json:"filepath"` Filepath *string `json:"filepath"`
Coverfile *string `json:"coverfile"`
Title *string `json:"title"` Title *string `json:"title"`
Author *string `json:"author"` Author *string `json:"author"`
Series *string `json:"series"` Series *string `json:"series"`
SeriesIndex *int64 `json:"series_index"` SeriesIndex *int64 `json:"series_index"`
Lang *string `json:"lang"` Lang *string `json:"lang"`
Description *string `json:"description"` Description *string `json:"description"`
Gbid *string `json:"gbid"`
Olid *string `json:"-"` Olid *string `json:"-"`
Isbn10 *string `json:"isbn10"`
Isbn13 *string `json:"isbn13"`
Synced bool `json:"-"` Synced bool `json:"-"`
Deleted bool `json:"-"` Deleted bool `json:"-"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
@ -62,6 +66,19 @@ type DocumentProgress struct {
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
} }
type Metadatum struct {
ID int64 `json:"id"`
DocumentID string `json:"document_id"`
Title *string `json:"title"`
Author *string `json:"author"`
Description *string `json:"description"`
Gbid *string `json:"gbid"`
Olid *string `json:"olid"`
Isbn10 *string `json:"isbn10"`
Isbn13 *string `json:"isbn13"`
CreatedAt time.Time `json:"created_at"`
}
type RescaledActivity struct { type RescaledActivity struct {
DocumentID string `json:"document_id"` DocumentID string `json:"document_id"`
DeviceID string `json:"device_id"` DeviceID string `json:"device_id"`

View File

@ -1,3 +1,17 @@
-- name: AddMetadata :one
INSERT INTO metadata (
document_id,
title,
author,
description,
gbid,
olid,
isbn10,
isbn13
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
RETURNING *;
-- name: CreateUser :execrows -- name: CreateUser :execrows
INSERT INTO users (id, pass) INSERT INTO users (id, pass)
VALUES (?, ?) VALUES (?, ?)
@ -12,26 +26,34 @@ INSERT INTO documents (
id, id,
md5, md5,
filepath, filepath,
coverfile,
title, title,
author, author,
series, series,
series_index, series_index,
lang, lang,
description, description,
olid olid,
gbid,
isbn10,
isbn13
) )
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT DO UPDATE ON CONFLICT DO UPDATE
SET SET
md5 = COALESCE(excluded.md5, md5), md5 = COALESCE(excluded.md5, md5),
filepath = COALESCE(excluded.filepath, filepath), filepath = COALESCE(excluded.filepath, filepath),
coverfile = COALESCE(excluded.coverfile, coverfile),
title = COALESCE(excluded.title, title), title = COALESCE(excluded.title, title),
author = COALESCE(excluded.author, author), author = COALESCE(excluded.author, author),
series = COALESCE(excluded.series, series), series = COALESCE(excluded.series, series),
series_index = COALESCE(excluded.series_index, series_index), series_index = COALESCE(excluded.series_index, series_index),
lang = COALESCE(excluded.lang, lang), lang = COALESCE(excluded.lang, lang),
description = COALESCE(excluded.description, description), description = COALESCE(excluded.description, description),
olid = COALESCE(excluded.olid, olid) olid = COALESCE(excluded.olid, olid),
gbid = COALESCE(excluded.gbid, gbid),
isbn10 = COALESCE(excluded.isbn10, isbn10),
isbn13 = COALESCE(excluded.isbn13, isbn13)
RETURNING *; RETURNING *;
-- name: DeleteDocument :execrows -- name: DeleteDocument :execrows
@ -222,6 +244,7 @@ SELECT
FROM documents FROM documents
LEFT JOIN true_progress ON true_progress.document_id = documents.id LEFT JOIN true_progress ON true_progress.document_id = documents.id
LEFT JOIN users ON users.id = $user_id LEFT JOIN users ON users.id = $user_id
WHERE documents.deleted == false
ORDER BY true_progress.last_read DESC, documents.created_at DESC ORDER BY true_progress.last_read DESC, documents.created_at DESC
LIMIT $limit LIMIT $limit
OFFSET $offset; OFFSET $offset;

View File

@ -61,6 +61,59 @@ func (q *Queries) AddActivity(ctx context.Context, arg AddActivityParams) (Activ
return i, err return i, err
} }
const addMetadata = `-- name: AddMetadata :one
INSERT INTO metadata (
document_id,
title,
author,
description,
gbid,
olid,
isbn10,
isbn13
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
RETURNING id, document_id, title, author, description, gbid, olid, isbn10, isbn13, created_at
`
type AddMetadataParams struct {
DocumentID string `json:"document_id"`
Title *string `json:"title"`
Author *string `json:"author"`
Description *string `json:"description"`
Gbid *string `json:"gbid"`
Olid *string `json:"olid"`
Isbn10 *string `json:"isbn10"`
Isbn13 *string `json:"isbn13"`
}
func (q *Queries) AddMetadata(ctx context.Context, arg AddMetadataParams) (Metadatum, error) {
row := q.db.QueryRowContext(ctx, addMetadata,
arg.DocumentID,
arg.Title,
arg.Author,
arg.Description,
arg.Gbid,
arg.Olid,
arg.Isbn10,
arg.Isbn13,
)
var i Metadatum
err := row.Scan(
&i.ID,
&i.DocumentID,
&i.Title,
&i.Author,
&i.Description,
&i.Gbid,
&i.Olid,
&i.Isbn10,
&i.Isbn13,
&i.CreatedAt,
)
return i, err
}
const createUser = `-- name: CreateUser :execrows const createUser = `-- name: CreateUser :execrows
INSERT INTO users (id, pass) INSERT INTO users (id, pass)
VALUES (?, ?) VALUES (?, ?)
@ -366,7 +419,7 @@ func (q *Queries) GetDevices(ctx context.Context, arg GetDevicesParams) ([]Devic
} }
const getDocument = `-- name: GetDocument :one const getDocument = `-- name: GetDocument :one
SELECT id, md5, filepath, title, author, series, series_index, lang, description, olid, synced, deleted, updated_at, created_at FROM documents SELECT id, md5, filepath, coverfile, title, author, series, series_index, lang, description, gbid, olid, isbn10, isbn13, synced, deleted, updated_at, created_at FROM documents
WHERE id = ?1 LIMIT 1 WHERE id = ?1 LIMIT 1
` `
@ -377,13 +430,17 @@ func (q *Queries) GetDocument(ctx context.Context, documentID string) (Document,
&i.ID, &i.ID,
&i.Md5, &i.Md5,
&i.Filepath, &i.Filepath,
&i.Coverfile,
&i.Title, &i.Title,
&i.Author, &i.Author,
&i.Series, &i.Series,
&i.SeriesIndex, &i.SeriesIndex,
&i.Lang, &i.Lang,
&i.Description, &i.Description,
&i.Gbid,
&i.Olid, &i.Olid,
&i.Isbn10,
&i.Isbn13,
&i.Synced, &i.Synced,
&i.Deleted, &i.Deleted,
&i.UpdatedAt, &i.UpdatedAt,
@ -501,7 +558,7 @@ WITH true_progress AS (
LIMIT 1 LIMIT 1
) )
SELECT SELECT
documents.id, documents.md5, documents.filepath, documents.title, documents.author, documents.series, documents.series_index, documents.lang, documents.description, documents.olid, documents.synced, documents.deleted, documents.updated_at, documents.created_at, documents.id, documents.md5, documents.filepath, documents.coverfile, documents.title, documents.author, documents.series, documents.series_index, documents.lang, documents.description, documents.gbid, documents.olid, documents.isbn10, documents.isbn13, documents.synced, documents.deleted, documents.updated_at, documents.created_at,
CAST(IFNULL(current_page, 0) AS INTEGER) AS current_page, CAST(IFNULL(current_page, 0) AS INTEGER) AS current_page,
CAST(IFNULL(total_pages, 0) AS INTEGER) AS total_pages, CAST(IFNULL(total_pages, 0) AS INTEGER) AS total_pages,
@ -531,13 +588,17 @@ type GetDocumentWithStatsRow struct {
ID string `json:"id"` ID string `json:"id"`
Md5 *string `json:"md5"` Md5 *string `json:"md5"`
Filepath *string `json:"filepath"` Filepath *string `json:"filepath"`
Coverfile *string `json:"coverfile"`
Title *string `json:"title"` Title *string `json:"title"`
Author *string `json:"author"` Author *string `json:"author"`
Series *string `json:"series"` Series *string `json:"series"`
SeriesIndex *int64 `json:"series_index"` SeriesIndex *int64 `json:"series_index"`
Lang *string `json:"lang"` Lang *string `json:"lang"`
Description *string `json:"description"` Description *string `json:"description"`
Gbid *string `json:"gbid"`
Olid *string `json:"-"` Olid *string `json:"-"`
Isbn10 *string `json:"isbn10"`
Isbn13 *string `json:"isbn13"`
Synced bool `json:"-"` Synced bool `json:"-"`
Deleted bool `json:"-"` Deleted bool `json:"-"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
@ -556,13 +617,17 @@ func (q *Queries) GetDocumentWithStats(ctx context.Context, arg GetDocumentWithS
&i.ID, &i.ID,
&i.Md5, &i.Md5,
&i.Filepath, &i.Filepath,
&i.Coverfile,
&i.Title, &i.Title,
&i.Author, &i.Author,
&i.Series, &i.Series,
&i.SeriesIndex, &i.SeriesIndex,
&i.Lang, &i.Lang,
&i.Description, &i.Description,
&i.Gbid,
&i.Olid, &i.Olid,
&i.Isbn10,
&i.Isbn13,
&i.Synced, &i.Synced,
&i.Deleted, &i.Deleted,
&i.UpdatedAt, &i.UpdatedAt,
@ -577,7 +642,7 @@ func (q *Queries) GetDocumentWithStats(ctx context.Context, arg GetDocumentWithS
} }
const getDocuments = `-- name: GetDocuments :many const getDocuments = `-- name: GetDocuments :many
SELECT id, md5, filepath, title, author, series, series_index, lang, description, olid, synced, deleted, updated_at, created_at FROM documents SELECT id, md5, filepath, coverfile, title, author, series, series_index, lang, description, gbid, olid, isbn10, isbn13, synced, deleted, updated_at, created_at FROM documents
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT ?2 LIMIT ?2
OFFSET ?1 OFFSET ?1
@ -601,13 +666,17 @@ func (q *Queries) GetDocuments(ctx context.Context, arg GetDocumentsParams) ([]D
&i.ID, &i.ID,
&i.Md5, &i.Md5,
&i.Filepath, &i.Filepath,
&i.Coverfile,
&i.Title, &i.Title,
&i.Author, &i.Author,
&i.Series, &i.Series,
&i.SeriesIndex, &i.SeriesIndex,
&i.Lang, &i.Lang,
&i.Description, &i.Description,
&i.Gbid,
&i.Olid, &i.Olid,
&i.Isbn10,
&i.Isbn13,
&i.Synced, &i.Synced,
&i.Deleted, &i.Deleted,
&i.UpdatedAt, &i.UpdatedAt,
@ -641,7 +710,7 @@ WITH true_progress AS (
HAVING MAX(start_time) HAVING MAX(start_time)
) )
SELECT SELECT
documents.id, documents.md5, documents.filepath, documents.title, documents.author, documents.series, documents.series_index, documents.lang, documents.description, documents.olid, documents.synced, documents.deleted, documents.updated_at, documents.created_at, documents.id, documents.md5, documents.filepath, documents.coverfile, documents.title, documents.author, documents.series, documents.series_index, documents.lang, documents.description, documents.gbid, documents.olid, documents.isbn10, documents.isbn13, documents.synced, documents.deleted, documents.updated_at, documents.created_at,
CAST(IFNULL(current_page, 0) AS INTEGER) AS current_page, CAST(IFNULL(current_page, 0) AS INTEGER) AS current_page,
CAST(IFNULL(total_pages, 0) AS INTEGER) AS total_pages, CAST(IFNULL(total_pages, 0) AS INTEGER) AS total_pages,
@ -657,6 +726,7 @@ SELECT
FROM documents FROM documents
LEFT JOIN true_progress ON true_progress.document_id = documents.id LEFT JOIN true_progress ON true_progress.document_id = documents.id
LEFT JOIN users ON users.id = ?1 LEFT JOIN users ON users.id = ?1
WHERE documents.deleted == false
ORDER BY true_progress.last_read DESC, documents.created_at DESC ORDER BY true_progress.last_read DESC, documents.created_at DESC
LIMIT ?3 LIMIT ?3
OFFSET ?2 OFFSET ?2
@ -672,13 +742,17 @@ type GetDocumentsWithStatsRow struct {
ID string `json:"id"` ID string `json:"id"`
Md5 *string `json:"md5"` Md5 *string `json:"md5"`
Filepath *string `json:"filepath"` Filepath *string `json:"filepath"`
Coverfile *string `json:"coverfile"`
Title *string `json:"title"` Title *string `json:"title"`
Author *string `json:"author"` Author *string `json:"author"`
Series *string `json:"series"` Series *string `json:"series"`
SeriesIndex *int64 `json:"series_index"` SeriesIndex *int64 `json:"series_index"`
Lang *string `json:"lang"` Lang *string `json:"lang"`
Description *string `json:"description"` Description *string `json:"description"`
Gbid *string `json:"gbid"`
Olid *string `json:"-"` Olid *string `json:"-"`
Isbn10 *string `json:"isbn10"`
Isbn13 *string `json:"isbn13"`
Synced bool `json:"-"` Synced bool `json:"-"`
Deleted bool `json:"-"` Deleted bool `json:"-"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
@ -703,13 +777,17 @@ func (q *Queries) GetDocumentsWithStats(ctx context.Context, arg GetDocumentsWit
&i.ID, &i.ID,
&i.Md5, &i.Md5,
&i.Filepath, &i.Filepath,
&i.Coverfile,
&i.Title, &i.Title,
&i.Author, &i.Author,
&i.Series, &i.Series,
&i.SeriesIndex, &i.SeriesIndex,
&i.Lang, &i.Lang,
&i.Description, &i.Description,
&i.Gbid,
&i.Olid, &i.Olid,
&i.Isbn10,
&i.Isbn13,
&i.Synced, &i.Synced,
&i.Deleted, &i.Deleted,
&i.UpdatedAt, &i.UpdatedAt,
@ -754,7 +832,7 @@ func (q *Queries) GetLastActivity(ctx context.Context, arg GetLastActivityParams
} }
const getMissingDocuments = `-- name: GetMissingDocuments :many const getMissingDocuments = `-- name: GetMissingDocuments :many
SELECT documents.id, documents.md5, documents.filepath, documents.title, documents.author, documents.series, documents.series_index, documents.lang, documents.description, documents.olid, documents.synced, documents.deleted, documents.updated_at, documents.created_at FROM documents SELECT documents.id, documents.md5, documents.filepath, documents.coverfile, documents.title, documents.author, documents.series, documents.series_index, documents.lang, documents.description, documents.gbid, documents.olid, documents.isbn10, documents.isbn13, documents.synced, documents.deleted, documents.updated_at, documents.created_at FROM documents
WHERE WHERE
documents.filepath IS NOT NULL documents.filepath IS NOT NULL
AND documents.deleted = false AND documents.deleted = false
@ -784,13 +862,17 @@ func (q *Queries) GetMissingDocuments(ctx context.Context, documentIds []string)
&i.ID, &i.ID,
&i.Md5, &i.Md5,
&i.Filepath, &i.Filepath,
&i.Coverfile,
&i.Title, &i.Title,
&i.Author, &i.Author,
&i.Series, &i.Series,
&i.SeriesIndex, &i.SeriesIndex,
&i.Lang, &i.Lang,
&i.Description, &i.Description,
&i.Gbid,
&i.Olid, &i.Olid,
&i.Isbn10,
&i.Isbn13,
&i.Synced, &i.Synced,
&i.Deleted, &i.Deleted,
&i.UpdatedAt, &i.UpdatedAt,
@ -1073,7 +1155,7 @@ UPDATE documents
SET SET
deleted = ?1 deleted = ?1
WHERE id = ?2 WHERE id = ?2
RETURNING id, md5, filepath, title, author, series, series_index, lang, description, olid, synced, deleted, updated_at, created_at RETURNING id, md5, filepath, coverfile, title, author, series, series_index, lang, description, gbid, olid, isbn10, isbn13, synced, deleted, updated_at, created_at
` `
type UpdateDocumentDeletedParams struct { type UpdateDocumentDeletedParams struct {
@ -1088,13 +1170,17 @@ func (q *Queries) UpdateDocumentDeleted(ctx context.Context, arg UpdateDocumentD
&i.ID, &i.ID,
&i.Md5, &i.Md5,
&i.Filepath, &i.Filepath,
&i.Coverfile,
&i.Title, &i.Title,
&i.Author, &i.Author,
&i.Series, &i.Series,
&i.SeriesIndex, &i.SeriesIndex,
&i.Lang, &i.Lang,
&i.Description, &i.Description,
&i.Gbid,
&i.Olid, &i.Olid,
&i.Isbn10,
&i.Isbn13,
&i.Synced, &i.Synced,
&i.Deleted, &i.Deleted,
&i.UpdatedAt, &i.UpdatedAt,
@ -1108,7 +1194,7 @@ UPDATE documents
SET SET
synced = ?1 synced = ?1
WHERE id = ?2 WHERE id = ?2
RETURNING id, md5, filepath, title, author, series, series_index, lang, description, olid, synced, deleted, updated_at, created_at RETURNING id, md5, filepath, coverfile, title, author, series, series_index, lang, description, gbid, olid, isbn10, isbn13, synced, deleted, updated_at, created_at
` `
type UpdateDocumentSyncParams struct { type UpdateDocumentSyncParams struct {
@ -1123,13 +1209,17 @@ func (q *Queries) UpdateDocumentSync(ctx context.Context, arg UpdateDocumentSync
&i.ID, &i.ID,
&i.Md5, &i.Md5,
&i.Filepath, &i.Filepath,
&i.Coverfile,
&i.Title, &i.Title,
&i.Author, &i.Author,
&i.Series, &i.Series,
&i.SeriesIndex, &i.SeriesIndex,
&i.Lang, &i.Lang,
&i.Description, &i.Description,
&i.Gbid,
&i.Olid, &i.Olid,
&i.Isbn10,
&i.Isbn13,
&i.Synced, &i.Synced,
&i.Deleted, &i.Deleted,
&i.UpdatedAt, &i.UpdatedAt,
@ -1211,33 +1301,42 @@ INSERT INTO documents (
id, id,
md5, md5,
filepath, filepath,
coverfile,
title, title,
author, author,
series, series,
series_index, series_index,
lang, lang,
description, description,
olid olid,
gbid,
isbn10,
isbn13
) )
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT DO UPDATE ON CONFLICT DO UPDATE
SET SET
md5 = COALESCE(excluded.md5, md5), md5 = COALESCE(excluded.md5, md5),
filepath = COALESCE(excluded.filepath, filepath), filepath = COALESCE(excluded.filepath, filepath),
coverfile = COALESCE(excluded.coverfile, coverfile),
title = COALESCE(excluded.title, title), title = COALESCE(excluded.title, title),
author = COALESCE(excluded.author, author), author = COALESCE(excluded.author, author),
series = COALESCE(excluded.series, series), series = COALESCE(excluded.series, series),
series_index = COALESCE(excluded.series_index, series_index), series_index = COALESCE(excluded.series_index, series_index),
lang = COALESCE(excluded.lang, lang), lang = COALESCE(excluded.lang, lang),
description = COALESCE(excluded.description, description), description = COALESCE(excluded.description, description),
olid = COALESCE(excluded.olid, olid) olid = COALESCE(excluded.olid, olid),
RETURNING id, md5, filepath, title, author, series, series_index, lang, description, olid, synced, deleted, updated_at, created_at gbid = COALESCE(excluded.gbid, gbid),
isbn10 = COALESCE(excluded.isbn10, isbn10),
isbn13 = COALESCE(excluded.isbn13, isbn13)
RETURNING id, md5, filepath, coverfile, title, author, series, series_index, lang, description, gbid, olid, isbn10, isbn13, synced, deleted, updated_at, created_at
` `
type UpsertDocumentParams struct { type UpsertDocumentParams struct {
ID string `json:"id"` ID string `json:"id"`
Md5 *string `json:"md5"` Md5 *string `json:"md5"`
Filepath *string `json:"filepath"` Filepath *string `json:"filepath"`
Coverfile *string `json:"coverfile"`
Title *string `json:"title"` Title *string `json:"title"`
Author *string `json:"author"` Author *string `json:"author"`
Series *string `json:"series"` Series *string `json:"series"`
@ -1245,6 +1344,9 @@ type UpsertDocumentParams struct {
Lang *string `json:"lang"` Lang *string `json:"lang"`
Description *string `json:"description"` Description *string `json:"description"`
Olid *string `json:"-"` Olid *string `json:"-"`
Gbid *string `json:"gbid"`
Isbn10 *string `json:"isbn10"`
Isbn13 *string `json:"isbn13"`
} }
func (q *Queries) UpsertDocument(ctx context.Context, arg UpsertDocumentParams) (Document, error) { func (q *Queries) UpsertDocument(ctx context.Context, arg UpsertDocumentParams) (Document, error) {
@ -1252,6 +1354,7 @@ func (q *Queries) UpsertDocument(ctx context.Context, arg UpsertDocumentParams)
arg.ID, arg.ID,
arg.Md5, arg.Md5,
arg.Filepath, arg.Filepath,
arg.Coverfile,
arg.Title, arg.Title,
arg.Author, arg.Author,
arg.Series, arg.Series,
@ -1259,19 +1362,26 @@ func (q *Queries) UpsertDocument(ctx context.Context, arg UpsertDocumentParams)
arg.Lang, arg.Lang,
arg.Description, arg.Description,
arg.Olid, arg.Olid,
arg.Gbid,
arg.Isbn10,
arg.Isbn13,
) )
var i Document var i Document
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.Md5, &i.Md5,
&i.Filepath, &i.Filepath,
&i.Coverfile,
&i.Title, &i.Title,
&i.Author, &i.Author,
&i.Series, &i.Series,
&i.SeriesIndex, &i.SeriesIndex,
&i.Lang, &i.Lang,
&i.Description, &i.Description,
&i.Gbid,
&i.Olid, &i.Olid,
&i.Isbn10,
&i.Isbn13,
&i.Synced, &i.Synced,
&i.Deleted, &i.Deleted,
&i.UpdatedAt, &i.UpdatedAt,

View File

@ -18,13 +18,19 @@ CREATE TABLE IF NOT EXISTS documents (
md5 TEXT, md5 TEXT,
filepath TEXT, filepath TEXT,
coverfile TEXT,
title TEXT, title TEXT,
author TEXT, author TEXT,
series TEXT, series TEXT,
series_index INTEGER, series_index INTEGER,
lang TEXT, lang TEXT,
description TEXT, description TEXT,
gbid TEXT,
olid TEXT, olid TEXT,
isbn10 TEXT,
isbn13 TEXT,
synced BOOLEAN NOT NULL DEFAULT 0 CHECK (synced IN (0, 1)), synced BOOLEAN NOT NULL DEFAULT 0 CHECK (synced IN (0, 1)),
deleted BOOLEAN NOT NULL DEFAULT 0 CHECK (deleted IN (0, 1)), deleted BOOLEAN NOT NULL DEFAULT 0 CHECK (deleted IN (0, 1)),
@ -32,6 +38,25 @@ CREATE TABLE IF NOT EXISTS documents (
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
); );
-- Metadata
CREATE TABLE IF NOT EXISTS metadata (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_id TEXT NOT NULL,
title TEXT,
author TEXT,
description TEXT,
gbid TEXT,
olid TEXT,
isbn10 TEXT,
isbn13 TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (document_id) REFERENCES documents (id)
);
-- Devices -- Devices
CREATE TABLE IF NOT EXISTS devices ( CREATE TABLE IF NOT EXISTS devices (
id TEXT NOT NULL PRIMARY KEY, id TEXT NOT NULL PRIMARY KEY,

View File

@ -8,6 +8,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"path/filepath"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -17,7 +18,9 @@ type MetadataInfo struct {
Author *string Author *string
Description *string Description *string
GBID *string GBID *string
ISBN []*string OLID *string
ISBN10 *string
ISBN13 *string
} }
type gBooksIdentifiers struct { type gBooksIdentifiers struct {
@ -46,73 +49,99 @@ const GBOOKS_QUERY_URL string = "https://www.googleapis.com/books/v1/volumes?q=%
const GBOOKS_GBID_INFO_URL string = "https://www.googleapis.com/books/v1/volumes/%s" const GBOOKS_GBID_INFO_URL string = "https://www.googleapis.com/books/v1/volumes/%s"
const GBOOKS_GBID_COVER_URL string = "https://books.google.com/books/content/images/frontcover/%s?fife=w480-h690" const GBOOKS_GBID_COVER_URL string = "https://books.google.com/books/content/images/frontcover/%s?fife=w480-h690"
func GetMetadata(data *MetadataInfo) error { func GetMetadata(metadataSearch MetadataInfo) ([]MetadataInfo, error) {
var queryResult *gBooksQueryItem var queryResults []gBooksQueryItem
if data.GBID != nil { if metadataSearch.GBID != nil {
// Use GBID // Use GBID
resp, err := performGBIDRequest(*data.GBID) resp, err := performGBIDRequest(*metadataSearch.GBID)
if err != nil { if err != nil {
return err return nil, err
} }
queryResult = resp queryResults = []gBooksQueryItem{*resp}
} else if len(data.ISBN) > 0 { } else if metadataSearch.ISBN13 != nil {
searchQuery := "isbn:" + *data.ISBN[0] searchQuery := "isbn:" + *metadataSearch.ISBN13
resp, err := performSearchRequest(searchQuery) resp, err := performSearchRequest(searchQuery)
if err != nil { if err != nil {
return err return nil, err
} }
queryResult = &resp.Items[0] queryResults = resp.Items
} else if data.Title != nil && data.Author != nil { } else if metadataSearch.ISBN10 != nil {
searchQuery := url.QueryEscape(fmt.Sprintf("%s %s", *data.Title, *data.Author)) searchQuery := "isbn:" + *metadataSearch.ISBN10
resp, err := performSearchRequest(searchQuery) resp, err := performSearchRequest(searchQuery)
if err != nil { if err != nil {
return err return nil, err
} }
queryResult = &resp.Items[0] queryResults = resp.Items
} else if metadataSearch.Title != nil && metadataSearch.Author != nil {
searchQuery := url.QueryEscape(fmt.Sprintf("%s %s", *metadataSearch.Title, *metadataSearch.Author))
resp, err := performSearchRequest(searchQuery)
if err != nil {
return nil, err
}
queryResults = resp.Items
} else { } else {
return errors.New("Invalid Data") return nil, errors.New("Invalid Data")
} }
// Merge Data // Normalize Data
data.GBID = &queryResult.ID allMetadata := []MetadataInfo{}
data.Description = &queryResult.Info.Description
data.Title = &queryResult.Info.Title for i := range queryResults {
if len(queryResult.Info.Authors) > 0 { // Account for range value changing every iteration
data.Author = &queryResult.Info.Authors[0] item := queryResults[i]
} itemResult := MetadataInfo{
for _, item := range queryResult.Info.Identifiers { GBID: &item.ID,
if item.Type == "ISBN_10" || item.Type == "ISBN_13" { Title: &item.Info.Title,
data.ISBN = append(data.ISBN, &item.Identifier) Description: &item.Info.Description,
} }
if len(item.Info.Authors) > 0 {
itemResult.Author = &item.Info.Authors[0]
}
for _, item := range item.Info.Identifiers {
if itemResult.ISBN10 != nil && itemResult.ISBN13 != nil {
break
} else if itemResult.ISBN10 == nil && item.Type == "ISBN_10" {
itemResult.ISBN10 = &item.Identifier
} else if itemResult.ISBN13 == nil && item.Type == "ISBN_13" {
itemResult.ISBN13 = &item.Identifier
}
} }
return nil allMetadata = append(allMetadata, itemResult)
} }
func SaveCover(id string, safePath string) error { return allMetadata, nil
}
func SaveCover(gbid string, coverDir string, documentID string) (*string, error) {
// Google Books -> JPG
coverFile := "." + filepath.Clean(fmt.Sprintf("/%s.jpg", documentID))
coverFilePath := filepath.Join(coverDir, coverFile)
// Validate File Doesn't Exists // Validate File Doesn't Exists
_, err := os.Stat(safePath) _, err := os.Stat(coverFilePath)
if err == nil { if err == nil {
log.Warn("[SaveCover] File Alreads Exists") log.Warn("[SaveCover] File Alreads Exists")
return nil return &coverFile, nil
} }
// Create File // Create File
out, err := os.Create(safePath) out, err := os.Create(coverFilePath)
if err != nil { if err != nil {
log.Error("[SaveCover] File Create Error") log.Error("[SaveCover] File Create Error")
return errors.New("File Failure") return nil, errors.New("File Failure")
} }
defer out.Close() defer out.Close()
// Download File // Download File
log.Info("[SaveCover] Downloading Cover") log.Info("[SaveCover] Downloading Cover")
coverURL := fmt.Sprintf(GBOOKS_GBID_COVER_URL, id) coverURL := fmt.Sprintf(GBOOKS_GBID_COVER_URL, gbid)
resp, err := http.Get(coverURL) resp, err := http.Get(coverURL)
if err != nil { if err != nil {
log.Error("[SaveCover] Cover URL API Failure") log.Error("[SaveCover] Cover URL API Failure")
return errors.New("API Failure") return nil, errors.New("API Failure")
} }
defer resp.Body.Close() defer resp.Body.Close()
@ -121,20 +150,19 @@ func SaveCover(id string, safePath string) error {
_, err = io.Copy(out, resp.Body) _, err = io.Copy(out, resp.Body)
if err != nil { if err != nil {
log.Error("[SaveCover] File Copy Error") log.Error("[SaveCover] File Copy Error")
return errors.New("File Failure") return nil, errors.New("File Failure")
} }
// Return FilePath // Return FilePath
return nil return &coverFile, nil
} }
func performSearchRequest(searchQuery string) (*gBooksQueryResponse, error) { func performSearchRequest(searchQuery string) (*gBooksQueryResponse, error) {
apiQuery := fmt.Sprintf(GBOOKS_QUERY_URL, searchQuery) apiQuery := fmt.Sprintf(GBOOKS_QUERY_URL, searchQuery)
log.Info("[performSearchRequest] Acquiring Metadata: ", apiQuery)
log.Info("[performSearchRequest] Acquiring CoverID")
resp, err := http.Get(apiQuery) resp, err := http.Get(apiQuery)
if err != nil { if err != nil {
log.Error("[performSearchRequest] Cover URL API Failure") log.Error("[performSearchRequest] Google Books Query URL API Failure")
return nil, errors.New("API Failure") return nil, errors.New("API Failure")
} }

View File

@ -18,6 +18,10 @@ sql:
go_type: go_type:
type: "string" type: "string"
pointer: true pointer: true
- column: "documents.coverfile"
go_type:
type: "string"
pointer: true
- column: "documents.title" - column: "documents.title"
go_type: go_type:
type: "string" type: "string"
@ -46,6 +50,47 @@ sql:
go_type: go_type:
type: "string" type: "string"
pointer: true pointer: true
- column: "documents.gbid"
go_type:
type: "string"
pointer: true
- column: "documents.isbn10"
go_type:
type: "string"
pointer: true
- column: "documents.isbn13"
go_type:
type: "string"
pointer: true
- column: "metadata.title"
go_type:
type: "string"
pointer: true
- column: "metadata.author"
go_type:
type: "string"
pointer: true
- column: "metadata.description"
go_type:
type: "string"
pointer: true
- column: "metadata.gbid"
go_type:
type: "string"
pointer: true
- column: "metadata.olid"
go_type:
type: "string"
pointer: true
- column: "metadata.isbn10"
go_type:
type: "string"
pointer: true
- column: "metadata.isbn13"
go_type:
type: "string"
pointer: true
# Do not generate JSON # Do not generate JSON
- column: "documents.synced" - column: "documents.synced"

View File

@ -0,0 +1,166 @@
{{template "base.html" .}}
{{define "title"}}Documents{{end}}
{{define "header"}}
<a href="../documents">Documents</a>
{{end}}
{{define "content"}}
<div class="h-full w-full overflow-scroll bg-white shadow-lg dark:bg-gray-700 rounded dark:text-white p-6">
<div class="flex flex-col gap-2 float-left mr-6 mb-6">
<img class="rounded w-40 md:w-60 lg:w-80 object-fill h-full" src="../documents/{{.Data.ID}}/cover"></img>
<div class="flex gap-2 justify-end text-gray-500 dark:text-gray-400">
<div class="relative">
<label for="delete-button">
<svg
width="24"
height="24"
class="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 6.52381C3 6.12932 3.32671 5.80952 3.72973 5.80952H8.51787C8.52437 4.9683 8.61554 3.81504 9.45037 3.01668C10.1074 2.38839 11.0081 2 12 2C12.9919 2 13.8926 2.38839 14.5496 3.01668C15.3844 3.81504 15.4756 4.9683 15.4821 5.80952H20.2703C20.6733 5.80952 21 6.12932 21 6.52381C21 6.9183 20.6733 7.2381 20.2703 7.2381H3.72973C3.32671 7.2381 3 6.9183 3 6.52381Z"
/>
<path
d="M11.6066 22H12.3935C15.101 22 16.4547 22 17.3349 21.1368C18.2151 20.2736 18.3052 18.8576 18.4853 16.0257L18.7448 11.9452C18.8425 10.4086 18.8913 9.64037 18.4498 9.15352C18.0082 8.66667 17.2625 8.66667 15.7712 8.66667H8.22884C6.7375 8.66667 5.99183 8.66667 5.55026 9.15352C5.1087 9.64037 5.15756 10.4086 5.25528 11.9452L5.51479 16.0257C5.69489 18.8576 5.78494 20.2736 6.66513 21.1368C7.54532 22 8.89906 22 11.6066 22Z"
/>
</svg>
</label>
<input type="checkbox" id="delete-button" class="hidden css-button"/>
<form
method="POST"
action="./{{ .Data.ID }}/delete"
class="absolute bottom-7 left-5 text-black bg-gray-200 transition-all duration-200 rounded shadow-inner shadow-lg shadow-gray-500 dark:text-white dark:shadow-gray-900 dark:bg-gray-600 text-sm p-3"
>
<p class="font-medium w-24 pb-2">Are you sure?</p>
<button class="font-medium w-full px-2 py-1 text-white bg-gray-500 dark:text-gray-800 dark:bg-gray-400 hover:bg-gray-800 dark:hover:bg-gray-100" type="submit">Delete</button>
</form>
</div>
<a href="../activity?document={{ .Data.ID }}">
<svg
width="24"
height="24"
class="hover:text-gray-800 dark:hover:text-gray-100"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M9.5 2C8.67157 2 8 2.67157 8 3.5V4.5C8 5.32843 8.67157 6 9.5 6H14.5C15.3284 6 16 5.32843 16 4.5V3.5C16 2.67157 15.3284 2 14.5 2H9.5Z"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.5 4.03662C5.24209 4.10719 4.44798 4.30764 3.87868 4.87694C3 5.75562 3 7.16983 3 9.99826V15.9983C3 18.8267 3 20.2409 3.87868 21.1196C4.75736 21.9983 6.17157 21.9983 9 21.9983H15C17.8284 21.9983 19.2426 21.9983 20.1213 21.1196C21 20.2409 21 18.8267 21 15.9983V9.99826C21 7.16983 21 5.75562 20.1213 4.87694C19.552 4.30764 18.7579 4.10719 17.5 4.03662V4.5C17.5 6.15685 16.1569 7.5 14.5 7.5H9.5C7.84315 7.5 6.5 6.15685 6.5 4.5V4.03662ZM7 9.75C6.58579 9.75 6.25 10.0858 6.25 10.5C6.25 10.9142 6.58579 11.25 7 11.25H7.5C7.91421 11.25 8.25 10.9142 8.25 10.5C8.25 10.0858 7.91421 9.75 7.5 9.75H7ZM10.5 9.75C10.0858 9.75 9.75 10.0858 9.75 10.5C9.75 10.9142 10.0858 11.25 10.5 11.25H17C17.4142 11.25 17.75 10.9142 17.75 10.5C17.75 10.0858 17.4142 9.75 17 9.75H10.5ZM7 13.25C6.58579 13.25 6.25 13.5858 6.25 14C6.25 14.4142 6.58579 14.75 7 14.75H7.5C7.91421 14.75 8.25 14.4142 8.25 14C8.25 13.5858 7.91421 13.25 7.5 13.25H7ZM10.5 13.25C10.0858 13.25 9.75 13.5858 9.75 14C9.75 14.4142 10.0858 14.75 10.5 14.75H17C17.4142 14.75 17.75 14.4142 17.75 14C17.75 13.5858 17.4142 13.25 17 13.25H10.5ZM7 16.75C6.58579 16.75 6.25 17.0858 6.25 17.5C6.25 17.9142 6.58579 18.25 7 18.25H7.5C7.91421 18.25 8.25 17.9142 8.25 17.5C8.25 17.0858 7.91421 16.75 7.5 16.75H7ZM10.5 16.75C10.0858 16.75 9.75 17.0858 9.75 17.5C9.75 17.9142 10.0858 18.25 10.5 18.25H17C17.4142 18.25 17.75 17.9142 17.75 17.5C17.75 17.0858 17.4142 16.75 17 16.75H10.5Z"/>
</svg>
</a>
<div class="relative">
<label for="edit-button">
<svg
width="24"
height="24"
class="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M21.1938 2.80624C22.2687 3.88124 22.2687 5.62415 21.1938 6.69914L20.6982 7.19469C20.5539 7.16345 20.3722 7.11589 20.1651 7.04404C19.6108 6.85172 18.8823 6.48827 18.197 5.803C17.5117 5.11774 17.1483 4.38923 16.956 3.8349C16.8841 3.62781 16.8366 3.44609 16.8053 3.30179L17.3009 2.80624C18.3759 1.73125 20.1188 1.73125 21.1938 2.80624Z"
/>
<path
d="M14.5801 13.3128C14.1761 13.7168 13.9741 13.9188 13.7513 14.0926C13.4886 14.2975 13.2043 14.4732 12.9035 14.6166C12.6485 14.7381 12.3775 14.8284 11.8354 15.0091L8.97709 15.9619C8.71035 16.0508 8.41626 15.9814 8.21744 15.7826C8.01862 15.5837 7.9492 15.2897 8.03811 15.0229L8.99089 12.1646C9.17157 11.6225 9.26191 11.3515 9.38344 11.0965C9.52679 10.7957 9.70249 10.5114 9.90743 10.2487C10.0812 10.0259 10.2832 9.82394 10.6872 9.41993L15.6033 4.50385C15.867 5.19804 16.3293 6.05663 17.1363 6.86366C17.9434 7.67069 18.802 8.13296 19.4962 8.39674L14.5801 13.3128Z"
/>
<path
d="M20.5355 20.5355C22 19.0711 22 16.714 22 12C22 10.4517 22 9.15774 21.9481 8.0661L15.586 14.4283C15.2347 14.7797 14.9708 15.0437 14.6738 15.2753C14.3252 15.5473 13.948 15.7804 13.5488 15.9706C13.2088 16.1327 12.8546 16.2506 12.3833 16.4076L9.45143 17.3849C8.64568 17.6535 7.75734 17.4438 7.15678 16.8432C6.55621 16.2427 6.34651 15.3543 6.61509 14.5486L7.59235 11.6167C7.74936 11.1454 7.86732 10.7912 8.02935 10.4512C8.21958 10.052 8.45272 9.6748 8.72466 9.32615C8.9563 9.02918 9.22032 8.76528 9.57173 8.41404L15.9339 2.05188C14.8423 2 13.5483 2 12 2C7.28595 2 4.92893 2 3.46447 3.46447C2 4.92893 2 7.28595 2 12C2 16.714 2 19.0711 3.46447 20.5355C4.92893 22 7.28595 22 12 22C16.714 22 19.0711 22 20.5355 20.5355Z"
/>
</svg>
</label>
<input type="checkbox" id="edit-button" class="hidden css-button"/>
<form
method="POST"
action="./{{ .Data.ID }}/edit"
class="absolute bottom-7 left-5 text-black bg-gray-200 transition-all duration-200 rounded shadow-inner shadow-lg shadow-gray-500 dark:text-white dark:shadow-gray-900 dark:bg-gray-600 text-sm p-3"
>
<label class="font-medium" for="isbn">ISBN</label>
<input class="mt-1 mb-2 p-1 bg-gray-400 text-black dark:bg-gray-700 dark:text-white" type="text" id="isbn" name="ISBN"><br>
<button class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 dark:bg-gray-400 hover:bg-gray-800 dark:hover:bg-gray-100" type="submit">Search Metadata</button>
</form>
</div>
{{ if .Data.Filepath }}
<a href="./{{.Data.ID}}/file">
<svg
width="24"
height="24"
class="hover:text-gray-800 dark:hover:text-gray-100"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12ZM12 6.25C12.4142 6.25 12.75 6.58579 12.75 7V12.1893L14.4697 10.4697C14.7626 10.1768 15.2374 10.1768 15.5303 10.4697C15.8232 10.7626 15.8232 11.2374 15.5303 11.5303L12.5303 14.5303C12.3897 14.671 12.1989 14.75 12 14.75C11.8011 14.75 11.6103 14.671 11.4697 14.5303L8.46967 11.5303C8.17678 11.2374 8.17678 10.7626 8.46967 10.4697C8.76256 10.1768 9.23744 10.1768 9.53033 10.4697L11.25 12.1893V7C11.25 6.58579 11.5858 6.25 12 6.25ZM8 16.25C7.58579 16.25 7.25 16.5858 7.25 17C7.25 17.4142 7.58579 17.75 8 17.75H16C16.4142 17.75 16.75 17.4142 16.75 17C16.75 16.5858 16.4142 16.25 16 16.25H8Z"
/>
</svg>
</a>
{{ else }}
<svg
width="24"
height="24"
class="text-gray-200 dark:text-gray-600"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12ZM12 6.25C12.4142 6.25 12.75 6.58579 12.75 7V12.1893L14.4697 10.4697C14.7626 10.1768 15.2374 10.1768 15.5303 10.4697C15.8232 10.7626 15.8232 11.2374 15.5303 11.5303L12.5303 14.5303C12.3897 14.671 12.1989 14.75 12 14.75C11.8011 14.75 11.6103 14.671 11.4697 14.5303L8.46967 11.5303C8.17678 11.2374 8.17678 10.7626 8.46967 10.4697C8.76256 10.1768 9.23744 10.1768 9.53033 10.4697L11.25 12.1893V7C11.25 6.58579 11.5858 6.25 12 6.25ZM8 16.25C7.58579 16.25 7.25 16.5858 7.25 17C7.25 17.4142 7.58579 17.75 8 17.75H16C16.4142 17.75 16.75 17.4142 16.75 17C16.75 16.5858 16.4142 16.25 16 16.25H8Z"
/>
</svg>
{{ end }}
</div>
</div>
<div class="flex flex-wrap justify-between gap-6 pb-6">
<div>
<p class="text-gray-400">Title</p>
<!-- <input type="text" class="font-medium bg-transparent" value="{{ or .Data.Title "Unknown" }}"/> -->
<p class="font-medium text-lg">
{{ or .Data.Title "N/A" }}
</p>
</div>
<div>
<p class="text-gray-400">Author</p>
<p class="font-medium text-lg">
{{ or .Data.Author "N/A" }}
</p>
</div>
<div>
<p class="text-gray-400">Progress</p>
<p class="font-medium text-lg">
{{ .Data.CurrentPage }} / {{ .Data.TotalPages }} ({{ .Data.Percentage }}%)
</p>
</div>
<div>
<p class="text-gray-400">Time Read</p>
<p class="font-medium text-lg">
{{ .Data.TotalTimeMinutes }} Minutes
</p>
</div>
</div>
<p class="text-gray-400">Description</p>
<p class="font-medium text-justify hyphens-auto">
{{ or .Data.Description "N/A" }}
</p>
</div>
<style>
.css-button:checked + form {
visibility: visible;
opacity: 1;
}
.css-button + form {
visibility: hidden;
opacity: 0;
}
</style>
{{end}}

View File

@ -7,10 +7,37 @@
{{end}} {{end}}
{{define "content"}} {{define "content"}}
<div class="h-full w-full overflow-scroll bg-white shadow-lg dark:bg-gray-700 rounded dark:text-white text-sm 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 mr-4"> <div class="flex flex-col gap-2 float-left mr-4 mb-4 relative">
<label class="z-20 cursor-pointer" for="edit-cover-button">
<img class="rounded w-40 md:w-60 lg:w-80 object-fill h-full" src="../documents/{{.Data.ID}}/cover"></img> <img class="rounded w-40 md:w-60 lg:w-80 object-fill h-full" src="../documents/{{.Data.ID}}/cover"></img>
<div class="flex gap-2 justify-end text-gray-500 dark:text-gray-400"> </label>
<div class="flex relative z-40 gap-2 justify-end text-gray-500 dark:text-gray-400">
<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">
<form
method="POST"
enctype="multipart/form-data"
action="./{{ .Data.ID }}/edit"
class="flex flex-col gap-2 w-72 text-black dark:text-white text-sm"
>
<input
type="file"
id="cover"
name="cover"
>
<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>
</form>
<form
method="POST"
action="./{{ .Data.ID }}/edit"
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>
</form>
</div>
<div class="relative"> <div class="relative">
<label for="delete-button"> <label for="delete-button">
<svg <svg
@ -30,15 +57,17 @@
</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">
<form <form
method="POST" method="POST"
action="./{{ .Data.ID }}/delete" action="./{{ .Data.ID }}/delete"
class="absolute bottom-7 left-5 text-black bg-gray-200 transition-all duration-200 rounded shadow-inner shadow-lg shadow-gray-500 dark:text-white dark:shadow-gray-900 dark:bg-gray-600 text-sm p-3" class="text-black dark:text-white text-sm"
> >
<p class="font-medium w-24 pb-2">Are you sure?</p> <p class="font-medium w-24 pb-2">Are you sure?</p>
<button class="font-medium w-full px-2 py-1 text-white bg-gray-500 dark:text-gray-800 dark:bg-gray-400 hover:bg-gray-800 dark:hover:bg-gray-100" type="submit">Delete</button> <button class="font-medium w-full px-2 py-1 text-white bg-gray-500 dark:text-gray-800 dark:bg-gray-400 hover:bg-gray-800 dark:hover:bg-gray-100" type="submit">Delete</button>
</form> </form>
</div> </div>
</div>
<a href="../activity?document={{ .Data.ID }}"> <a href="../activity?document={{ .Data.ID }}">
<svg <svg
width="24" width="24"
@ -53,7 +82,7 @@
</svg> </svg>
</a> </a>
<div class="relative"> <div class="relative">
<label for="metadata-button"> <label for="edit-button">
<svg <svg
width="24" width="24"
height="24" height="24"
@ -63,23 +92,34 @@
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
fill-rule="evenodd" d="M21.1938 2.80624C22.2687 3.88124 22.2687 5.62415 21.1938 6.69914L20.6982 7.19469C20.5539 7.16345 20.3722 7.11589 20.1651 7.04404C19.6108 6.85172 18.8823 6.48827 18.197 5.803C17.5117 5.11774 17.1483 4.38923 16.956 3.8349C16.8841 3.62781 16.8366 3.44609 16.8053 3.30179L17.3009 2.80624C18.3759 1.73125 20.1188 1.73125 21.1938 2.80624Z"
clip-rule="evenodd" />
d="M12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22ZM12 7.75C11.3787 7.75 10.875 8.25368 10.875 8.875C10.875 9.28921 10.5392 9.625 10.125 9.625C9.71079 9.625 9.375 9.28921 9.375 8.875C9.375 7.42525 10.5503 6.25 12 6.25C13.4497 6.25 14.625 7.42525 14.625 8.875C14.625 9.58584 14.3415 10.232 13.883 10.704C13.7907 10.7989 13.7027 10.8869 13.6187 10.9708C13.4029 11.1864 13.2138 11.3753 13.0479 11.5885C12.8289 11.8699 12.75 12.0768 12.75 12.25V13C12.75 13.4142 12.4142 13.75 12 13.75C11.5858 13.75 11.25 13.4142 11.25 13V12.25C11.25 11.5948 11.555 11.0644 11.8642 10.6672C12.0929 10.3733 12.3804 10.0863 12.6138 9.85346C12.6842 9.78321 12.7496 9.71789 12.807 9.65877C13.0046 9.45543 13.125 9.18004 13.125 8.875C13.125 8.25368 12.6213 7.75 12 7.75ZM12 17C12.5523 17 13 16.5523 13 16C13 15.4477 12.5523 15 12 15C11.4477 15 11 15.4477 11 16C11 16.5523 11.4477 17 12 17Z" <path
d="M14.5801 13.3128C14.1761 13.7168 13.9741 13.9188 13.7513 14.0926C13.4886 14.2975 13.2043 14.4732 12.9035 14.6166C12.6485 14.7381 12.3775 14.8284 11.8354 15.0091L8.97709 15.9619C8.71035 16.0508 8.41626 15.9814 8.21744 15.7826C8.01862 15.5837 7.9492 15.2897 8.03811 15.0229L8.99089 12.1646C9.17157 11.6225 9.26191 11.3515 9.38344 11.0965C9.52679 10.7957 9.70249 10.5114 9.90743 10.2487C10.0812 10.0259 10.2832 9.82394 10.6872 9.41993L15.6033 4.50385C15.867 5.19804 16.3293 6.05663 17.1363 6.86366C17.9434 7.67069 18.802 8.13296 19.4962 8.39674L14.5801 13.3128Z"
/>
<path
d="M20.5355 20.5355C22 19.0711 22 16.714 22 12C22 10.4517 22 9.15774 21.9481 8.0661L15.586 14.4283C15.2347 14.7797 14.9708 15.0437 14.6738 15.2753C14.3252 15.5473 13.948 15.7804 13.5488 15.9706C13.2088 16.1327 12.8546 16.2506 12.3833 16.4076L9.45143 17.3849C8.64568 17.6535 7.75734 17.4438 7.15678 16.8432C6.55621 16.2427 6.34651 15.3543 6.61509 14.5486L7.59235 11.6167C7.74936 11.1454 7.86732 10.7912 8.02935 10.4512C8.21958 10.052 8.45272 9.6748 8.72466 9.32615C8.9563 9.02918 9.22032 8.76528 9.57173 8.41404L15.9339 2.05188C14.8423 2 13.5483 2 12 2C7.28595 2 4.92893 2 3.46447 3.46447C2 4.92893 2 7.28595 2 12C2 16.714 2 19.0711 3.46447 20.5355C4.92893 22 7.28595 22 12 22C16.714 22 19.0711 22 20.5355 20.5355Z"
/> />
</svg> </svg>
</label> </label>
<input type="checkbox" id="metadata-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">
<form <form
method="POST" method="POST"
action="./{{ .Data.ID }}/identify" action="./{{ .Data.ID }}/edit"
class="absolute bottom-7 left-5 text-black bg-gray-200 transition-all duration-200 rounded shadow-inner shadow-lg shadow-gray-500 dark:text-white dark:shadow-gray-900 dark:bg-gray-600 text-sm p-3" class="text-black dark:text-white text-sm"
> >
<label class="font-medium" for="isbn">ISBN</label> <label class="font-medium" for="isbn">ISBN</label>
<input class="mt-1 mb-2 p-1 bg-gray-400 text-black dark:bg-gray-700 dark:text-white" type="text" id="isbn" name="isbn"><br> <input
type="text"
id="isbn"
name="ISBN"
class="mt-1 mb-2 p-2 bg-gray-400 text-black dark:bg-gray-700 dark:text-white"
><br>
<button class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 dark:bg-gray-400 hover:bg-gray-800 dark:hover:bg-gray-100" type="submit">Search Metadata</button> <button class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 dark:bg-gray-400 hover:bg-gray-800 dark:hover:bg-gray-100" type="submit">Search Metadata</button>
</form> </form>
</div> </div>
</div>
{{ if .Data.Filepath }} {{ if .Data.Filepath }}
<a href="./{{.Data.ID}}/file"> <a href="./{{.Data.ID}}/file">
<svg <svg
@ -116,44 +156,180 @@
</div> </div>
</div> </div>
<div class="flex flex-wrap justify-between gap-4 pb-4"> <div class="flex flex-wrap justify-between gap-4 pb-4">
<div> <div class="relative">
<p class="text-gray-400">Title</p> <div class="text-gray-400 inline-flex gap-2 relative">
<!-- <input type="text" class="font-medium bg-transparent" value="{{ or .Data.Title "Unknown" }}"/> --> <p>Title</p>
<p class="font-medium"> <label class="my-auto" for="edit-title-button">
<svg
width="18"
height="18"
class="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M21.1938 2.80624C22.2687 3.88124 22.2687 5.62415 21.1938 6.69914L20.6982 7.19469C20.5539 7.16345 20.3722 7.11589 20.1651 7.04404C19.6108 6.85172 18.8823 6.48827 18.197 5.803C17.5117 5.11774 17.1483 4.38923 16.956 3.8349C16.8841 3.62781 16.8366 3.44609 16.8053 3.30179L17.3009 2.80624C18.3759 1.73125 20.1188 1.73125 21.1938 2.80624Z"
/>
<path
d="M14.5801 13.3128C14.1761 13.7168 13.9741 13.9188 13.7513 14.0926C13.4886 14.2975 13.2043 14.4732 12.9035 14.6166C12.6485 14.7381 12.3775 14.8284 11.8354 15.0091L8.97709 15.9619C8.71035 16.0508 8.41626 15.9814 8.21744 15.7826C8.01862 15.5837 7.9492 15.2897 8.03811 15.0229L8.99089 12.1646C9.17157 11.6225 9.26191 11.3515 9.38344 11.0965C9.52679 10.7957 9.70249 10.5114 9.90743 10.2487C10.0812 10.0259 10.2832 9.82394 10.6872 9.41993L15.6033 4.50385C15.867 5.19804 16.3293 6.05663 17.1363 6.86366C17.9434 7.67069 18.802 8.13296 19.4962 8.39674L14.5801 13.3128Z"
/>
<path
d="M20.5355 20.5355C22 19.0711 22 16.714 22 12C22 10.4517 22 9.15774 21.9481 8.0661L15.586 14.4283C15.2347 14.7797 14.9708 15.0437 14.6738 15.2753C14.3252 15.5473 13.948 15.7804 13.5488 15.9706C13.2088 16.1327 12.8546 16.2506 12.3833 16.4076L9.45143 17.3849C8.64568 17.6535 7.75734 17.4438 7.15678 16.8432C6.55621 16.2427 6.34651 15.3543 6.61509 14.5486L7.59235 11.6167C7.74936 11.1454 7.86732 10.7912 8.02935 10.4512C8.21958 10.052 8.45272 9.6748 8.72466 9.32615C8.9563 9.02918 9.22032 8.76528 9.57173 8.41404L15.9339 2.05188C14.8423 2 13.5483 2 12 2C7.28595 2 4.92893 2 3.46447 3.46447C2 4.92893 2 7.28595 2 12C2 16.714 2 19.0711 3.46447 20.5355C4.92893 22 7.28595 22 12 22C16.714 22 19.0711 22 20.5355 20.5355Z"
/>
</svg>
</label>
<input type="checkbox" id="edit-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">
<form
method="POST"
action="./{{ .Data.ID }}/edit"
class="flex flex-col gap-2 text-black dark:text-white text-sm"
>
<input
type="text"
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"
>
<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>
</form>
</div>
</div>
<p class="font-medium text-lg">
{{ or .Data.Title "N/A" }} {{ or .Data.Title "N/A" }}
</p> </p>
</div> </div>
<div> <div class="relative">
<p class="text-gray-400">Author</p> <div class="text-gray-400 inline-flex gap-2 relative">
<p class="font-medium"> <p>Author</p>
<label class="my-auto" for="edit-author-button">
<svg
width="18"
height="18"
class="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M21.1938 2.80624C22.2687 3.88124 22.2687 5.62415 21.1938 6.69914L20.6982 7.19469C20.5539 7.16345 20.3722 7.11589 20.1651 7.04404C19.6108 6.85172 18.8823 6.48827 18.197 5.803C17.5117 5.11774 17.1483 4.38923 16.956 3.8349C16.8841 3.62781 16.8366 3.44609 16.8053 3.30179L17.3009 2.80624C18.3759 1.73125 20.1188 1.73125 21.1938 2.80624Z"
/>
<path
d="M14.5801 13.3128C14.1761 13.7168 13.9741 13.9188 13.7513 14.0926C13.4886 14.2975 13.2043 14.4732 12.9035 14.6166C12.6485 14.7381 12.3775 14.8284 11.8354 15.0091L8.97709 15.9619C8.71035 16.0508 8.41626 15.9814 8.21744 15.7826C8.01862 15.5837 7.9492 15.2897 8.03811 15.0229L8.99089 12.1646C9.17157 11.6225 9.26191 11.3515 9.38344 11.0965C9.52679 10.7957 9.70249 10.5114 9.90743 10.2487C10.0812 10.0259 10.2832 9.82394 10.6872 9.41993L15.6033 4.50385C15.867 5.19804 16.3293 6.05663 17.1363 6.86366C17.9434 7.67069 18.802 8.13296 19.4962 8.39674L14.5801 13.3128Z"
/>
<path
d="M20.5355 20.5355C22 19.0711 22 16.714 22 12C22 10.4517 22 9.15774 21.9481 8.0661L15.586 14.4283C15.2347 14.7797 14.9708 15.0437 14.6738 15.2753C14.3252 15.5473 13.948 15.7804 13.5488 15.9706C13.2088 16.1327 12.8546 16.2506 12.3833 16.4076L9.45143 17.3849C8.64568 17.6535 7.75734 17.4438 7.15678 16.8432C6.55621 16.2427 6.34651 15.3543 6.61509 14.5486L7.59235 11.6167C7.74936 11.1454 7.86732 10.7912 8.02935 10.4512C8.21958 10.052 8.45272 9.6748 8.72466 9.32615C8.9563 9.02918 9.22032 8.76528 9.57173 8.41404L15.9339 2.05188C14.8423 2 13.5483 2 12 2C7.28595 2 4.92893 2 3.46447 3.46447C2 4.92893 2 7.28595 2 12C2 16.714 2 19.0711 3.46447 20.5355C4.92893 22 7.28595 22 12 22C16.714 22 19.0711 22 20.5355 20.5355Z"
/>
</svg>
</label>
<input type="checkbox" id="edit-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">
<form
method="POST"
action="./{{ .Data.ID }}/edit"
class="flex flex-col gap-2 text-black dark:text-white text-sm"
>
<input
type="text"
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"
>
<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>
</form>
</div>
</div>
<p class="font-medium text-lg">
{{ or .Data.Author "N/A" }} {{ or .Data.Author "N/A" }}
</p> </p>
</div> </div>
<div> <div>
<p class="text-gray-400">Progress</p> <p class="text-gray-400">Time Read</p>
<p class="font-medium"> <p class="font-medium text-lg">
{{ .Data.CurrentPage }} / {{ .Data.TotalPages }} ({{ .Data.Percentage }}%)
</p>
</div>
<div>
<p class="text-gray-400">Minutes Read</p>
<p class="font-medium">
{{ .Data.TotalTimeMinutes }} Minutes {{ .Data.TotalTimeMinutes }} Minutes
</p> </p>
</div> </div>
</div> <div>
<p class="text-gray-400">Description</p> <p class="text-gray-400">Progress</p>
<p class="font-medium text-justify hyphens-auto"> <p class="font-medium text-lg">
{{ or .Data.Description "N/A" }} {{ .Data.CurrentPage }} / {{ .Data.TotalPages }} ({{ .Data.Percentage }}%)
</p> </p>
</div> </div>
<!--
<div>
<p class="text-gray-400">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="font-medium text-lg">
{{ or .Data.Isbn13 "N/A" }}
</p>
</div>
-->
</div>
<div class="relative">
<div class="text-gray-400 inline-flex gap-2 relative">
<p>Description</p>
<label class="my-auto" for="edit-description-button">
<svg
width="18"
height="18"
class="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M21.1938 2.80624C22.2687 3.88124 22.2687 5.62415 21.1938 6.69914L20.6982 7.19469C20.5539 7.16345 20.3722 7.11589 20.1651 7.04404C19.6108 6.85172 18.8823 6.48827 18.197 5.803C17.5117 5.11774 17.1483 4.38923 16.956 3.8349C16.8841 3.62781 16.8366 3.44609 16.8053 3.30179L17.3009 2.80624C18.3759 1.73125 20.1188 1.73125 21.1938 2.80624Z"
/>
<path
d="M14.5801 13.3128C14.1761 13.7168 13.9741 13.9188 13.7513 14.0926C13.4886 14.2975 13.2043 14.4732 12.9035 14.6166C12.6485 14.7381 12.3775 14.8284 11.8354 15.0091L8.97709 15.9619C8.71035 16.0508 8.41626 15.9814 8.21744 15.7826C8.01862 15.5837 7.9492 15.2897 8.03811 15.0229L8.99089 12.1646C9.17157 11.6225 9.26191 11.3515 9.38344 11.0965C9.52679 10.7957 9.70249 10.5114 9.90743 10.2487C10.0812 10.0259 10.2832 9.82394 10.6872 9.41993L15.6033 4.50385C15.867 5.19804 16.3293 6.05663 17.1363 6.86366C17.9434 7.67069 18.802 8.13296 19.4962 8.39674L14.5801 13.3128Z"
/>
<path
d="M20.5355 20.5355C22 19.0711 22 16.714 22 12C22 10.4517 22 9.15774 21.9481 8.0661L15.586 14.4283C15.2347 14.7797 14.9708 15.0437 14.6738 15.2753C14.3252 15.5473 13.948 15.7804 13.5488 15.9706C13.2088 16.1327 12.8546 16.2506 12.3833 16.4076L9.45143 17.3849C8.64568 17.6535 7.75734 17.4438 7.15678 16.8432C6.55621 16.2427 6.34651 15.3543 6.61509 14.5486L7.59235 11.6167C7.74936 11.1454 7.86732 10.7912 8.02935 10.4512C8.21958 10.052 8.45272 9.6748 8.72466 9.32615C8.9563 9.02918 9.22032 8.76528 9.57173 8.41404L15.9339 2.05188C14.8423 2 13.5483 2 12 2C7.28595 2 4.92893 2 3.46447 3.46447C2 4.92893 2 7.28595 2 12C2 16.714 2 19.0711 3.46447 20.5355C4.92893 22 7.28595 22 12 22C16.714 22 19.0711 22 20.5355 20.5355Z"
/>
</svg>
</label>
</div>
</div>
<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"
>
<img class="hidden md:block invisible rounded w-40 md:w-60 lg:w-80 object-fill" src="../documents/{{.Data.ID}}/cover"></img>
<form
method="POST"
action="./{{ .Data.ID }}/edit"
class="flex flex-col gap-2 w-full text-black bg-gray-200 rounded shadow-lg shadow-gray-500 dark:text-white dark:shadow-gray-900 dark:bg-gray-600 text-sm p-3"
>
<textarea
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"
>{{ 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>
</form>
</div>
<p>{{ or .Data.Description "N/A" }}</p>
</div>
</div>
<style> <style>
.css-button:checked + form { .css-button:checked + div {
visibility: visible; visibility: visible;
opacity: 1; opacity: 1;
} }
.css-button + form { .css-button + div {
visibility: hidden; visibility: hidden;
opacity: 0; opacity: 0;
} }

View File

@ -43,7 +43,7 @@
</div> </div>
<div class="inline-flex shrink-0 items-center"> <div class="inline-flex shrink-0 items-center">
<div> <div>
<p class="text-gray-400">Minutes Read</p> <p class="text-gray-400">Time Read</p>
<p class="font-medium"> <p class="font-medium">
{{ $doc.TotalTimeMinutes }} Minutes {{ $doc.TotalTimeMinutes }} Minutes
</p> </p>