[add] better error handling, [add] font selector, [add] tailwind generation
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
3577dd89a0
commit
cdec621043
15
Makefile
15
Makefile
@ -1,4 +1,4 @@
|
||||
build_local:
|
||||
build_local: build_tailwind
|
||||
go mod download
|
||||
rm -r ./build
|
||||
mkdir -p ./build
|
||||
@ -10,17 +10,17 @@ build_local:
|
||||
env GOOS=darwin GOARCH=arm64 go build -o ./build/server_darwin_arm64
|
||||
env GOOS=darwin GOARCH=amd64 go build -o ./build/server_darwin_amd64
|
||||
|
||||
docker_build_local:
|
||||
docker_build_local: build_tailwind
|
||||
docker build -t bookmanager:latest .
|
||||
|
||||
docker_build_release_dev:
|
||||
docker_build_release_dev: build_tailwind
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
-t gitea.va.reichard.io/evan/bookmanager:dev \
|
||||
-f Dockerfile-BuildKit \
|
||||
--push .
|
||||
|
||||
docker_build_release_latest:
|
||||
docker_build_release_latest: build_tailwind
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
-t gitea.va.reichard.io/evan/bookmanager:latest \
|
||||
@ -28,6 +28,13 @@ docker_build_release_latest:
|
||||
-f Dockerfile-BuildKit \
|
||||
--push .
|
||||
|
||||
build_tailwind:
|
||||
tailwind build -o ./assets/style.css
|
||||
|
||||
|
||||
clean:
|
||||
rm -rf ./build
|
||||
|
||||
tests_integration:
|
||||
go test -v -tags=integration -coverpkg=./... ./metadata
|
||||
|
||||
|
10
api/api.go
10
api/api.go
@ -78,6 +78,7 @@ func (api *API) registerWebAppRoutes() {
|
||||
"NiceSeconds": niceSeconds,
|
||||
}
|
||||
|
||||
render.AddFromFiles("error", "templates/error.html")
|
||||
render.AddFromFilesFuncs("login", helperFuncs, "templates/login.html")
|
||||
render.AddFromFilesFuncs("reader", helperFuncs, "templates/reader-base.html", "templates/reader.html")
|
||||
render.AddFromFilesFuncs("home", helperFuncs, "templates/base.html", "templates/home.html")
|
||||
@ -101,9 +102,10 @@ func (api *API) registerWebAppRoutes() {
|
||||
api.Router.POST("/settings", api.authWebAppMiddleware, api.editSettings)
|
||||
api.Router.GET("/activity", api.authWebAppMiddleware, api.createAppResourcesRoute("activity"))
|
||||
api.Router.GET("/documents", api.authWebAppMiddleware, api.createAppResourcesRoute("documents"))
|
||||
api.Router.POST("/documents", api.authWebAppMiddleware, api.uploadNewDocument)
|
||||
api.Router.GET("/documents/:document", api.authWebAppMiddleware, api.createAppResourcesRoute("document"))
|
||||
api.Router.GET("/documents/:document/reader", api.authWebAppMiddleware, api.documentReader)
|
||||
api.Router.GET("/documents/:document/file", api.authWebAppMiddleware, api.downloadDocumentFile)
|
||||
api.Router.GET("/documents/:document/file", api.authWebAppMiddleware, api.downloadDocument)
|
||||
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)
|
||||
@ -127,8 +129,8 @@ func (api *API) registerKOAPIRoutes(apiGroup *gin.RouterGroup) {
|
||||
|
||||
koGroup.POST("/documents", api.authKOMiddleware, api.addDocuments)
|
||||
koGroup.POST("/syncs/documents", api.authKOMiddleware, api.checkDocumentsSync)
|
||||
koGroup.PUT("/documents/:document/file", api.authKOMiddleware, api.uploadDocumentFile)
|
||||
koGroup.GET("/documents/:document/file", api.authKOMiddleware, api.downloadDocumentFile)
|
||||
koGroup.PUT("/documents/:document/file", api.authKOMiddleware, api.uploadExistingDocument)
|
||||
koGroup.GET("/documents/:document/file", api.authKOMiddleware, api.downloadDocument)
|
||||
|
||||
koGroup.POST("/activity", api.authKOMiddleware, api.addActivities)
|
||||
koGroup.POST("/syncs/activity", api.authKOMiddleware, api.checkActivitySync)
|
||||
@ -139,7 +141,7 @@ func (api *API) registerOPDSRoutes(apiGroup *gin.RouterGroup) {
|
||||
|
||||
opdsGroup.GET("/", api.authOPDSMiddleware, api.opdsDocuments)
|
||||
opdsGroup.GET("/search.xml", api.authOPDSMiddleware, api.opdsSearchDescription)
|
||||
opdsGroup.GET("/documents/:document/file", api.authOPDSMiddleware, api.downloadDocumentFile)
|
||||
opdsGroup.GET("/documents/:document/file", api.authOPDSMiddleware, api.downloadDocument)
|
||||
opdsGroup.GET("/documents/:document/cover", api.authOPDSMiddleware, api.getDocumentCover)
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,7 @@ import (
|
||||
"reichard.io/bbank/database"
|
||||
"reichard.io/bbank/metadata"
|
||||
"reichard.io/bbank/search"
|
||||
"reichard.io/bbank/utils"
|
||||
)
|
||||
|
||||
type queryParams struct {
|
||||
@ -32,6 +33,10 @@ type searchParams struct {
|
||||
BookType *string `form:"book_type"`
|
||||
}
|
||||
|
||||
type requestDocumentUpload struct {
|
||||
DocumentFile *multipart.FileHeader `form:"document_file"`
|
||||
}
|
||||
|
||||
type requestDocumentEdit struct {
|
||||
Title *string `form:"title"`
|
||||
Author *string `form:"author"`
|
||||
@ -100,7 +105,7 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("[createAppResourcesRoute] GetDocumentsWithStats DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocumentsWithStats DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
@ -113,7 +118,7 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any
|
||||
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"})
|
||||
errorPage(c, http.StatusNotFound, "Invalid document.")
|
||||
return
|
||||
}
|
||||
|
||||
@ -123,11 +128,10 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("[createAppResourcesRoute] GetDocumentWithStats DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocumentsWithStats DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
templateVars["RelBase"] = "../"
|
||||
templateVars["Data"] = document
|
||||
templateVars["TotalTimeLeftSeconds"] = (document.Pages - document.Page) * document.SecondsPerPage
|
||||
} else if routeName == "activity" {
|
||||
@ -145,7 +149,7 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any
|
||||
activity, err := api.DB.Queries.GetActivity(api.DB.Ctx, activityFilter)
|
||||
if err != nil {
|
||||
log.Error("[createAppResourcesRoute] GetActivity DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetActivity DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
@ -172,14 +176,14 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any
|
||||
user, err := api.DB.Queries.GetUser(api.DB.Ctx, userID)
|
||||
if err != nil {
|
||||
log.Error("[createAppResourcesRoute] GetUser DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUser DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
devices, err := api.DB.Queries.GetDevices(api.DB.Ctx, userID)
|
||||
if err != nil {
|
||||
log.Error("[createAppResourcesRoute] GetDevices DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDevices DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
@ -221,7 +225,7 @@ func (api *API) getDocumentCover(c *gin.Context) {
|
||||
var rDoc requestDocumentID
|
||||
if err := c.ShouldBindUri(&rDoc); err != nil {
|
||||
log.Error("[getDocumentCover] Invalid URI Bind")
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
errorPage(c, http.StatusNotFound, "Invalid cover.")
|
||||
return
|
||||
}
|
||||
|
||||
@ -229,7 +233,7 @@ func (api *API) getDocumentCover(c *gin.Context) {
|
||||
document, err := api.DB.Queries.GetDocument(api.DB.Ctx, rDoc.DocumentID)
|
||||
if err != nil {
|
||||
log.Error("[getDocumentCover] GetDocument DB Error:", err)
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocument DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
@ -314,7 +318,7 @@ func (api *API) documentReader(c *gin.Context) {
|
||||
var rDoc requestDocumentID
|
||||
if err := c.ShouldBindUri(&rDoc); err != nil {
|
||||
log.Error("[documentReader] Invalid URI Bind")
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
errorPage(c, http.StatusNotFound, "Invalid document.")
|
||||
return
|
||||
}
|
||||
|
||||
@ -325,7 +329,7 @@ func (api *API) documentReader(c *gin.Context) {
|
||||
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
log.Error("[documentReader] UpsertDocument DB Error:", err)
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("UpsertDocument DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
@ -335,7 +339,7 @@ func (api *API) documentReader(c *gin.Context) {
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("[documentReader] GetDocumentWithStats DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocumentWithStats DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
@ -343,22 +347,152 @@ func (api *API) documentReader(c *gin.Context) {
|
||||
"SearchEnabled": api.Config.SearchEnabled,
|
||||
"Progress": progress.Progress,
|
||||
"Data": document,
|
||||
"RelBase": "../../",
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) uploadNewDocument(c *gin.Context) {
|
||||
var rDocUpload requestDocumentUpload
|
||||
if err := c.ShouldBind(&rDocUpload); err != nil {
|
||||
log.Error("[uploadNewDocument] Invalid Form Bind")
|
||||
errorPage(c, http.StatusBadRequest, "Invalid or missing form values.")
|
||||
return
|
||||
}
|
||||
|
||||
if rDocUpload.DocumentFile == nil {
|
||||
c.Redirect(http.StatusFound, "./documents")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate Type & Derive Extension on MIME
|
||||
uploadedFile, err := rDocUpload.DocumentFile.Open()
|
||||
if err != nil {
|
||||
log.Error("[uploadNewDocument] File Error: ", err)
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to open file.")
|
||||
return
|
||||
}
|
||||
|
||||
fileMime, err := mimetype.DetectReader(uploadedFile)
|
||||
if err != nil {
|
||||
log.Error("[uploadNewDocument] MIME Error")
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to detect filetype.")
|
||||
return
|
||||
}
|
||||
fileExtension := fileMime.Extension()
|
||||
|
||||
// Validate Extension
|
||||
if !slices.Contains([]string{".epub"}, fileExtension) {
|
||||
log.Error("[uploadNewDocument] Invalid FileType: ", fileExtension)
|
||||
errorPage(c, http.StatusBadRequest, "Invalid filetype.")
|
||||
return
|
||||
}
|
||||
|
||||
// Create Temp File
|
||||
tempFile, err := os.CreateTemp("", "book")
|
||||
if err != nil {
|
||||
log.Warn("[uploadNewDocument] Temp File Create Error: ", err)
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to create temp file.")
|
||||
return
|
||||
}
|
||||
defer tempFile.Close()
|
||||
|
||||
// Save Temp
|
||||
err = c.SaveUploadedFile(rDocUpload.DocumentFile, tempFile.Name())
|
||||
if err != nil {
|
||||
log.Error("[uploadNewDocument] File Error: ", err)
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to save file.")
|
||||
return
|
||||
}
|
||||
|
||||
// Get Metadata
|
||||
metadataInfo, err := metadata.GetMetadata(tempFile.Name())
|
||||
if err != nil {
|
||||
log.Warn("[uploadNewDocument] GetMetadata Error: ", err)
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to acquire file metadata.")
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate Partial MD5 ID
|
||||
partialMD5, err := utils.CalculatePartialMD5(tempFile.Name())
|
||||
if err != nil {
|
||||
log.Warn("[uploadNewDocument] Partial MD5 Error: ", err)
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to calculate partial MD5.")
|
||||
return
|
||||
}
|
||||
|
||||
// Check Exists
|
||||
_, err = api.DB.Queries.GetDocument(api.DB.Ctx, partialMD5)
|
||||
if err == nil {
|
||||
c.Redirect(http.StatusFound, fmt.Sprintf("./documents/%s", partialMD5))
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate Actual MD5
|
||||
fileHash, err := getFileMD5(tempFile.Name())
|
||||
if err != nil {
|
||||
log.Error("[uploadNewDocument] MD5 Hash Failure:", err)
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to calculate MD5.")
|
||||
return
|
||||
}
|
||||
|
||||
// Derive Filename
|
||||
var fileName string
|
||||
if *metadataInfo.Author != "" {
|
||||
fileName = fileName + *metadataInfo.Author
|
||||
} else {
|
||||
fileName = fileName + "Unknown"
|
||||
}
|
||||
|
||||
if *metadataInfo.Title != "" {
|
||||
fileName = fileName + " - " + *metadataInfo.Title
|
||||
} else {
|
||||
fileName = fileName + " - Unknown"
|
||||
}
|
||||
|
||||
// Remove Slashes
|
||||
fileName = strings.ReplaceAll(fileName, "/", "")
|
||||
|
||||
// Derive & Sanitize File Name
|
||||
fileName = "." + filepath.Clean(fmt.Sprintf("/%s [%s]%s", fileName, partialMD5, fileExtension))
|
||||
|
||||
// Generate Storage Path
|
||||
safePath := filepath.Join(api.Config.DataPath, "documents", fileName)
|
||||
|
||||
// Move File
|
||||
if err := os.Rename(tempFile.Name(), safePath); err != nil {
|
||||
log.Error("[uploadNewDocument] Move Temp File Error:", err)
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to save file.")
|
||||
return
|
||||
}
|
||||
|
||||
// Upsert Document
|
||||
if _, err = api.DB.Queries.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{
|
||||
ID: partialMD5,
|
||||
Title: metadataInfo.Title,
|
||||
Author: metadataInfo.Author,
|
||||
Description: metadataInfo.Description,
|
||||
Md5: fileHash,
|
||||
Filepath: &fileName,
|
||||
}); err != nil {
|
||||
log.Error("[uploadNewDocument] UpsertDocument DB Error:", err)
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("UpsertDocument DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, fmt.Sprintf("./documents/%s", partialMD5))
|
||||
}
|
||||
|
||||
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"})
|
||||
errorPage(c, http.StatusNotFound, "Invalid document.")
|
||||
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"})
|
||||
errorPage(c, http.StatusBadRequest, "Invalid or missing form values.")
|
||||
return
|
||||
}
|
||||
|
||||
@ -372,7 +506,7 @@ func (api *API) editDocument(c *gin.Context) {
|
||||
rDocEdit.CoverGBID == nil &&
|
||||
rDocEdit.CoverFile == nil {
|
||||
log.Error("[createAppResourcesRoute] Missing Form Values")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
errorPage(c, http.StatusBadRequest, "Invalid or missing form values.")
|
||||
return
|
||||
}
|
||||
|
||||
@ -386,14 +520,14 @@ func (api *API) editDocument(c *gin.Context) {
|
||||
uploadedFile, err := rDocEdit.CoverFile.Open()
|
||||
if err != nil {
|
||||
log.Error("[createAppResourcesRoute] File Error")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to open file.")
|
||||
return
|
||||
}
|
||||
|
||||
fileMime, err := mimetype.DetectReader(uploadedFile)
|
||||
if err != nil {
|
||||
log.Error("[createAppResourcesRoute] MIME Error")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to detect filetype.")
|
||||
return
|
||||
}
|
||||
fileExtension := fileMime.Extension()
|
||||
@ -401,7 +535,7 @@ func (api *API) editDocument(c *gin.Context) {
|
||||
// Validate Extension
|
||||
if !slices.Contains([]string{".jpg", ".png"}, fileExtension) {
|
||||
log.Error("[uploadDocumentFile] Invalid FileType: ", fileExtension)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Filetype"})
|
||||
errorPage(c, http.StatusBadRequest, "Invalid filetype.")
|
||||
return
|
||||
}
|
||||
|
||||
@ -412,8 +546,8 @@ func (api *API) editDocument(c *gin.Context) {
|
||||
// Save
|
||||
err = c.SaveUploadedFile(rDocEdit.CoverFile, safePath)
|
||||
if err != nil {
|
||||
log.Error("[createAppResourcesRoute] File Error")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
log.Error("[createAppResourcesRoute] File Error: ", err)
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to save file.")
|
||||
return
|
||||
}
|
||||
|
||||
@ -437,7 +571,7 @@ func (api *API) editDocument(c *gin.Context) {
|
||||
Coverfile: coverFileName,
|
||||
}); err != nil {
|
||||
log.Error("[createAppResourcesRoute] UpsertDocument DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("UpsertDocument DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
@ -449,18 +583,18 @@ 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"})
|
||||
errorPage(c, http.StatusNotFound, "Invalid document.")
|
||||
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"})
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("DeleteDocument DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
if changed == 0 {
|
||||
log.Error("[deleteDocument] DeleteDocument DB Error")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Document"})
|
||||
errorPage(c, http.StatusNotFound, "Invalid document.")
|
||||
return
|
||||
}
|
||||
|
||||
@ -473,14 +607,14 @@ 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"})
|
||||
errorPage(c, http.StatusNotFound, "Invalid document.")
|
||||
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"})
|
||||
errorPage(c, http.StatusBadRequest, "Invalid or missing form values.")
|
||||
return
|
||||
}
|
||||
|
||||
@ -498,13 +632,12 @@ func (api *API) identifyDocument(c *gin.Context) {
|
||||
// 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"})
|
||||
errorPage(c, http.StatusBadRequest, "Invalid or missing form values.")
|
||||
return
|
||||
}
|
||||
|
||||
// Template Variables
|
||||
templateVars := gin.H{
|
||||
"RelBase": "../../",
|
||||
"SearchEnabled": api.Config.SearchEnabled,
|
||||
}
|
||||
|
||||
@ -544,7 +677,7 @@ func (api *API) identifyDocument(c *gin.Context) {
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("[identifyDocument] GetDocumentWithStats DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocumentWithStats DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
@ -558,7 +691,7 @@ func (api *API) saveNewDocument(c *gin.Context) {
|
||||
var rDocAdd requestDocumentAdd
|
||||
if err := c.ShouldBind(&rDocAdd); err != nil {
|
||||
log.Error("[saveNewDocument] Invalid Form Bind")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
errorPage(c, http.StatusBadRequest, "Invalid or missing form values.")
|
||||
return
|
||||
}
|
||||
|
||||
@ -568,7 +701,7 @@ func (api *API) saveNewDocument(c *gin.Context) {
|
||||
rDocAdd.Title == nil ||
|
||||
rDocAdd.Author == nil {
|
||||
log.Error("[saveNewDocument] Missing Form Values")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
errorPage(c, http.StatusBadRequest, "Invalid or missing form values.")
|
||||
return
|
||||
}
|
||||
|
||||
@ -581,15 +714,15 @@ func (api *API) saveNewDocument(c *gin.Context) {
|
||||
tempFilePath, err := search.SaveBook(*rDocAdd.ID, bType)
|
||||
if err != nil {
|
||||
log.Warn("[saveNewDocument] Temp File Error: ", err)
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to save file.")
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate Partial MD5 ID
|
||||
partialMD5, err := calculatePartialMD5(tempFilePath)
|
||||
partialMD5, err := utils.CalculatePartialMD5(tempFilePath)
|
||||
if err != nil {
|
||||
log.Warn("[saveNewDocument] Partial MD5 Error: ", err)
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to calculate partial MD5.")
|
||||
return
|
||||
}
|
||||
|
||||
@ -623,7 +756,7 @@ func (api *API) saveNewDocument(c *gin.Context) {
|
||||
// Move File
|
||||
if err := os.Rename(tempFilePath, safePath); err != nil {
|
||||
log.Warn("[saveNewDocument] Move Temp File Error: ", err)
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to save file.")
|
||||
return
|
||||
}
|
||||
|
||||
@ -631,7 +764,7 @@ func (api *API) saveNewDocument(c *gin.Context) {
|
||||
fileHash, err := getFileMD5(safePath)
|
||||
if err != nil {
|
||||
log.Error("[saveNewDocument] Hash Failure:", err)
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to calculate MD5.")
|
||||
return
|
||||
}
|
||||
|
||||
@ -644,7 +777,7 @@ func (api *API) saveNewDocument(c *gin.Context) {
|
||||
Filepath: &fileName,
|
||||
}); err != nil {
|
||||
log.Error("[saveNewDocument] UpsertDocument DB Error:", err)
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("UpsertDocument DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
@ -657,14 +790,14 @@ func (api *API) editSettings(c *gin.Context) {
|
||||
var rUserSettings requestSettingsEdit
|
||||
if err := c.ShouldBind(&rUserSettings); err != nil {
|
||||
log.Error("[editSettings] Invalid Form Bind")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
errorPage(c, http.StatusBadRequest, "Invalid or missing form values.")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate Something Exists
|
||||
if rUserSettings.Password == nil && rUserSettings.NewPassword == nil && rUserSettings.TimeOffset == nil {
|
||||
log.Error("[editSettings] Missing Form Values")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
errorPage(c, http.StatusBadRequest, "Invalid or missing form values.")
|
||||
return
|
||||
}
|
||||
|
||||
@ -703,7 +836,7 @@ func (api *API) editSettings(c *gin.Context) {
|
||||
_, err := api.DB.Queries.UpdateUser(api.DB.Ctx, newUserSettings)
|
||||
if err != nil {
|
||||
log.Error("[editSettings] UpdateUser DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("UpdateUser DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
@ -711,7 +844,7 @@ func (api *API) editSettings(c *gin.Context) {
|
||||
user, err := api.DB.Queries.GetUser(api.DB.Ctx, rUser.(string))
|
||||
if err != nil {
|
||||
log.Error("[editSettings] GetUser DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUser DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
@ -719,7 +852,7 @@ func (api *API) editSettings(c *gin.Context) {
|
||||
devices, err := api.DB.Queries.GetDevices(api.DB.Ctx, rUser.(string))
|
||||
if err != nil {
|
||||
log.Error("[editSettings] GetDevices DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDevices DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
@ -792,3 +925,24 @@ func bindQueryParams(c *gin.Context) queryParams {
|
||||
|
||||
return qParams
|
||||
}
|
||||
|
||||
func errorPage(c *gin.Context, errorCode int, errorMessage string) {
|
||||
var errorHuman string = "We're not even sure what happened."
|
||||
|
||||
switch errorCode {
|
||||
case http.StatusInternalServerError:
|
||||
errorHuman = "Server hiccup."
|
||||
case http.StatusNotFound:
|
||||
errorHuman = "Something's missing."
|
||||
case http.StatusBadRequest:
|
||||
errorHuman = "We didn't expect that."
|
||||
case http.StatusUnauthorized:
|
||||
errorHuman = "You're not allowed to do that."
|
||||
}
|
||||
|
||||
c.HTML(errorCode, "error", gin.H{
|
||||
"Status": errorCode,
|
||||
"Error": errorHuman,
|
||||
"Message": errorMessage,
|
||||
})
|
||||
}
|
||||
|
@ -112,6 +112,7 @@ func (api *API) authWebAppMiddleware(c *gin.Context) {
|
||||
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
func (api *API) authFormLogin(c *gin.Context) {
|
||||
@ -152,7 +153,8 @@ func (api *API) authFormLogin(c *gin.Context) {
|
||||
|
||||
func (api *API) authFormRegister(c *gin.Context) {
|
||||
if !api.Config.RegistrationEnabled {
|
||||
c.AbortWithStatus(http.StatusConflict)
|
||||
errorPage(c, http.StatusUnauthorized, "Nice try. Registration is disabled.")
|
||||
return
|
||||
}
|
||||
|
||||
username := strings.TrimSpace(c.PostForm("username"))
|
||||
@ -202,7 +204,7 @@ func (api *API) authFormRegister(c *gin.Context) {
|
||||
// Set Session
|
||||
session := sessions.Default(c)
|
||||
if err := setSession(session, username); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
errorPage(c, http.StatusUnauthorized, "Unauthorized.")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -500,17 +500,17 @@ func (api *API) checkDocumentsSync(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, rCheckDocSync)
|
||||
}
|
||||
|
||||
func (api *API) uploadDocumentFile(c *gin.Context) {
|
||||
func (api *API) uploadExistingDocument(c *gin.Context) {
|
||||
var rDoc requestDocumentID
|
||||
if err := c.ShouldBindUri(&rDoc); err != nil {
|
||||
log.Error("[uploadDocumentFile] Invalid URI Bind")
|
||||
log.Error("[uploadExistingDocument] Invalid URI Bind")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
|
||||
fileData, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
log.Error("[uploadDocumentFile] File Error:", err)
|
||||
log.Error("[uploadExistingDocument] File Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "File Error"})
|
||||
return
|
||||
}
|
||||
@ -521,7 +521,7 @@ func (api *API) uploadDocumentFile(c *gin.Context) {
|
||||
fileExtension := fileMime.Extension()
|
||||
|
||||
if !slices.Contains([]string{".epub", ".html"}, fileExtension) {
|
||||
log.Error("[uploadDocumentFile] Invalid FileType:", fileExtension)
|
||||
log.Error("[uploadExistingDocument] Invalid FileType:", fileExtension)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Filetype"})
|
||||
return
|
||||
}
|
||||
@ -529,7 +529,7 @@ func (api *API) uploadDocumentFile(c *gin.Context) {
|
||||
// Validate Document Exists in DB
|
||||
document, err := api.DB.Queries.GetDocument(api.DB.Ctx, rDoc.DocumentID)
|
||||
if err != nil {
|
||||
log.Error("[uploadDocumentFile] GetDocument DB Error:", err)
|
||||
log.Error("[uploadExistingDocument] GetDocument DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Document"})
|
||||
return
|
||||
}
|
||||
@ -562,7 +562,7 @@ func (api *API) uploadDocumentFile(c *gin.Context) {
|
||||
if os.IsNotExist(err) {
|
||||
err = c.SaveUploadedFile(fileData, safePath)
|
||||
if err != nil {
|
||||
log.Error("[uploadDocumentFile] Save Failure:", err)
|
||||
log.Error("[uploadExistingDocument] Save Failure:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "File Error"})
|
||||
return
|
||||
}
|
||||
@ -571,7 +571,7 @@ func (api *API) uploadDocumentFile(c *gin.Context) {
|
||||
// Get MD5 Hash
|
||||
fileHash, err := getFileMD5(safePath)
|
||||
if err != nil {
|
||||
log.Error("[uploadDocumentFile] Hash Failure:", err)
|
||||
log.Error("[uploadExistingDocument] Hash Failure:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "File Error"})
|
||||
return
|
||||
}
|
||||
@ -582,7 +582,7 @@ func (api *API) uploadDocumentFile(c *gin.Context) {
|
||||
Md5: fileHash,
|
||||
Filepath: &fileName,
|
||||
}); err != nil {
|
||||
log.Error("[uploadDocumentFile] UpsertDocument DB Error:", err)
|
||||
log.Error("[uploadExistingDocument] UpsertDocument DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Document Error"})
|
||||
return
|
||||
}
|
||||
@ -592,7 +592,7 @@ func (api *API) uploadDocumentFile(c *gin.Context) {
|
||||
ID: document.ID,
|
||||
Synced: true,
|
||||
}); err != nil {
|
||||
log.Error("[uploadDocumentFile] UpdateDocumentSync DB Error:", err)
|
||||
log.Error("[uploadExistingDocument] UpdateDocumentSync DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Document"})
|
||||
return
|
||||
}
|
||||
@ -602,10 +602,10 @@ func (api *API) uploadDocumentFile(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) downloadDocumentFile(c *gin.Context) {
|
||||
func (api *API) downloadDocument(c *gin.Context) {
|
||||
var rDoc requestDocumentID
|
||||
if err := c.ShouldBindUri(&rDoc); err != nil {
|
||||
log.Error("[downloadDocumentFile] Invalid URI Bind")
|
||||
log.Error("[downloadDocument] Invalid URI Bind")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
@ -613,13 +613,13 @@ func (api *API) downloadDocumentFile(c *gin.Context) {
|
||||
// Get Document
|
||||
document, err := api.DB.Queries.GetDocument(api.DB.Ctx, rDoc.DocumentID)
|
||||
if err != nil {
|
||||
log.Error("[uploadDocumentFile] GetDocument DB Error:", err)
|
||||
log.Error("[downloadDocument] GetDocument DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Document"})
|
||||
return
|
||||
}
|
||||
|
||||
if document.Filepath == nil {
|
||||
log.Error("[uploadDocumentFile] Document Doesn't Have File:", rDoc.DocumentID)
|
||||
log.Error("[downloadDocument] Document Doesn't Have File:", rDoc.DocumentID)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Document Doesn't Exist"})
|
||||
return
|
||||
}
|
||||
@ -630,7 +630,7 @@ func (api *API) downloadDocumentFile(c *gin.Context) {
|
||||
// Validate File Exists
|
||||
_, err = os.Stat(filePath)
|
||||
if os.IsNotExist(err) {
|
||||
log.Error("[uploadDocumentFile] File Doesn't Exist:", rDoc.DocumentID)
|
||||
log.Error("[downloadDocument] File Doesn't Exist:", rDoc.DocumentID)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Document Doesn't Exists"})
|
||||
return
|
||||
}
|
||||
|
39
api/utils.go
39
api/utils.go
@ -1,12 +1,8 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
|
||||
"reichard.io/bbank/database"
|
||||
"reichard.io/bbank/graph"
|
||||
@ -90,41 +86,6 @@ func niceSeconds(input int64) (result string) {
|
||||
return
|
||||
}
|
||||
|
||||
// Reimplemented KOReader Partial MD5 Calculation
|
||||
func calculatePartialMD5(filePath string) (string, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
var step int64 = 1024
|
||||
var size int64 = 1024
|
||||
var buf bytes.Buffer
|
||||
|
||||
for i := -1; i <= 10; i++ {
|
||||
byteStep := make([]byte, size)
|
||||
|
||||
var newShift int64 = int64(i * 2)
|
||||
var newOffset int64
|
||||
if i == -1 {
|
||||
newOffset = 0
|
||||
} else {
|
||||
newOffset = step << newShift
|
||||
}
|
||||
|
||||
_, err := file.ReadAt(byteStep, newOffset)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
buf.Write(byteStep)
|
||||
}
|
||||
|
||||
allBytes := buf.Bytes()
|
||||
return fmt.Sprintf("%x", md5.Sum(allBytes)), nil
|
||||
}
|
||||
|
||||
// Convert Database Array -> Int64 Array
|
||||
func getSVGGraphData(inputData []database.GetDailyReadStatsRow, svgWidth int, svgHeight int) graph.SVGGraphData {
|
||||
var intData []int64
|
||||
|
12
api/utils_test.go
Normal file
12
api/utils_test.go
Normal file
@ -0,0 +1,12 @@
|
||||
package api
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNiceSeconds(t *testing.T) {
|
||||
want := "22d 7h 39m 31s"
|
||||
nice := niceSeconds(1928371)
|
||||
|
||||
if nice != want {
|
||||
t.Fatalf(`Expected: %v, Got: %v`, want, nice)
|
||||
}
|
||||
}
|
@ -53,9 +53,6 @@ class EBookReader {
|
||||
// Start Timer
|
||||
this.bookState.pageStart = Date.now();
|
||||
|
||||
// Restore Theme
|
||||
this.setTheme(this.readerSettings.theme || "tan");
|
||||
|
||||
// Get Stats
|
||||
let stats = this.getBookStats();
|
||||
this.updateBookStats(stats);
|
||||
@ -64,19 +61,8 @@ class EBookReader {
|
||||
// Register Content Hook
|
||||
this.rendition.hooks.content.register(getStats);
|
||||
|
||||
/**
|
||||
* Display @ CFI x 3 (Hack)
|
||||
*
|
||||
* This is absurd. Only way to get it to consistently show the correct
|
||||
* page is to execute this three times. I tried the font hook,
|
||||
* rendition hook, relocated hook, etc. No reliable way outside of
|
||||
* running this three times.
|
||||
*
|
||||
* Likely Bug: https://github.com/futurepress/epub.js/issues/1194
|
||||
**/
|
||||
await this.rendition.display(cfi);
|
||||
await this.rendition.display(cfi);
|
||||
await this.rendition.display(cfi);
|
||||
// Update Position
|
||||
await this.setPosition(cfi);
|
||||
|
||||
// Highlight Element - DOM Has Element
|
||||
let { element } = await this.getCFIFromXPath(this.bookState.progress);
|
||||
@ -100,6 +86,7 @@ class EBookReader {
|
||||
|
||||
this.readerSettings.deviceID = this.readerSettings.deviceID || randomID();
|
||||
|
||||
// Save Settings (Device ID)
|
||||
this.saveSettings();
|
||||
}
|
||||
|
||||
@ -154,24 +141,66 @@ class EBookReader {
|
||||
themeLinkEl.setAttribute("rel", "stylesheet");
|
||||
themeLinkEl.setAttribute("href", THEME_FILE);
|
||||
document.head.append(themeLinkEl);
|
||||
|
||||
// Set Theme Style
|
||||
this.rendition.themes.default({
|
||||
"*": {
|
||||
"font-size": "var(--editor-font-size) !important",
|
||||
"font-family": "var(--editor-font-family) !important",
|
||||
},
|
||||
});
|
||||
|
||||
// Restore Theme Hook
|
||||
this.rendition.hooks.content.register(
|
||||
function () {
|
||||
// Restore Theme
|
||||
this.setTheme();
|
||||
|
||||
// Set Fonts - TODO: Local
|
||||
// https://gwfh.mranftl.com/fonts
|
||||
this.rendition.getContents().forEach((c) => {
|
||||
[
|
||||
"https://fonts.googleapis.com/css?family=Arbutus+Slab",
|
||||
"https://fonts.googleapis.com/css?family=Open+Sans",
|
||||
"https://fonts.googleapis.com/css?family=Lato:400,400i,700,700i",
|
||||
].forEach((url) => {
|
||||
let el = c.document.head.appendChild(
|
||||
c.document.createElement("link")
|
||||
);
|
||||
el.setAttribute("rel", "stylesheet");
|
||||
el.setAttribute("href", url);
|
||||
});
|
||||
});
|
||||
}.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set theme & meta theme color
|
||||
**/
|
||||
setTheme(newTheme) {
|
||||
// Update Settings
|
||||
this.readerSettings.theme = newTheme;
|
||||
this.saveSettings();
|
||||
// Assert Theme Object
|
||||
this.readerSettings.theme =
|
||||
typeof this.readerSettings.theme == "object"
|
||||
? this.readerSettings.theme
|
||||
: {};
|
||||
|
||||
// Assign Values
|
||||
Object.assign(this.readerSettings.theme, newTheme);
|
||||
|
||||
// Get Desired Theme (Defaults)
|
||||
let colorScheme = this.readerSettings.theme.colorScheme || "tan";
|
||||
let fontFamily = this.readerSettings.theme.fontFamily || "serif";
|
||||
let fontSize = this.readerSettings.theme.fontSize || 1;
|
||||
|
||||
// Set Reader Theme
|
||||
this.rendition.themes.select(newTheme);
|
||||
this.rendition.themes.select(colorScheme);
|
||||
|
||||
// Get Reader Theme
|
||||
let themeColorEl = document.querySelector("[name='theme-color']");
|
||||
let themeStyleSheet = document.querySelector("#themes").sheet;
|
||||
let themeStyleRule = Array.from(themeStyleSheet.cssRules).find(
|
||||
(item) => item.selectorText == "." + newTheme
|
||||
(item) => item.selectorText == "." + colorScheme
|
||||
);
|
||||
|
||||
// Match Reader Theme
|
||||
@ -180,16 +209,37 @@ class EBookReader {
|
||||
themeColorEl.setAttribute("content", backgroundColor);
|
||||
document.body.style.backgroundColor = backgroundColor;
|
||||
|
||||
// Update Position Highlight Theme
|
||||
// Set Font Family & Highlight Style
|
||||
this.rendition.getContents().forEach((item) => {
|
||||
// Set Font Family
|
||||
item.document.documentElement.style.setProperty(
|
||||
"--editor-font-family",
|
||||
fontFamily
|
||||
);
|
||||
|
||||
// Set Font Size
|
||||
item.document.documentElement.style.setProperty(
|
||||
"--editor-font-size",
|
||||
fontSize + "em"
|
||||
);
|
||||
|
||||
// Set Highlight Style
|
||||
item.document.querySelectorAll(".highlight").forEach((el) => {
|
||||
Object.assign(el.style, {
|
||||
background: backgroundColor,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Save Settings (Theme)
|
||||
this.saveSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes existing progressElement and applies the highlight style to it.
|
||||
* This is nice when font size or font family changes as it can cause
|
||||
* the position to move.
|
||||
**/
|
||||
highlightPositionMarker() {
|
||||
if (!this.bookState.progressElement) return;
|
||||
|
||||
@ -248,7 +298,7 @@ class EBookReader {
|
||||
|
||||
// Local Functions
|
||||
let getCFIFromXPath = this.getCFIFromXPath.bind(this);
|
||||
let highlightPositionMarker = this.highlightPositionMarker.bind(this);
|
||||
let setPosition = this.setPosition.bind(this);
|
||||
let nextPage = this.nextPage.bind(this);
|
||||
let prevPage = this.prevPage.bind(this);
|
||||
let saveSettings = this.saveSettings.bind(this);
|
||||
@ -260,15 +310,6 @@ class EBookReader {
|
||||
this.rendition.hooks.render.register(function (doc, data) {
|
||||
let renderDoc = doc.document;
|
||||
|
||||
// Initial Font Size
|
||||
renderDoc.documentElement.style.setProperty(
|
||||
"--editor-font-size",
|
||||
(readerSettings.fontSize || 1) + "em"
|
||||
);
|
||||
this.themes.default({
|
||||
"*": { "font-size": "var(--editor-font-size) !important" },
|
||||
});
|
||||
|
||||
// ------------------------------------------------ //
|
||||
// ---------------- Wake Lock Hack ---------------- //
|
||||
// ------------------------------------------------ //
|
||||
@ -279,55 +320,6 @@ class EBookReader {
|
||||
renderDoc.addEventListener("gesturechange", wakeLockListener);
|
||||
renderDoc.addEventListener("touchstart", wakeLockListener);
|
||||
|
||||
// ------------------------------------------------ //
|
||||
// ---------------- Resize Helpers ---------------- //
|
||||
// ------------------------------------------------ //
|
||||
let lastScale = 1;
|
||||
let isScaling = false;
|
||||
let timeoutID = undefined;
|
||||
let cfiLocation = undefined;
|
||||
|
||||
// Gesture Listener
|
||||
renderDoc.addEventListener(
|
||||
"gesturechange",
|
||||
async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
isScaling = true;
|
||||
clearTimeout(timeoutID);
|
||||
|
||||
if (!cfiLocation)
|
||||
({ cfi: cfiLocation } = await getCFIFromXPath(bookState.progress));
|
||||
|
||||
// Damped Scale
|
||||
readerSettings.fontSize =
|
||||
(readerSettings.fontSize || 1) + (e.scale - lastScale) / 5;
|
||||
lastScale = e.scale;
|
||||
|
||||
// Update Font Size
|
||||
renderDoc.documentElement.style.setProperty(
|
||||
"--editor-font-size",
|
||||
(readerSettings.fontSize || 1) + "em"
|
||||
);
|
||||
|
||||
timeoutID = setTimeout(() => {
|
||||
// Display Position
|
||||
this.display(cfiLocation);
|
||||
|
||||
// Reset Variables
|
||||
isScaling = false;
|
||||
cfiLocation = undefined;
|
||||
|
||||
// Highlight Location
|
||||
highlightPositionMarker();
|
||||
|
||||
// Save Settings (Font Size)
|
||||
saveSettings();
|
||||
}, 250);
|
||||
}.bind(this),
|
||||
true
|
||||
);
|
||||
|
||||
// ------------------------------------------------ //
|
||||
// --------------- Swipe Pagination --------------- //
|
||||
// ------------------------------------------------ //
|
||||
@ -350,7 +342,7 @@ class EBookReader {
|
||||
function (event) {
|
||||
touchEndX = event.changedTouches[0].screenX;
|
||||
touchEndY = event.changedTouches[0].screenY;
|
||||
if (!isScaling) handleGesture(event);
|
||||
handleGesture(event);
|
||||
},
|
||||
false
|
||||
);
|
||||
@ -447,11 +439,14 @@ class EBookReader {
|
||||
|
||||
// "t" Key (Theme Cycle)
|
||||
if ((e.keyCode || e.which) == 84) {
|
||||
let currentThemeIdx = THEMES.indexOf(readerSettings.theme);
|
||||
if (THEMES.length == currentThemeIdx + 1)
|
||||
readerSettings.theme = THEMES[0];
|
||||
else readerSettings.theme = THEMES[currentThemeIdx + 1];
|
||||
setTheme(readerSettings.theme);
|
||||
let currentThemeIdx = THEMES.indexOf(
|
||||
readerSettings.theme.colorScheme
|
||||
);
|
||||
let colorScheme =
|
||||
THEMES.length == currentThemeIdx + 1
|
||||
? THEMES[0]
|
||||
: THEMES[currentThemeIdx + 1];
|
||||
setTheme({ colorScheme });
|
||||
}
|
||||
},
|
||||
false
|
||||
@ -469,6 +464,7 @@ class EBookReader {
|
||||
let nextPage = this.nextPage.bind(this);
|
||||
let prevPage = this.prevPage.bind(this);
|
||||
|
||||
// Keyboard Shortcuts
|
||||
document.addEventListener(
|
||||
"keyup",
|
||||
function (e) {
|
||||
@ -484,28 +480,76 @@ class EBookReader {
|
||||
|
||||
// "t" Key (Theme Cycle)
|
||||
if ((e.keyCode || e.which) == 84) {
|
||||
let currentThemeIdx = THEMES.indexOf(this.readerSettings.theme);
|
||||
let newTheme =
|
||||
let currentThemeIdx = THEMES.indexOf(
|
||||
this.readerSettings.theme.colorScheme
|
||||
);
|
||||
let colorScheme =
|
||||
THEMES.length == currentThemeIdx + 1
|
||||
? THEMES[0]
|
||||
: THEMES[currentThemeIdx + 1];
|
||||
this.setTheme(newTheme);
|
||||
this.setTheme({ colorScheme });
|
||||
}
|
||||
}.bind(this),
|
||||
false
|
||||
);
|
||||
|
||||
document.querySelectorAll(".theme").forEach(
|
||||
// Color Scheme Switcher
|
||||
document.querySelectorAll(".color-scheme").forEach(
|
||||
function (item) {
|
||||
item.addEventListener(
|
||||
"click",
|
||||
function (event) {
|
||||
this.setTheme(event.target.innerText);
|
||||
let colorScheme = event.target.innerText;
|
||||
console.log(colorScheme);
|
||||
this.setTheme({ colorScheme });
|
||||
}.bind(this)
|
||||
);
|
||||
}.bind(this)
|
||||
);
|
||||
|
||||
// Font Switcher
|
||||
document.querySelectorAll(".font-family").forEach(
|
||||
function (item) {
|
||||
item.addEventListener(
|
||||
"click",
|
||||
async function (event) {
|
||||
let { cfi } = await this.getCFIFromXPath(this.bookState.progress);
|
||||
|
||||
let fontFamily = event.target.innerText;
|
||||
this.setTheme({ fontFamily });
|
||||
|
||||
this.setPosition(cfi);
|
||||
}.bind(this)
|
||||
);
|
||||
}.bind(this)
|
||||
);
|
||||
|
||||
// Font Size
|
||||
document.querySelectorAll(".font-size").forEach(
|
||||
function (item) {
|
||||
item.addEventListener(
|
||||
"click",
|
||||
async function (event) {
|
||||
// Get Initial CFI
|
||||
let { cfi } = await this.getCFIFromXPath(this.bookState.progress);
|
||||
|
||||
// Modify Size
|
||||
let currentSize = this.readerSettings.theme.fontSize || 1;
|
||||
let direction = event.target.innerText;
|
||||
if (direction == "-") {
|
||||
this.setTheme({ fontSize: currentSize * 0.99 });
|
||||
} else if (direction == "+") {
|
||||
this.setTheme({ fontSize: currentSize * 1.01 });
|
||||
}
|
||||
|
||||
// Restore CFI
|
||||
this.setPosition(cfi);
|
||||
}.bind(this)
|
||||
);
|
||||
}.bind(this)
|
||||
);
|
||||
|
||||
// Close Top Bar
|
||||
document.querySelector(".close-top-bar").addEventListener("click", () => {
|
||||
topBar.classList.remove("top-0");
|
||||
});
|
||||
@ -584,6 +628,24 @@ class EBookReader {
|
||||
this.flushProgress();
|
||||
}
|
||||
|
||||
/**
|
||||
* Display @ CFI x 3 (Hack)
|
||||
*
|
||||
* This is absurd. Only way to get it to consistently show the correct
|
||||
* page is to execute this three times. I tried the font hook,
|
||||
* rendition hook, relocated hook, etc. No reliable way outside of
|
||||
* running this three times.
|
||||
*
|
||||
* Likely Bug: https://github.com/futurepress/epub.js/issues/1194
|
||||
**/
|
||||
async setPosition(cfi) {
|
||||
await this.rendition.display(cfi);
|
||||
await this.rendition.display(cfi);
|
||||
await this.rendition.display(cfi);
|
||||
|
||||
this.highlightPositionMarker();
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize and flush activity
|
||||
**/
|
||||
@ -659,6 +721,9 @@ class EBookReader {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush progress to the API. Called when the page changes.
|
||||
**/
|
||||
async flushProgress() {
|
||||
console.log("Flushing Progress...");
|
||||
|
||||
@ -833,13 +898,13 @@ class EBookReader {
|
||||
// - [...]/text().184 = text node of parent, character offset @ 184 chars?
|
||||
|
||||
// No XPath
|
||||
if (!xpath || xpath == "") return;
|
||||
if (!xpath || xpath == "") return {};
|
||||
|
||||
// Match Document Fragment Index
|
||||
let fragMatch = xpath.match(/^\/body\/DocFragment\[(\d+)\]/);
|
||||
if (!fragMatch) {
|
||||
console.warn("No XPath Match");
|
||||
return;
|
||||
return {};
|
||||
}
|
||||
|
||||
// Match Item Index
|
||||
@ -976,10 +1041,9 @@ class EBookReader {
|
||||
/**
|
||||
* Save settings to localStorage
|
||||
**/
|
||||
saveSettings(obj) {
|
||||
saveSettings() {
|
||||
if (!this.readerSettings) this.loadSettings();
|
||||
let newSettings = Object.assign(this.readerSettings, obj);
|
||||
localStorage.setItem("readerSettings", JSON.stringify(newSettings));
|
||||
localStorage.setItem("readerSettings", JSON.stringify(this.readerSettings));
|
||||
}
|
||||
|
||||
/**
|
||||
|
2083
assets/style.css
Normal file
2083
assets/style.css
Normal file
File diff suppressed because it is too large
Load Diff
@ -8,6 +8,20 @@ import (
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
func getEPUBMetadata(filepath string) (*MetadataInfo, error) {
|
||||
rc, err := epub.OpenReader(filepath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rf := rc.Rootfiles[0]
|
||||
|
||||
return &MetadataInfo{
|
||||
Title: &rf.Title,
|
||||
Author: &rf.Creator,
|
||||
Description: &rf.Description,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func countEPUBWords(filepath string) (int64, error) {
|
||||
rc, err := epub.OpenReader(filepath)
|
||||
if err != nil {
|
||||
|
@ -35,7 +35,7 @@ func TestGBooksISBNQuery(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGBooksTitleQuery(t *testing.T) {
|
||||
title := "Alice in Wonderland"
|
||||
title := "Alice in Wonderland 1877527815"
|
||||
metadataResp, err := getGBooksMetadata(MetadataInfo{
|
||||
Title: &title,
|
||||
})
|
||||
|
@ -67,3 +67,16 @@ func GetWordCount(filepath string) (int64, error) {
|
||||
return 0, errors.New("Invalid Extension")
|
||||
}
|
||||
}
|
||||
|
||||
func GetMetadata(filepath string) (*MetadataInfo, error) {
|
||||
fileMime, err := mimetype.DetectFile(filepath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if fileExtension := fileMime.Extension(); fileExtension == ".epub" {
|
||||
return getEPUBMetadata(filepath)
|
||||
} else {
|
||||
return nil, errors.New("Invalid Extension")
|
||||
}
|
||||
}
|
||||
|
@ -6,9 +6,31 @@ import (
|
||||
|
||||
func TestGetWordCount(t *testing.T) {
|
||||
var want int64 = 30477
|
||||
wordCount, err := countEPUBWords("./_test_files/alice.epub")
|
||||
wordCount, err := countEPUBWords("../_test_files/alice.epub")
|
||||
|
||||
if wordCount != want {
|
||||
t.Fatalf(`Expected: %v, Got: %v, Error: %v`, want, wordCount, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetMetadata(t *testing.T) {
|
||||
metadataInfo, err := getEPUBMetadata("../_test_files/alice.epub")
|
||||
if err != nil {
|
||||
t.Fatalf(`Expected: *MetadataInfo, Got: nil, Error: %v`, err)
|
||||
}
|
||||
|
||||
want := "Alice's Adventures in Wonderland / Illustrated by Arthur Rackham. With a Proem by Austin Dobson"
|
||||
if *metadataInfo.Title != want {
|
||||
t.Fatalf(`Expected: %v, Got: %v, Error: %v`, want, *metadataInfo.Title, err)
|
||||
}
|
||||
|
||||
want = "Lewis Carroll"
|
||||
if *metadataInfo.Author != want {
|
||||
t.Fatalf(`Expected: %v, Got: %v, Error: %v`, want, *metadataInfo.Author, err)
|
||||
}
|
||||
|
||||
want = ""
|
||||
if *metadataInfo.Description != want {
|
||||
t.Fatalf(`Expected: %v, Got: %v, Error: %v`, want, *metadataInfo.Description, err)
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +0,0 @@
|
||||
#!/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,7 +3,6 @@
|
||||
pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
go
|
||||
zig
|
||||
nodejs_20
|
||||
nodePackages.tailwindcss
|
||||
];
|
||||
}
|
||||
|
8
tailwind.config.js
Normal file
8
tailwind.config.js
Normal file
@ -0,0 +1,8 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./templates/**/*.{html,js}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
@ -6,11 +6,14 @@
|
||||
content="width=device-width, initial-scale=0.90, user-scalable=no, viewport-fit=cover">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes"/>
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/>
|
||||
<link rel="manifest" href="{{ .RelBase }}./manifest.json" />
|
||||
<meta name="theme-color" content="#F3F4F6" media="(prefers-color-scheme: light)">
|
||||
<meta name="theme-color" content="#1F2937" media="(prefers-color-scheme: dark)">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<title>Book Manager - {{block "title" .}}{{end}}</title>
|
||||
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="stylesheet" href="/assets/style.css">
|
||||
|
||||
<style>
|
||||
/* ----------------------------- */
|
||||
/* -------- PWA Styling -------- */
|
||||
|
@ -3,7 +3,7 @@
|
||||
{{define "title"}}Documents{{end}}
|
||||
|
||||
{{define "header"}}
|
||||
<a href="{{ .RelBase }}./documents">Documents</a>
|
||||
<a href="/documents">Documents</a>
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
@ -12,7 +12,7 @@
|
||||
<div class="h-full w-full overflow-scroll bg-white shadow-lg dark:bg-gray-700 rounded dark:text-white p-4">
|
||||
<div class="flex flex-col gap-2 float-left w-44 md:w-60 lg:w-80 mr-4 mb-2 relative">
|
||||
<label class="z-10 cursor-pointer" for="edit-cover-button">
|
||||
<img class="rounded object-fill w-full" src="{{ .RelBase }}./documents/{{.Data.ID}}/cover"></img>
|
||||
<img class="rounded object-fill w-full" src="/documents/{{.Data.ID}}/cover"></img>
|
||||
</label>
|
||||
|
||||
{{ if .Data.Filepath }}
|
||||
@ -52,7 +52,7 @@
|
||||
name="cover_file"
|
||||
>
|
||||
<button
|
||||
class="font-medium px-2 py-1 text-white bg-gray-500 dark:bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
type="submit"
|
||||
>Upload Cover</button>
|
||||
</form>
|
||||
@ -63,7 +63,7 @@
|
||||
>
|
||||
<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:bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
type="submit"
|
||||
>Remove Cover</button>
|
||||
</form>
|
||||
@ -95,7 +95,7 @@
|
||||
class="text-black dark:text-white text-sm"
|
||||
>
|
||||
<button
|
||||
class="font-medium w-24 px-2 py-1 text-white bg-gray-500 dark:bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
class="font-medium w-24 px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
type="submit"
|
||||
>Delete</button>
|
||||
</form>
|
||||
@ -163,7 +163,7 @@
|
||||
class="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white"
|
||||
>
|
||||
<button
|
||||
class="font-medium px-2 py-1 text-white bg-gray-500 dark:bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
type="submit"
|
||||
>Identify</button>
|
||||
</form>
|
||||
@ -244,7 +244,7 @@
|
||||
class="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white"
|
||||
>
|
||||
<button
|
||||
class="font-medium px-2 py-1 text-white bg-gray-500 dark:bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
type="submit"
|
||||
>Save</button>
|
||||
</form>
|
||||
@ -293,7 +293,7 @@
|
||||
class="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white"
|
||||
>
|
||||
<button
|
||||
class="font-medium px-2 py-1 text-white bg-gray-500 dark:bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
type="submit"
|
||||
>Save</button>
|
||||
</form>
|
||||
@ -401,7 +401,7 @@
|
||||
<div
|
||||
class="absolute h-full w-full min-h-[10em] z-30 top-1 right-0 gap-4 flex transition-all duration-200"
|
||||
>
|
||||
<img class="hidden md:block invisible rounded w-44 md:w-60 lg:w-80 object-fill" src="{{ .RelBase }}./documents/{{.Data.ID}}/cover"></img>
|
||||
<img class="hidden md:block invisible rounded w-44 md:w-60 lg:w-80 object-fill" src="/documents/{{.Data.ID}}/cover"></img>
|
||||
<form
|
||||
method="POST"
|
||||
action="./{{ .Data.ID }}/edit"
|
||||
@ -414,7 +414,7 @@
|
||||
class="h-full w-full p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white"
|
||||
>{{ or .Data.Description "N/A" }}</textarea>
|
||||
<button
|
||||
class="font-medium px-2 py-1 text-white bg-gray-500 dark:bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
type="submit"
|
||||
>Save</button>
|
||||
</form>
|
||||
@ -430,8 +430,8 @@
|
||||
<div class="text-center">
|
||||
<h3 class="text-lg font-bold leading-6 dark:text-gray-300">No Metadata Results Found</h3>
|
||||
</div>
|
||||
<a href="{{ .RelBase }}./documents/{{ .Data.ID }}"
|
||||
class="w-full text-center font-medium px-2 py-1 text-white bg-gray-500 dark:bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
<a href="/documents/{{ .Data.ID }}"
|
||||
class="w-full text-center font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
type="submit"
|
||||
>Back to Document</a>
|
||||
</div>
|
||||
@ -449,7 +449,7 @@
|
||||
<form
|
||||
id="metadata-save"
|
||||
method="POST"
|
||||
action="{{ .RelBase }}./documents/{{ .Data.ID }}/edit"
|
||||
action="/documents/{{ .Data.ID }}/edit"
|
||||
class="text-black dark:text-white border-b dark:border-black"
|
||||
>
|
||||
<dl>
|
||||
@ -512,13 +512,13 @@
|
||||
</div>
|
||||
</form>
|
||||
<div class="flex justify-end gap-4 m-4">
|
||||
<a href="{{ .RelBase }}./documents/{{ .Data.ID }}"
|
||||
class="w-24 text-center font-medium px-2 py-1 text-white bg-gray-500 dark:bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
<a href="/documents/{{ .Data.ID }}"
|
||||
class="w-24 text-center font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
type="submit"
|
||||
>Cancel</a>
|
||||
<button
|
||||
form="metadata-save"
|
||||
class="w-24 font-medium px-2 py-1 text-white bg-gray-500 dark:bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
class="w-24 font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
type="submit"
|
||||
>Save</button>
|
||||
</div>
|
||||
|
@ -103,4 +103,55 @@
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="fixed bottom-6 right-6 rounded-full flex items-center justify-center">
|
||||
<input type="checkbox" id="upload-file-button" class="hidden css-button"/>
|
||||
<div class="rounded p-4 bg-gray-800 dark:bg-gray-200 text-white dark:text-black w-72 text-sm flex flex-col gap-2">
|
||||
<form method="POST" enctype="multipart/form-data" action="./documents" class="flex flex-col gap-2">
|
||||
<input type="file" accept=".epub" id="document_file" name="document_file">
|
||||
<button class="font-medium px-2 py-1 text-gray-800 bg-gray-500 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-800" type="submit">Upload File</button>
|
||||
</form>
|
||||
<label for="upload-file-button">
|
||||
<div class="w-full text-center cursor-pointer font-medium mt-2 px-2 py-1 text-gray-800 bg-gray-500 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-800">Cancel Upload</div>
|
||||
</label>
|
||||
</div>
|
||||
<label
|
||||
class="w-16 h-16 bg-gray-800 dark:bg-gray-200 rounded-full flex items-center justify-center opacity-30 hover:opacity-100 transition-all duration-200 cursor-pointer"
|
||||
for="upload-file-button"
|
||||
>
|
||||
<svg
|
||||
width="34"
|
||||
height="34"
|
||||
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="M12 15.75C12.4142 15.75 12.75 15.4142 12.75 15V4.02744L14.4306 5.98809C14.7001 6.30259 15.1736 6.33901 15.4881 6.06944C15.8026 5.79988 15.839 5.3264 15.5694 5.01191L12.5694 1.51191C12.427 1.34567 12.2189 1.25 12 1.25C11.7811 1.25 11.573 1.34567 11.4306 1.51191L8.43056 5.01191C8.16099 5.3264 8.19741 5.79988 8.51191 6.06944C8.8264 6.33901 9.29988 6.30259 9.56944 5.98809L11.25 4.02744L11.25 15C11.25 15.4142 11.5858 15.75 12 15.75Z"
|
||||
/>
|
||||
<path
|
||||
d="M16 9C15.2978 9 14.9467 9 14.6945 9.16851C14.5853 9.24148 14.4915 9.33525 14.4186 9.44446C14.25 9.69667 14.25 10.0478 14.25 10.75L14.25 15C14.25 16.2426 13.2427 17.25 12 17.25C10.7574 17.25 9.75004 16.2426 9.75004 15L9.75004 10.75C9.75004 10.0478 9.75004 9.69664 9.58149 9.4444C9.50854 9.33523 9.41481 9.2415 9.30564 9.16855C9.05341 9 8.70227 9 8 9C5.17157 9 3.75736 9 2.87868 9.87868C2 10.7574 2 12.1714 2 14.9998V15.9998C2 18.8282 2 20.2424 2.87868 21.1211C3.75736 21.9998 5.17157 21.9998 8 21.9998H16C18.8284 21.9998 20.2426 21.9998 21.1213 21.1211C22 20.2424 22 18.8282 22 15.9998V14.9998C22 12.1714 22 10.7574 21.1213 9.87868C20.2426 9 18.8284 9 16 9Z"
|
||||
/>
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.css-button:checked + div {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.css-button + div {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.css-button:checked + div + label {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
|
58
templates/error.html
Normal file
58
templates/error.html
Normal file
@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=0.90, user-scalable=no, viewport-fit=cover"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta
|
||||
name="apple-mobile-web-app-status-bar-style"
|
||||
content="black-translucent"
|
||||
/>
|
||||
<meta
|
||||
name="theme-color"
|
||||
content="#F3F4F6"
|
||||
media="(prefers-color-scheme: light)"
|
||||
/>
|
||||
<meta
|
||||
name="theme-color"
|
||||
content="#1F2937"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
/>
|
||||
|
||||
<title>Book Manager - Error</title>
|
||||
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="stylesheet" href="/assets/style.css" />
|
||||
</head>
|
||||
<body
|
||||
class="bg-gray-100 dark:bg-gray-800 flex flex-col justify-center h-screen"
|
||||
>
|
||||
<section>
|
||||
<div class="py-8 px-4 mx-auto max-w-screen-xl lg:py-16 lg:px-6">
|
||||
<div class="mx-auto max-w-screen-sm text-center">
|
||||
<h1
|
||||
class="mb-4 text-7xl tracking-tight font-extrabold lg:text-9xl text-gray-600 dark:text-gray-500"
|
||||
>
|
||||
{{ .Status }}
|
||||
</h1>
|
||||
<p
|
||||
class="mb-4 text-3xl tracking-tight font-bold text-gray-900 md:text-4xl dark:text-white"
|
||||
>
|
||||
{{ .Error }}
|
||||
</p>
|
||||
<p class="mb-8 text-lg font-light text-gray-500 dark:text-gray-400">
|
||||
{{ .Message }}
|
||||
</p>
|
||||
<a
|
||||
href="/"
|
||||
class="rounded text-center font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
>Back to Homepage</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
@ -11,7 +11,6 @@
|
||||
name="apple-mobile-web-app-status-bar-style"
|
||||
content="black-translucent"
|
||||
/>
|
||||
<link rel="manifest" href="./manifest.json" />
|
||||
<meta
|
||||
name="theme-color"
|
||||
content="#F3F4F6"
|
||||
@ -22,10 +21,11 @@
|
||||
content="#1F2937"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
/>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<title>Book Manager - {{if .Register}}Register{{else}}Login{{end}}</title>
|
||||
|
||||
<link rel="manifest" href="./manifest.json" />
|
||||
<link rel="stylesheet" href="./assets/style.css" />
|
||||
</head>
|
||||
<body class="bg-gray-100 dark:bg-gray-800 dark:text-white">
|
||||
<div class="flex flex-wrap w-full">
|
||||
|
@ -12,10 +12,13 @@
|
||||
name="apple-mobile-web-app-status-bar-style"
|
||||
content="black-translucent"
|
||||
/>
|
||||
<link rel="manifest" href="{{ .RelBase }}./manifest.json" />
|
||||
<meta name="theme-color" content="#D2B48C" />
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<title>Book Manager - {{block "title" .}}{{end}}</title>
|
||||
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="stylesheet" href="/assets/style.css" />
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
@ -166,32 +169,76 @@
|
||||
<p class="text-gray-400">Theme</p>
|
||||
<div class="flex justify-around w-full gap-4 p-2 text-sm">
|
||||
<div
|
||||
class="theme cursor-pointer rounded border border-white bg-[#fff] text-[#000] grow text-center"
|
||||
class="color-scheme cursor-pointer rounded border border-white bg-[#fff] text-[#000] grow text-center"
|
||||
>
|
||||
light
|
||||
</div>
|
||||
<div
|
||||
class="theme cursor-pointer rounded border border-white bg-[#d2b48c] text-[#333] grow text-center"
|
||||
class="color-scheme cursor-pointer rounded border border-white bg-[#d2b48c] text-[#333] grow text-center"
|
||||
>
|
||||
tan
|
||||
</div>
|
||||
<div
|
||||
class="theme cursor-pointer rounded border border-white bg-[#1f2937] text-[#fff] grow text-center"
|
||||
class="color-scheme cursor-pointer rounded border border-white bg-[#1f2937] text-[#fff] grow text-center"
|
||||
>
|
||||
blue
|
||||
</div>
|
||||
<div
|
||||
class="theme cursor-pointer rounded border border-white bg-[#232323] text-[#fff] grow text-center"
|
||||
class="color-scheme cursor-pointer rounded border border-white bg-[#232323] text-[#fff] grow text-center"
|
||||
>
|
||||
gray
|
||||
</div>
|
||||
<div
|
||||
class="theme cursor-pointer rounded border border-white bg-[#000] text-[#ccc] grow text-center"
|
||||
class="color-scheme cursor-pointer rounded border border-white bg-[#000] text-[#ccc] grow text-center"
|
||||
>
|
||||
black
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="items-center flex flex-col w-screen h-full flex-none snap-center p-2"
|
||||
>
|
||||
<p class="text-gray-400">Font</p>
|
||||
<div class="flex justify-around w-full gap-4 p-2 text-sm">
|
||||
<div
|
||||
class="font-family cursor-pointer rounded border border-white grow text-center dark:text-white"
|
||||
>
|
||||
Serif
|
||||
</div>
|
||||
<div
|
||||
class="font-family cursor-pointer rounded border border-white grow text-center dark:text-white"
|
||||
>
|
||||
Open Sans
|
||||
</div>
|
||||
<div
|
||||
class="font-family cursor-pointer rounded border border-white grow text-center dark:text-white"
|
||||
>
|
||||
Arbutus Slab
|
||||
</div>
|
||||
<div
|
||||
class="font-family cursor-pointer rounded border border-white grow text-center dark:text-white"
|
||||
>
|
||||
Lato
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="items-center flex flex-col w-screen h-full flex-none snap-center p-2"
|
||||
>
|
||||
<p class="text-gray-400">Font Size</p>
|
||||
<div class="flex justify-around w-full gap-4 p-2 text-sm">
|
||||
<div
|
||||
class="font-size cursor-pointer rounded border border-white grow text-center dark:text-white"
|
||||
>
|
||||
-
|
||||
</div>
|
||||
<div
|
||||
class="font-size cursor-pointer rounded border border-white grow text-center dark:text-white"
|
||||
>
|
||||
+
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{block "content" .}}{{end}}
|
||||
</main>
|
||||
|
44
utils/utils.go
Normal file
44
utils/utils.go
Normal file
@ -0,0 +1,44 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Reimplemented KOReader Partial MD5 Calculation
|
||||
func CalculatePartialMD5(filePath string) (string, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
var step int64 = 1024
|
||||
var size int64 = 1024
|
||||
var buf bytes.Buffer
|
||||
|
||||
for i := -1; i <= 10; i++ {
|
||||
byteStep := make([]byte, size)
|
||||
|
||||
var newShift int64 = int64(i * 2)
|
||||
var newOffset int64
|
||||
if i == -1 {
|
||||
newOffset = 0
|
||||
} else {
|
||||
newOffset = step << newShift
|
||||
}
|
||||
|
||||
_, err := file.ReadAt(byteStep, newOffset)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
buf.Write(byteStep)
|
||||
}
|
||||
|
||||
allBytes := buf.Bytes()
|
||||
return fmt.Sprintf("%x", md5.Sum(allBytes)), nil
|
||||
}
|
12
utils/utils_test.go
Normal file
12
utils/utils_test.go
Normal file
@ -0,0 +1,12 @@
|
||||
package utils
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCalculatePartialPD5(t *testing.T) {
|
||||
partialMD5, err := CalculatePartialMD5("../_test_files/alice.epub")
|
||||
|
||||
want := "386d1cb51fe4a72e5c9fdad5e059bad9"
|
||||
if partialMD5 != want {
|
||||
t.Fatalf(`Expected: %v, Got: %v, Error: %v`, want, partialMD5, err)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user