[add] editing, deletion, metadata recording
This commit is contained in:
parent
3150c89303
commit
c22154ed77
6
Makefile
6
Makefile
@ -1,8 +1,12 @@
|
|||||||
build_local:
|
build_local:
|
||||||
|
go mod download
|
||||||
|
rm -r ./build
|
||||||
mkdir -p ./build
|
mkdir -p ./build
|
||||||
cp -a ./templates ./build/templates
|
cp -a ./templates ./build/templates
|
||||||
cp -a ./assets ./build/assets
|
cp -a ./assets ./build/assets
|
||||||
CGO_ENABLED=1 CGO_CFLAGS="-D_LARGEFILE64_SOURCE" go build -o ./build/server
|
|
||||||
|
env GOOS=linux GOARCH=amd64 CGO_ENABLED=1 CC="zig cc -target x86_64-linux" CXX="zig c++ -target x86_64-linux" go build -o ./build/server_linux_x86_64
|
||||||
|
env GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 CGO_CFLAGS="-D_LARGEFILE64_SOURCE" go build -o ./build/server_darwin_arm64
|
||||||
|
|
||||||
docker_build_local:
|
docker_build_local:
|
||||||
docker build -t bookmanager:latest .
|
docker build -t bookmanager:latest .
|
||||||
|
@ -86,3 +86,4 @@ CGO_ENABLED=1 CGO_CFLAGS="-D_LARGEFILE64_SOURCE" go build -o /bookmanager cmd/ma
|
|||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Icons: https://www.svgrepo.com/collection/solar-bold-icons
|
- Icons: https://www.svgrepo.com/collection/solar-bold-icons
|
||||||
|
- Icons: https://www.svgrepo.com/collection/scarlab-solid-oval-interface-icons/
|
||||||
|
@ -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,
|
||||||
@ -99,6 +99,9 @@ func (api *API) registerWebAppRoutes() {
|
|||||||
api.Router.GET("/documents/:document", api.authWebAppMiddleware, api.createAppResourcesRoute("document"))
|
api.Router.GET("/documents/:document", api.authWebAppMiddleware, api.createAppResourcesRoute("document"))
|
||||||
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)
|
||||||
|
api.Router.POST("/documents/:document/edit", api.authWebAppMiddleware, api.editDocument)
|
||||||
|
api.Router.POST("/documents/:document/identify", api.authWebAppMiddleware, api.identifyDocument)
|
||||||
|
api.Router.POST("/documents/:document/delete", api.authWebAppMiddleware, api.deleteDocument)
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
api.Router.GET("/graphs", api.authWebAppMiddleware, baseResourceRoute("graphs"))
|
api.Router.GET("/graphs", api.authWebAppMiddleware, baseResourceRoute("graphs"))
|
||||||
|
@ -2,17 +2,35 @@ 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type requestDocumentIdentify struct {
|
||||||
|
Title *string `form:"title"`
|
||||||
|
Author *string `form:"author"`
|
||||||
|
ISBN *string `form:"isbn"`
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
||||||
@ -164,19 +182,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 +203,232 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *API) editDocument(c *gin.Context) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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, "../")
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
var rDocIdentify requestDocumentIdentify
|
||||||
|
if err := c.ShouldBind(&rDocIdentify); err != nil {
|
||||||
|
log.Error("[identifyDocument] Invalid Form Bind")
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disallow Empty Strings
|
||||||
|
if rDocIdentify.Title != nil && strings.TrimSpace(*rDocIdentify.Title) == "" {
|
||||||
|
rDocIdentify.Title = nil
|
||||||
|
}
|
||||||
|
if rDocIdentify.Author != nil && strings.TrimSpace(*rDocIdentify.Author) == "" {
|
||||||
|
rDocIdentify.Author = nil
|
||||||
|
}
|
||||||
|
if rDocIdentify.ISBN != nil && strings.TrimSpace(*rDocIdentify.ISBN) == "" {
|
||||||
|
rDocIdentify.ISBN = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate Values
|
||||||
|
if rDocIdentify.ISBN == nil && rDocIdentify.Title == nil && rDocIdentify.Author == nil {
|
||||||
|
log.Error("[identifyDocument] Invalid Form")
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataResults, err := metadata.GetMetadata(metadata.MetadataInfo{
|
||||||
|
Title: rDocIdentify.Title,
|
||||||
|
Author: rDocIdentify.Author,
|
||||||
|
ISBN10: rDocIdentify.ISBN,
|
||||||
|
ISBN13: rDocIdentify.ISBN,
|
||||||
|
})
|
||||||
|
if err != nil || len(metadataResults) == 0 {
|
||||||
|
log.Error("[identifyDocument] Metadata Error")
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Metadata Error"})
|
||||||
|
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, "/")
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"`
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -8,6 +8,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@ -17,7 +19,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 {
|
||||||
@ -42,77 +46,117 @@ type gBooksQueryResponse struct {
|
|||||||
Items []gBooksQueryItem `json:"items"`
|
Items []gBooksQueryItem `json:"items"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const GBOOKS_QUERY_URL string = "https://www.googleapis.com/books/v1/volumes?q=%s&filter=ebooks&download=epub"
|
const GBOOKS_QUERY_URL string = "https://www.googleapis.com/books/v1/volumes?q=%s"
|
||||||
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
|
|
||||||
} else if len(data.ISBN) > 0 {
|
queryResults = []gBooksQueryItem{*resp}
|
||||||
searchQuery := "isbn:" + *data.ISBN[0]
|
} else if metadataSearch.ISBN13 != nil {
|
||||||
|
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]
|
|
||||||
} else if data.Title != nil && data.Author != nil {
|
queryResults = resp.Items
|
||||||
searchQuery := url.QueryEscape(fmt.Sprintf("%s %s", *data.Title, *data.Author))
|
} else if metadataSearch.ISBN10 != nil {
|
||||||
|
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 {
|
||||||
|
var searchQuery string
|
||||||
|
if metadataSearch.Title != nil {
|
||||||
|
searchQuery = searchQuery + *metadataSearch.Title
|
||||||
|
}
|
||||||
|
if metadataSearch.Author != nil {
|
||||||
|
searchQuery = searchQuery + " " + *metadataSearch.Author
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape & Trim
|
||||||
|
searchQuery = url.QueryEscape(strings.TrimSpace(searchQuery))
|
||||||
|
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
|
for i := range queryResults {
|
||||||
data.Title = &queryResult.Info.Title
|
item := queryResults[i] // Range Value Pointer Issue
|
||||||
if len(queryResult.Info.Authors) > 0 {
|
itemResult := MetadataInfo{
|
||||||
data.Author = &queryResult.Info.Authors[0]
|
GBID: &item.ID,
|
||||||
}
|
Title: &item.Info.Title,
|
||||||
for _, item := range queryResult.Info.Identifiers {
|
Description: &item.Info.Description,
|
||||||
if item.Type == "ISBN_10" || item.Type == "ISBN_13" {
|
|
||||||
data.ISBN = append(data.ISBN, &item.Identifier)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(item.Info.Authors) > 0 {
|
||||||
|
itemResult.Author = &item.Info.Authors[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
for i := range item.Info.Identifiers {
|
||||||
|
item := item.Info.Identifiers[i] // Range Value Pointer Issue
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func SaveCover(id string, safePath string) error {
|
allMetadata = append(allMetadata, itemResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 +165,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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
5
scripts/build.sh
Executable file
5
scripts/build.sh
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
env GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CGO_CFLAGS="-D_LARGEFILE64_SOURCE" go build -o ./build/server_linux_arm64
|
||||||
|
env GOOS=linux GOARCH=amd64 CGO_ENABLED=1 CGO_CFLAGS="-D_LARGEFILE64_SOURCE" go build -o ./build/server_linux_amd64
|
||||||
|
# env GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 go build -o ./build/server_darwin_amd64
|
||||||
|
# env GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 go build -o ./build/server_darwin_arm64
|
@ -3,6 +3,7 @@
|
|||||||
pkgs.mkShell {
|
pkgs.mkShell {
|
||||||
packages = with pkgs; [
|
packages = with pkgs; [
|
||||||
go
|
go
|
||||||
|
zig
|
||||||
nodejs_20
|
nodejs_20
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
45
sqlc.yaml
45
sqlc.yaml
@ -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"
|
||||||
|
166
templates/document-edit.html
Normal file
166
templates/document-edit.html
Normal 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}}
|
@ -7,15 +7,57 @@
|
|||||||
{{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 w-40 md:w-60 lg:w-80 mr-4 mb-2 relative">
|
||||||
<img class="rounded w-40 md:w-60 lg:w-80 object-fill h-full" src="../documents/{{.Data.ID}}/cover"></img>
|
<label class="z-20 cursor-pointer" for="edit-cover-button">
|
||||||
<div class="flex gap-2 justify-end text-gray-500 dark:text-gray-400">
|
<img class="rounded object-fill h-full" src="../documents/{{.Data.ID}}/cover"></img>
|
||||||
|
</label>
|
||||||
|
<div class="flex flex-wrap-reverse justify-between z-40 gap-2 relative">
|
||||||
|
<div class="mr-2 min-w-[50%]">
|
||||||
|
<div class="flex gap-1 text-sm">
|
||||||
|
<p class="text-gray-400">ISBN 10:</p>
|
||||||
|
<p class="font-medium">
|
||||||
|
{{ or .Data.Isbn10 "N/A" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-1 text-sm">
|
||||||
|
<p class="text-gray-400">ISBN 13:</p>
|
||||||
|
<p class="font-medium">
|
||||||
|
{{ or .Data.Isbn13 "N/A" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex grow justify-between my-auto text-gray-500 dark:text-gray-400">
|
||||||
|
<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
|
||||||
width="24"
|
width="28"
|
||||||
height="24"
|
height="28"
|
||||||
class="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100"
|
class="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
@ -30,19 +72,20 @@
|
|||||||
</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>
|
<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-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="28"
|
||||||
height="24"
|
height="28"
|
||||||
class="hover:text-gray-800 dark:hover:text-gray-100"
|
class="hover:text-gray-800 dark:hover:text-gray-100"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
@ -53,10 +96,10 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<label for="metadata-button">
|
<label for="edit-button">
|
||||||
<svg
|
<svg
|
||||||
width="24"
|
width="28"
|
||||||
height="24"
|
height="28"
|
||||||
class="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100"
|
class="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
@ -65,26 +108,47 @@
|
|||||||
<path
|
<path
|
||||||
fill-rule="evenodd"
|
fill-rule="evenodd"
|
||||||
clip-rule="evenodd"
|
clip-rule="evenodd"
|
||||||
d="M12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22ZM12 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"
|
d="M2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12ZM9 11.5C9 10.1193 10.1193 9 11.5 9C12.8807 9 14 10.1193 14 11.5C14 12.8807 12.8807 14 11.5 14C10.1193 14 9 12.8807 9 11.5ZM11.5 7C9.01472 7 7 9.01472 7 11.5C7 13.9853 9.01472 16 11.5 16C12.3805 16 13.202 15.7471 13.8957 15.31L15.2929 16.7071C15.6834 17.0976 16.3166 17.0976 16.7071 16.7071C17.0976 16.3166 17.0976 15.6834 16.7071 15.2929L15.31 13.8957C15.7471 13.202 16 12.3805 16 11.5C16 9.01472 13.9853 7 11.5 7Z"
|
||||||
/>
|
/>
|
||||||
</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 }}/identify"
|
||||||
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="flex flex-col gap-2 text-black dark:text-white text-sm"
|
||||||
>
|
>
|
||||||
<label class="font-medium" for="isbn">ISBN</label>
|
<input
|
||||||
<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>
|
type="text"
|
||||||
<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>
|
id="title"
|
||||||
|
name="title"
|
||||||
|
placeholder="Title"
|
||||||
|
class="p-2 bg-gray-400 text-black dark:bg-gray-700 dark:text-white"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="author"
|
||||||
|
name="author"
|
||||||
|
placeholder="Author"
|
||||||
|
class="p-2 bg-gray-400 text-black dark:bg-gray-700 dark:text-white"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="isbn"
|
||||||
|
name="isbn"
|
||||||
|
placeholder="ISBN 10 / ISBN 13"
|
||||||
|
class="p-2 bg-gray-400 text-black dark:bg-gray-700 dark:text-white"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{{ if .Data.Filepath }}
|
{{ if .Data.Filepath }}
|
||||||
<a href="./{{.Data.ID}}/file">
|
<a href="./{{.Data.ID}}/file">
|
||||||
<svg
|
<svg
|
||||||
width="24"
|
width="28"
|
||||||
height="24"
|
height="28"
|
||||||
class="hover:text-gray-800 dark:hover:text-gray-100"
|
class="hover:text-gray-800 dark:hover:text-gray-100"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
@ -99,8 +163,8 @@
|
|||||||
</a>
|
</a>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<svg
|
<svg
|
||||||
width="24"
|
width="28"
|
||||||
height="24"
|
height="28"
|
||||||
class="text-gray-200 dark:text-gray-600"
|
class="text-gray-200 dark:text-gray-600"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
@ -115,45 +179,182 @@
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap justify-between gap-4 pb-4">
|
</div>
|
||||||
<div>
|
<div class="grid grid-cols-2 justify-between gap-4 pb-4">
|
||||||
<p class="text-gray-400">Title</p>
|
<div class="relative">
|
||||||
<!-- <input type="text" class="font-medium bg-transparent" value="{{ or .Data.Title "Unknown" }}"/> -->
|
<div class="text-gray-400 inline-flex gap-2 relative">
|
||||||
<p class="font-medium">
|
<p>Title</p>
|
||||||
|
<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;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user