Compare commits

..

2 Commits

Author SHA1 Message Date
7c6acad689 chore(templates): component-ize things
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-25 20:04:26 -04:00
5482899075 feat(admin): adding user & importing 2024-05-25 20:02:57 -04:00
21 changed files with 427 additions and 334 deletions

View File

@ -227,6 +227,7 @@ func (api *API) generateTemplates() *multitemplate.Renderer {
render := multitemplate.NewRenderer() render := multitemplate.NewRenderer()
helperFuncs := template.FuncMap{ helperFuncs := template.FuncMap{
"dict": dict, "dict": dict,
"slice": slice,
"fields": fields, "fields": fields,
"getSVGGraphData": getSVGGraphData, "getSVGGraphData": getSVGGraphData,
"getTimeZones": getTimeZones, "getTimeZones": getTimeZones,

View File

@ -366,13 +366,13 @@ func (api *API) appPerformAdminImport(c *gin.Context) {
} }
// Get import directory // Get import directory
baseDirectory := filepath.Clean(rAdminImport.Directory) importDirectory := filepath.Clean(rAdminImport.Directory)
// Get data directory // Get data directory
absoluteDataPath, _ := filepath.Abs(filepath.Join(api.cfg.DataPath, "documents")) absoluteDataPath, _ := filepath.Abs(filepath.Join(api.cfg.DataPath, "documents"))
// Validate different path // Validate different path
if absoluteDataPath == baseDirectory { if absoluteDataPath == importDirectory {
appErrorPage(c, http.StatusBadRequest, "Directory is the same as data path") appErrorPage(c, http.StatusBadRequest, "Directory is the same as data path")
return return
} }
@ -397,7 +397,7 @@ func (api *API) appPerformAdminImport(c *gin.Context) {
importResults := make([]importResult, 0) importResults := make([]importResult, 0)
// Walk Directory & Import // Walk Directory & Import
err = filepath.WalkDir(baseDirectory, func(currentPath string, f fs.DirEntry, err error) error { err = filepath.WalkDir(importDirectory, func(importPath string, f fs.DirEntry, err error) error {
if err != nil { if err != nil {
return err return err
} }
@ -407,7 +407,8 @@ func (api *API) appPerformAdminImport(c *gin.Context) {
} }
// Get relative path // Get relative path
relFilePath, err := filepath.Rel(baseDirectory, currentPath) basePath := importDirectory
relFilePath, err := filepath.Rel(importDirectory, importPath)
if err != nil { if err != nil {
log.Warnf("path error: %v", err) log.Warnf("path error: %v", err)
return nil return nil
@ -423,7 +424,7 @@ func (api *API) appPerformAdminImport(c *gin.Context) {
}() }()
// Get metadata // Get metadata
fileMeta, err := metadata.GetMetadata(currentPath) fileMeta, err := metadata.GetMetadata(importPath)
if err != nil { if err != nil {
log.Errorf("metadata error: %v", err) log.Errorf("metadata error: %v", err)
iResult.Error = err iResult.Error = err
@ -440,15 +441,41 @@ func (api *API) appPerformAdminImport(c *gin.Context) {
return nil return nil
} }
// TODO - Import Copy // Import Copy
// newName := deriveBaseFileName(fileMeta) if rAdminImport.Type == importCopy {
// Derive & Sanitize File Name
relFilePath = deriveBaseFileName(fileMeta)
safePath := filepath.Join(api.cfg.DataPath, "documents", relFilePath)
// Open File on Disk // Open Source File
// file, err := os.Open(currentPath) srcFile, err := os.Open(importPath)
// if err != nil { if err != nil {
// return err log.Errorf("unable to open current file: %v", err)
// } iResult.Error = err
// defer file.Close() return nil
}
defer srcFile.Close()
// Open Destination File
destFile, err := os.Create(safePath)
if err != nil {
log.Errorf("unable to open destination file: %v", err)
iResult.Error = err
return nil
}
defer destFile.Close()
// Copy File
if _, err = io.Copy(destFile, srcFile); err != nil {
log.Errorf("unable to save file: %v", err)
iResult.Error = err
return nil
}
// Update Base & Path
basePath = filepath.Join(api.cfg.DataPath, "documents")
iResult.Path = relFilePath
}
// Upsert document // Upsert document
if _, err = qtx.UpsertDocument(api.db.Ctx, database.UpsertDocumentParams{ if _, err = qtx.UpsertDocument(api.db.Ctx, database.UpsertDocumentParams{
@ -459,7 +486,7 @@ func (api *API) appPerformAdminImport(c *gin.Context) {
Md5: fileMeta.MD5, Md5: fileMeta.MD5,
Words: fileMeta.WordCount, Words: fileMeta.WordCount,
Filepath: &relFilePath, Filepath: &relFilePath,
Basepath: &baseDirectory, Basepath: &basePath,
}); err != nil { }); err != nil {
log.Errorf("UpsertDocument DB Error: %v", err) log.Errorf("UpsertDocument DB Error: %v", err)
iResult.Error = err iResult.Error = err

View File

@ -373,10 +373,9 @@ func (api *API) appGetDocumentProgress(c *gin.Context) {
DocumentID: rDoc.DocumentID, DocumentID: rDoc.DocumentID,
UserID: auth.UserName, UserID: auth.UserName,
}) })
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
log.Error("UpsertDocument DB Error: ", err) log.Error("GetDocumentProgress DB Error: ", err)
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("UpsertDocument DB Error: %v", err)) appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocumentProgress DB Error: %v", err))
return return
} }
@ -465,7 +464,8 @@ func (api *API) appUploadNewDocument(c *gin.Context) {
// Derive & Sanitize File Name // Derive & Sanitize File Name
fileName := deriveBaseFileName(metadataInfo) fileName := deriveBaseFileName(metadataInfo)
safePath := filepath.Join(api.cfg.DataPath, "documents", fileName) basePath := filepath.Join(api.cfg.DataPath, "documents")
safePath := filepath.Join(basePath, fileName)
// Open Destination File // Open Destination File
destFile, err := os.Create(safePath) destFile, err := os.Create(safePath)
@ -492,9 +492,7 @@ func (api *API) appUploadNewDocument(c *gin.Context) {
Md5: metadataInfo.MD5, Md5: metadataInfo.MD5,
Words: metadataInfo.WordCount, Words: metadataInfo.WordCount,
Filepath: &fileName, Filepath: &fileName,
Basepath: &basePath,
// TODO (BasePath):
// - Should be current config directory
}); err != nil { }); err != nil {
log.Errorf("UpsertDocument DB Error: %v", err) log.Errorf("UpsertDocument DB Error: %v", err)
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("UpsertDocument DB Error: %v", err)) appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("UpsertDocument DB Error: %v", err))
@ -805,7 +803,9 @@ func (api *API) appSaveNewDocument(c *gin.Context) {
defer sourceFile.Close() defer sourceFile.Close()
// Generate Storage Path & Open File // Generate Storage Path & Open File
safePath := filepath.Join(api.cfg.DataPath, "documents", fileName) basePath := filepath.Join(api.cfg.DataPath, "documents")
safePath := filepath.Join(basePath, fileName)
destFile, err := os.Create(safePath) destFile, err := os.Create(safePath)
if err != nil { if err != nil {
log.Error("Dest File Error: ", err) log.Error("Dest File Error: ", err)
@ -852,8 +852,9 @@ func (api *API) appSaveNewDocument(c *gin.Context) {
Title: rDocAdd.Title, Title: rDocAdd.Title,
Author: rDocAdd.Author, Author: rDocAdd.Author,
Md5: fileHash, Md5: fileHash,
Filepath: &fileName,
Words: wordCount, Words: wordCount,
Filepath: &fileName,
Basepath: &basePath,
}); err != nil { }); err != nil {
log.Error("UpsertDocument DB Error: ", err) log.Error("UpsertDocument DB Error: ", err)
sendDownloadMessage("Unable to save to database", gin.H{"Error": true}) sendDownloadMessage("Unable to save to database", gin.H{"Error": true})

View File

@ -499,7 +499,8 @@ func (api *API) koUploadExistingDocument(c *gin.Context) {
}) })
// Generate Storage Path // Generate Storage Path
safePath := filepath.Join(api.cfg.DataPath, "documents", fileName) basePath := filepath.Join(api.cfg.DataPath, "documents")
safePath := filepath.Join(basePath, fileName)
// Save & Prevent Overwrites // Save & Prevent Overwrites
_, err = os.Stat(safePath) _, err = os.Stat(safePath)
@ -526,6 +527,7 @@ func (api *API) koUploadExistingDocument(c *gin.Context) {
Md5: metadataInfo.MD5, Md5: metadataInfo.MD5,
Words: metadataInfo.WordCount, Words: metadataInfo.WordCount,
Filepath: &fileName, Filepath: &fileName,
Basepath: &basePath,
}); err != nil { }); err != nil {
log.Error("UpsertDocument DB Error:", err) log.Error("UpsertDocument DB Error:", err)
apiErrorPage(c, http.StatusBadRequest, "Document Error") apiErrorPage(c, http.StatusBadRequest, "Document Error")

View File

@ -108,11 +108,11 @@ func getSVGGraphData(inputData []database.GetDailyReadStatsRow, svgWidth int, sv
return graph.GetSVGGraphData(intData, svgWidth, svgHeight) return graph.GetSVGGraphData(intData, svgWidth, svgHeight)
} }
func dict(values ...interface{}) (map[string]interface{}, error) { func dict(values ...any) (map[string]any, error) {
if len(values)%2 != 0 { if len(values)%2 != 0 {
return nil, errors.New("invalid dict call") return nil, errors.New("invalid dict call")
} }
dict := make(map[string]interface{}, len(values)/2) dict := make(map[string]any, len(values)/2)
for i := 0; i < len(values); i += 2 { for i := 0; i < len(values); i += 2 {
key, ok := values[i].(string) key, ok := values[i].(string)
if !ok { if !ok {
@ -123,12 +123,12 @@ func dict(values ...interface{}) (map[string]interface{}, error) {
return dict, nil return dict, nil
} }
func fields(value interface{}) (map[string]interface{}, error) { func fields(value any) (map[string]any, error) {
v := reflect.Indirect(reflect.ValueOf(value)) v := reflect.Indirect(reflect.ValueOf(value))
if v.Kind() != reflect.Struct { if v.Kind() != reflect.Struct {
return nil, fmt.Errorf("%T is not a struct", value) return nil, fmt.Errorf("%T is not a struct", value)
} }
m := make(map[string]interface{}) m := make(map[string]any)
t := v.Type() t := v.Type()
for i := 0; i < t.NumField(); i++ { for i := 0; i < t.NumField(); i++ {
sv := t.Field(i) sv := t.Field(i)
@ -137,6 +137,10 @@ func fields(value interface{}) (map[string]interface{}, error) {
return m, nil return m, nil
} }
func slice(elements ...any) []any {
return elements
}
func deriveBaseFileName(metadataInfo *metadata.MetadataInfo) string { func deriveBaseFileName(metadataInfo *metadata.MetadataInfo) string {
// Derive New FileName // Derive New FileName
var newFileName string var newFileName string

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,14 @@
<!-- Variant -->
{{ $baseClass := "transition duration-100 ease-in font-medium w-full h-full px-2 py-1 text-white" }}
{{ if eq .Variant "Secondary" }}
{{ $baseClass = printf "bg-black shadow-md hover:text-black hover:bg-white %s" $baseClass }}
{{ else }}
{{ $baseClass = printf "bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100 %s" $baseClass }}
{{ end }}
<!-- Type -->
{{ if eq .Type "Link" }}
<a href="{{ .URL }}" class="text-center {{ $baseClass }}" type="submit">{{ .Title }}</a>
{{ else }}
<button class="{{ $baseClass }}" type="submit" {{ if .FormName }} form="{{ .FormName}}" {{ end }}>{{ .Title }}
</button>
{{ end }}

View File

@ -0,0 +1,43 @@
<div class="w-full relative">
<div class="flex gap-4 w-full h-full p-4 shadow-lg bg-white dark:bg-gray-700 rounded">
<div class="min-w-fit my-auto h-48 relative">
<a href="./documents/{{.ID}}">
<img class="rounded object-cover h-full" src="./documents/{{.ID}}/cover" />
</a>
</div>
<div class="flex flex-col justify-around dark:text-white w-full text-sm">
<div class="inline-flex shrink-0 items-center">
<div>
<p class="text-gray-400">Title</p>
<p class="font-medium">{{ or .Title "Unknown" }}</p>
</div>
</div>
<div class="inline-flex shrink-0 items-center">
<div>
<p class="text-gray-400">Author</p>
<p class="font-medium">{{ or .Author "Unknown" }}</p>
</div>
</div>
<div class="inline-flex shrink-0 items-center">
<div>
<p class="text-gray-400">Progress</p>
<p class="font-medium">{{ .Percentage }}%</p>
</div>
</div>
<div class="inline-flex shrink-0 items-center">
<div>
<p class="text-gray-400">Time Read</p>
<p class="font-medium">{{ niceSeconds .TotalTimeSeconds }}</p>
</div>
</div>
</div>
<div class="absolute flex flex-col gap-2 right-4 bottom-4 text-gray-500 dark:text-gray-400">
<a href="./activity?document={{ .ID }}">{{ template "svg/activity" }}</a>
{{ if .Filepath }}
<a href="./documents/{{.ID}}/file">{{ template "svg/download" }}</a>
{{ else }}
{{ template "svg/download" (dict "Disabled" true) }}
{{ end }}
</div>
</div>
</div>

View File

@ -0,0 +1,12 @@
{{ if .Link }}<a href="{{ .Link }}" {{ else }} <div {{ end }}class="w-full">
<div class="flex gap-4 w-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded">
<div class="flex flex-col justify-around dark:text-white w-full text-sm">
<p class="text-2xl font-bold text-black dark:text-white">{{ .Size }}</p>
<p class="text-sm text-gray-400">{{ .Title }}</p>
</div>
</div>
{{ if .Link }}
</a>
{{ else }}
</div>
{{ end }}

View File

@ -0,0 +1,20 @@
<div class="relative">
<div class="text-gray-500 inline-flex gap-2 relative">
<p>{{ .Title }}</p>
<label class="my-auto cursor-pointer" for="edit-{{ .FormValue }}-button">
{{ template "svg/edit" (dict "Size" 18) }}
</label>
<input type="checkbox"
id="edit-{{ .FormValue }}-button"
class="hidden css-button" />
<div class="absolute z-30 top-7 right-0 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600">
<form method="POST"
action="{{ .URL }}"
class="flex flex-col gap-2 text-black dark:text-white text-sm">
<input type="text" id="{{ .FormValue }}" name="{{ .FormValue }}" value="{{ or .Value "N/A" }}" class="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white" />
{{ template "component/button" (dict "Title" "Save") }}
</form>
</div>
</div>
<p class="font-medium text-lg">{{ or .Value "N/A" }}</p>
</div>

View File

@ -0,0 +1,102 @@
{{ if .Error }}
<div class="absolute top-0 left-0 w-full h-full z-50">
<div class="fixed top-0 left-0 bg-black opacity-50 w-screen h-screen"></div>
<div class="relative flex flex-col gap-4 p-4 max-h-[95%] bg-white dark:bg-gray-800 overflow-scroll -translate-x-2/4 -translate-y-2/4 top-1/2 left-1/2 w-5/6 overflow-hidden shadow rounded">
<div class="text-center">
<h3 class="text-lg font-bold leading-6 dark:text-gray-300">No Metadata Results Found</h3>
</div>
{{ template "component/button" (dict
"Title" "Back to Document"
"Type" "Link"
"URL" (printf "/documents/%s" .ID)
)}}
</div>
</div>
{{ end }}
{{ if .Metadata }}
<div class="absolute top-0 left-0 w-full h-full z-50">
<div class="fixed top-0 left-0 bg-black opacity-50 w-screen h-screen"></div>
<div class="relative max-h-[95%] bg-white dark:bg-gray-800 overflow-scroll -translate-x-2/4 -translate-y-2/4 top-1/2 left-1/2 w-5/6 overflow-hidden shadow rounded">
<div class="py-5 text-center">
<h3 class="text-lg font-bold leading-6 dark:text-gray-300">Metadata Results</h3>
</div>
<form id="metadata-save"
method="POST"
action="/documents/{{ .ID }}/edit"
class="text-black dark:text-white border-b dark:border-black">
<dl>
<div class="p-3 bg-gray-100 dark:bg-gray-900 grid grid-cols-3 gap-4 sm:px-6">
<dt class="my-auto font-medium text-gray-500">Cover</dt>
<dd class="mt-1 text-sm sm:mt-0 sm:col-span-2">
<img class="rounded object-fill h-32"
src="https://books.google.com/books/content/images/frontcover/{{ .Metadata.ID }}?fife=w480-h690" />
</dd>
</div>
<div class="p-3 bg-white dark:bg-gray-800 grid grid-cols-3 gap-4 sm:px-6">
<dt class="my-auto font-medium text-gray-500">Title</dt>
<dd class="mt-1 text-sm sm:mt-0 sm:col-span-2">
{{ or .Metadata.Title "N/A" }}
</dd>
</div>
<div class="p-3 bg-gray-100 dark:bg-gray-900 grid grid-cols-3 gap-4 sm:px-6">
<dt class="my-auto font-medium text-gray-500">Author</dt>
<dd class="mt-1 text-sm sm:mt-0 sm:col-span-2">
{{ or .Metadata.Author "N/A" }}
</dd>
</div>
<div class="p-3 bg-white dark:bg-gray-800 grid grid-cols-3 gap-4 sm:px-6">
<dt class="my-auto font-medium text-gray-500">ISBN 10</dt>
<dd class="mt-1 text-sm sm:mt-0 sm:col-span-2">
{{ or .Metadata.ISBN10 "N/A" }}
</dd>
</div>
<div class="p-3 bg-gray-100 dark:bg-gray-900 grid grid-cols-3 gap-4 sm:px-6">
<dt class="my-auto font-medium text-gray-500">ISBN 13</dt>
<dd class="mt-1 text-sm sm:mt-0 sm:col-span-2">
{{ or .Metadata.ISBN13 "N/A" }}
</dd>
</div>
<div class="p-3 bg-white dark:bg-gray-800 sm:grid sm:grid-cols-3 sm:gap-4 px-6">
<dt class="my-auto font-medium text-gray-500">Description</dt>
<dd class="max-h-[10em] overflow-scroll mt-1 sm:mt-0 sm:col-span-2">
{{ or .Metadata.Description "N/A" }}
</dd>
</div>
</dl>
<div class="hidden">
<input type="text" id="title" name="title" value="{{ .Metadata.Title }}" />
<input type="text" id="author" name="author" value="{{ .Metadata.Author }}" />
<input type="text"
id="description"
name="description"
value="{{ .Metadata.Description }}" />
<input type="text"
id="isbn_10"
name="isbn_10"
value="{{ .Metadata.ISBN10 }}" />
<input type="text"
id="isbn_13"
name="isbn_13"
value="{{ .Metadata.ISBN13 }}" />
<input type="text"
id="cover_gbid"
name="cover_gbid"
value="{{ .Metadata.ID }}" />
</div>
</form>
<div class="flex justify-end">
<div class="flex gap-4 m-4 w-48">
{{ template "component/button" (dict
"Title" "Cancel"
"Type" "Link"
"URL" (printf "/documents/%s" .ID)
)}}
{{ template "component/button" (dict
"Title" "Save"
"FormName" "metadata-save"
)}}
</div>
</div>
</div>
</div>
{{ end }}

View File

@ -0,0 +1,39 @@
<div class="w-full">
<div class="relative w-full px-4 py-6 bg-white shadow-lg dark:bg-gray-700 rounded">
<p class="text-sm font-semibold text-gray-700 border-b border-gray-200 w-max dark:text-white dark:border-gray-500">
{{ if eq .Window "WEEK" }}
Weekly Read Streak
{{ else }}
Daily Read Streak
{{ end }}
</p>
<div class="flex items-end my-6 space-x-2">
<p class="text-5xl font-bold text-black dark:text-white">{{ .CurrentStreak }}</p>
</div>
<div class="dark:text-white">
<div class="flex items-center justify-between pb-2 mb-2 text-sm border-b border-gray-200">
<div>
<p>
{{ if eq .Window "WEEK" }} Current Weekly Streak {{ else }}
Current Daily Streak {{ end }}
</p>
<div class="flex items-end text-sm text-gray-400">{{ .CurrentStreakStartDate }} ➞ {{ .CurrentStreakEndDate }}</div>
</div>
<div class="flex items-end font-bold">{{ .CurrentStreak }}</div>
</div>
<div class="flex items-center justify-between pb-2 mb-2 text-sm">
<div>
<p>
{{ if eq .Window "WEEK" }}
Best Weekly Streak
{{ else }}
Best Daily Streak
{{ end }}
</p>
<div class="flex items-end text-sm text-gray-400">{{ .MaxStreakStartDate }} ➞ {{ .MaxStreakEndDate }}</div>
</div>
<div class="flex items-end font-bold">{{ .MaxStreak }}</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,28 @@
{{ $rows := .Rows }}
{{ $cols := .Columns }}
{{ $keys := .Keys }}
<table class="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm">
<thead class="text-gray-800 dark:text-gray-400">
<tr>
{{ range $col := $cols }}
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">{{ $col }}</th>
{{ end }}
</tr>
</thead>
<tbody class="text-black dark:text-white">
{{ if not $rows }}
<tr>
<td class="text-center p-3" colspan="4">No Results</td>
</tr>
{{ end }}
{{ range $row := $rows }}
<tr>
{{ range $key := $keys }}
<td class="p-3 border-b border-gray-200">
<p>{{ index (fields $row) $key }}</p>
</td>
{{ end }}
</tr>
{{ end }}
</tbody>
</table>

View File

@ -17,10 +17,12 @@
placeholder="JQ Filter" /> placeholder="JQ Filter" />
</div> </div>
</div> </div>
<button type="submit" <div class="lg:w-60">
class="px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2"> {{ template "component/button" (dict
<span class="w-full">Filter</span> "Title" "Filter"
</button> "Variant" "Secondary"
) }}
</div>
</form> </form>
</div> </div>
<!-- Required for iOS "Hover" Events (onclick) --> <!-- Required for iOS "Hover" Events (onclick) -->

View File

@ -21,10 +21,12 @@
<label for="backup_documents">Documents</label> <label for="backup_documents">Documents</label>
</div> </div>
</div> </div>
<button type="submit" <div class="w-40 h-10">
class="w-40 px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2"> {{ template "component/button" (dict
<span class="w-full">Backup</span> "Title" "Backup"
</button> "Variant" "Secondary"
) }}
</div>
</form> </form>
<form method="POST" <form method="POST"
enctype="multipart/form-data" enctype="multipart/form-data"
@ -34,10 +36,12 @@
<div class="flex items-center w-1/2"> <div class="flex items-center w-1/2">
<input type="file" accept=".zip" name="restore_file" class="w-full" /> <input type="file" accept=".zip" name="restore_file" class="w-full" />
</div> </div>
<button type="submit" <div class="w-40 h-10">
class="w-40 px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2"> {{ template "component/button" (dict
<span class="w-full">Restore</span> "Title" "Restore"
</button> "Variant" "Secondary"
) }}
</div>
</form> </form>
</div> </div>
{{ if .PasswordErrorMessage }} {{ if .PasswordErrorMessage }}
@ -57,10 +61,12 @@
<td class="py-2 float-right"> <td class="py-2 float-right">
<form action="./admin" method="POST"> <form action="./admin" method="POST">
<input type="text" name="action" value="METADATA_MATCH" class="hidden" /> <input type="text" name="action" value="METADATA_MATCH" class="hidden" />
<button type="submit" <div class="w-40 h-10 text-base">
class="w-40 px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2"> {{ template "component/button" (dict
<span class="w-full">Run</span> "Title" "Run"
</button> "Variant" "Secondary"
) }}
</div>
</form> </form>
</td> </td>
</tr> </tr>
@ -71,10 +77,12 @@
<td class="py-2 float-right"> <td class="py-2 float-right">
<form action="./admin" method="POST"> <form action="./admin" method="POST">
<input type="text" name="action" value="CACHE_TABLES" class="hidden" /> <input type="text" name="action" value="CACHE_TABLES" class="hidden" />
<button type="submit" <div class="w-40 h-10 text-base">
class="w-40 px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2"> {{ template "component/button" (dict
<span class="w-full">Run</span> "Title" "Run"
</button> "Variant" "Secondary"
) }}
</div>
</form> </form>
</td> </td>
</tr> </tr>

View File

@ -33,8 +33,7 @@
action="./{{ .Data.ID }}/edit" action="./{{ .Data.ID }}/edit"
class="flex flex-col gap-2 w-72 text-black dark:text-white text-sm"> class="flex flex-col gap-2 w-72 text-black dark:text-white text-sm">
<input type="file" id="cover_file" name="cover_file"> <input type="file" id="cover_file" name="cover_file">
<button 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" {{ template "component/button" (dict "Title" "Upload Cover") }}
type="submit">Upload Cover</button>
</form> </form>
<form method="POST" <form method="POST"
action="./{{ .Data.ID }}/edit" action="./{{ .Data.ID }}/edit"
@ -44,8 +43,7 @@
id="remove_cover" id="remove_cover"
name="remove_cover" name="remove_cover"
class="hidden" /> class="hidden" />
<button 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" {{ template "component/button" (dict "Title" "Remove Cover") }}
type="submit">Remove Cover</button>
</form> </form>
</div> </div>
<div class="relative"> <div class="relative">
@ -54,9 +52,8 @@
<div class="absolute z-30 bottom-7 left-5 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600"> <div class="absolute z-30 bottom-7 left-5 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600">
<form method="POST" <form method="POST"
action="./{{ .Data.ID }}/delete" action="./{{ .Data.ID }}/delete"
class="text-black dark:text-white text-sm"> class="text-black dark:text-white text-sm w-24">
<button 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" {{ template "component/button" (dict "Title" "Delete") }}
type="submit">Delete</button>
</form> </form>
</div> </div>
</div> </div>
@ -86,8 +83,7 @@
placeholder="ISBN 10 / ISBN 13" placeholder="ISBN 10 / ISBN 13"
value="{{ or .Data.Isbn13 (or .Data.Isbn10 nil) }}" value="{{ or .Data.Isbn13 (or .Data.Isbn10 nil) }}"
class="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white"> class="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white">
<button class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100" {{ template "component/button" (dict "Title" "Identify") }}
type="submit">Identify</button>
</form> </form>
</div> </div>
</div> </div>
@ -100,40 +96,18 @@
</div> </div>
</div> </div>
<div class="grid sm:grid-cols-2 justify-between gap-4 pb-4"> <div class="grid sm:grid-cols-2 justify-between gap-4 pb-4">
<div class="relative"> {{ template "component/key-val-edit" (dict
<div class="text-gray-500 inline-flex gap-2 relative"> "Title" "Title"
<p>Title</p> "Value" .Data.Title
<label class="my-auto" for="edit-title-button">{{ template "svg/edit" (dict "Size" 18) }}</label> "URL" (printf "./%s/edit" .Data.ID)
<input type="checkbox" id="edit-title-button" class="hidden css-button" /> "FormValue" "title"
<div class="absolute z-30 top-7 right-0 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600"> )}}
<form method="POST" {{ template "component/key-val-edit" (dict
action="./{{ .Data.ID }}/edit" "Title" "Author"
class="flex flex-col gap-2 text-black dark:text-white text-sm"> "Value" .Data.Author
<input type="text" id="title" name="title" value="{{ or .Data.Title "N/A" }}" class="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white"> "URL" (printf "./%s/edit" .Data.ID)
<button 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" "FormValue" "author"
type="submit">Save</button> )}}
</form>
</div>
</div>
<p class="font-medium text-lg">{{ or .Data.Title "N/A" }}</p>
</div>
<div class="relative">
<div class="text-gray-500 inline-flex gap-2 relative">
<p>Author</p>
<label class="my-auto" for="edit-author-button">{{ template "svg/edit" (dict "Size" 18) }}</label>
<input type="checkbox" id="edit-author-button" class="hidden css-button" />
<div class="absolute z-30 top-7 right-0 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600">
<form method="POST"
action="./{{ .Data.ID }}/edit"
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-300 text-black dark:bg-gray-700 dark:text-white">
<button class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 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" }}</p>
</div>
<div class="relative"> <div class="relative">
<div class="text-gray-500 inline-flex gap-2 relative"> <div class="text-gray-500 inline-flex gap-2 relative">
<p>Time Read</p> <p>Time Read</p>
@ -181,119 +155,16 @@
id="description" id="description"
name="description" name="description"
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> class="h-full w-full p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white">{{ or .Data.Description "N/A" }}</textarea>
<button class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100" {{ template "component/button" (dict "Title" "Save") }}
type="submit">Save</button>
</form> </form>
</div> </div>
<p>{{ or .Data.Description "N/A" }}</p> <p>{{ or .Data.Description "N/A" }}</p>
</div> </div>
</div> </div>
{{ if .MetadataError }} {{ template "component/metadata" (dict
<div class="absolute top-0 left-0 w-full h-full z-50"> "ID" .Data.ID
<div class="fixed top-0 left-0 bg-black opacity-50 w-screen h-screen"></div> "Metadata" .Metadata
<div class="relative flex flex-col gap-4 p-4 max-h-[95%] bg-white dark:bg-gray-800 overflow-scroll -translate-x-2/4 -translate-y-2/4 top-1/2 left-1/2 w-5/6 overflow-hidden shadow rounded"> "Error" .MetadataError
<div class="text-center"> )}}
<h3 class="text-lg font-bold leading-6 dark:text-gray-300">No Metadata Results Found</h3>
</div>
<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>
</div>
{{ end }}
<!-- Metadata Info -->
{{ if .Metadata }}
<div class="absolute top-0 left-0 w-full h-full z-50">
<div class="fixed top-0 left-0 bg-black opacity-50 w-screen h-screen"></div>
<div class="relative max-h-[95%] bg-white dark:bg-gray-800 overflow-scroll -translate-x-2/4 -translate-y-2/4 top-1/2 left-1/2 w-5/6 overflow-hidden shadow rounded">
<div class="py-5 text-center">
<h3 class="text-lg font-bold leading-6 dark:text-gray-300">Metadata Results</h3>
</div>
<form id="metadata-save"
method="POST"
action="/documents/{{ .Data.ID }}/edit"
class="text-black dark:text-white border-b dark:border-black">
<dl>
<div class="p-3 bg-gray-100 dark:bg-gray-900 grid grid-cols-3 gap-4 sm:px-6">
<dt class="my-auto font-medium text-gray-500">Cover</dt>
<dd class="mt-1 text-sm sm:mt-0 sm:col-span-2">
<img class="rounded object-fill h-32"
src="https://books.google.com/books/content/images/frontcover/{{ .Metadata.ID }}?fife=w480-h690" />
</dd>
</div>
<div class="p-3 bg-white dark:bg-gray-800 grid grid-cols-3 gap-4 sm:px-6">
<dt class="my-auto font-medium text-gray-500">Title</dt>
<dd class="mt-1 text-sm sm:mt-0 sm:col-span-2">
{{ or .Metadata.Title "N/A" }}
</dd>
</div>
<div class="p-3 bg-gray-100 dark:bg-gray-900 grid grid-cols-3 gap-4 sm:px-6">
<dt class="my-auto font-medium text-gray-500">Author</dt>
<dd class="mt-1 text-sm sm:mt-0 sm:col-span-2">
{{ or .Metadata.Author "N/A" }}
</dd>
</div>
<div class="p-3 bg-white dark:bg-gray-800 grid grid-cols-3 gap-4 sm:px-6">
<dt class="my-auto font-medium text-gray-500">ISBN 10</dt>
<dd class="mt-1 text-sm sm:mt-0 sm:col-span-2">
{{ or .Metadata.ISBN10 "N/A" }}
</dd>
</div>
<div class="p-3 bg-gray-100 dark:bg-gray-900 grid grid-cols-3 gap-4 sm:px-6">
<dt class="my-auto font-medium text-gray-500">ISBN 13</dt>
<dd class="mt-1 text-sm sm:mt-0 sm:col-span-2">
{{ or .Metadata.ISBN13 "N/A" }}
</dd>
</div>
<div class="p-3 bg-white dark:bg-gray-800 sm:grid sm:grid-cols-3 sm:gap-4 px-6">
<dt class="my-auto font-medium text-gray-500">Description</dt>
<dd class="max-h-[10em] overflow-scroll mt-1 sm:mt-0 sm:col-span-2">
{{ or .Metadata.Description "N/A" }}
</dd>
</div>
</dl>
<div class="hidden">
<input type="text" id="title" name="title" value="{{ .Metadata.Title }}">
<input type="text" id="author" name="author" value="{{ .Metadata.Author }}">
<input type="text"
id="description"
name="description"
value="{{ .Metadata.Description }}">
<input type="text"
id="isbn_10"
name="isbn_10"
value="{{ .Metadata.ISBN10 }}">
<input type="text"
id="isbn_13"
name="isbn_13"
value="{{ .Metadata.ISBN13 }}">
<input type="text"
id="cover_gbid"
name="cover_gbid"
value="{{ .Metadata.ID }}">
</div>
</form>
<div class="flex justify-end gap-4 m-4">
<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:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
type="submit">Save</button>
</div>
</div>
</div>
{{ end }}
</div> </div>
<style>
.css-button:checked+div {
visibility: visible;
opacity: 1;
}
.css-button+div {
visibility: hidden;
opacity: 0;
}
</style>
{{ end }} {{ end }}

View File

@ -18,58 +18,17 @@
placeholder="Search Author / Title" /> placeholder="Search Author / Title" />
</div> </div>
</div> </div>
<button type="submit" <div class="lg:w-60">
class="px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2"> {{ template "component/button" (dict
<span class="w-full">Search</span> "Title" "Search"
</button> "Variant" "Secondary"
) }}
</div>
</form> </form>
</div> </div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{{ range $doc := .Data }} {{ range $doc := .Data }}
<div class="w-full relative"> {{ template "component/document-card" $doc }}
<div class="flex gap-4 w-full h-full p-4 shadow-lg bg-white dark:bg-gray-700 rounded">
<div class="min-w-fit my-auto h-48 relative">
<a href="./documents/{{$doc.ID}}">
<img class="rounded object-cover h-full"
src="./documents/{{$doc.ID}}/cover" />
</a>
</div>
<div class="flex flex-col justify-around dark:text-white w-full text-sm">
<div class="inline-flex shrink-0 items-center">
<div>
<p class="text-gray-400">Title</p>
<p class="font-medium">{{ or $doc.Title "Unknown" }}</p>
</div>
</div>
<div class="inline-flex shrink-0 items-center">
<div>
<p class="text-gray-400">Author</p>
<p class="font-medium">{{ or $doc.Author "Unknown" }}</p>
</div>
</div>
<div class="inline-flex shrink-0 items-center">
<div>
<p class="text-gray-400">Progress</p>
<p class="font-medium">{{ $doc.Percentage }}%</p>
</div>
</div>
<div class="inline-flex shrink-0 items-center">
<div>
<p class="text-gray-400">Time Read</p>
<p class="font-medium">{{ niceSeconds $doc.TotalTimeSeconds }}</p>
</div>
</div>
</div>
<div class="absolute flex flex-col gap-2 right-4 bottom-4 text-gray-500 dark:text-gray-400">
<a href="./activity?document={{ $doc.ID }}">{{ template "svg/activity" }}</a>
{{ if $doc.Filepath }}
<a href="./documents/{{$doc.ID}}/file">{{ template "svg/download" }}</a>
{{ else }}
{{ template "svg/download" (dict "Disabled" true) }}
{{ end }}
</div>
</div>
</div>
{{ end }} {{ end }}
</div> </div>
<div class="w-full flex gap-4 justify-center mt-4 text-black dark:text-white"> <div class="w-full flex gap-4 justify-center mt-4 text-black dark:text-white">

View File

@ -41,82 +41,29 @@
</div> </div>
</div> </div>
<div class="grid grid-cols-2 gap-4 md:grid-cols-4"> <div class="grid grid-cols-2 gap-4 md:grid-cols-4">
<a href="./documents" class="w-full"> {{ template "component/info-card" (dict
<div class="flex gap-4 w-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded"> "Title" "Documents"
<div class="flex flex-col justify-around dark:text-white w-full text-sm"> "Size" .Data.DatabaseInfo.DocumentsSize
<p class="text-2xl font-bold text-black dark:text-white">{{ .Data.DatabaseInfo.DocumentsSize }}</p> "Link" "./documents"
<p class="text-sm text-gray-400">Documents</p> )}}
</div> {{ template "component/info-card" (dict
</div> "Title" "Activity Records"
</a> "Size" .Data.DatabaseInfo.ActivitySize
<a href="./activity" class="w-full"> "Link" "./activity"
<div class="flex gap-4 w-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded"> )}}
<div class="flex flex-col justify-around dark:text-white w-full text-sm"> {{ template "component/info-card" (dict
<p class="text-2xl font-bold text-black dark:text-white">{{ .Data.DatabaseInfo.ActivitySize }}</p> "Title" "Progress Records"
<p class="text-sm text-gray-400">Activity Records</p> "Size" .Data.DatabaseInfo.ProgressSize
</div> "Link" "./progress"
</div> )}}
</a> {{ template "component/info-card" (dict
<a href="./progress" class="w-full"> "Title" "Devices"
<div class="flex gap-4 w-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded"> "Size" .Data.DatabaseInfo.DevicesSize
<div class="flex flex-col justify-around dark:text-white w-full text-sm"> )}}
<p class="text-2xl font-bold text-black dark:text-white">{{ .Data.DatabaseInfo.ProgressSize }}</p>
<p class="text-sm text-gray-400">Progress Records</p>
</div>
</div>
</a>
<div class="w-full">
<div class="flex gap-4 w-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded">
<div class="flex flex-col justify-around dark:text-white w-full text-sm">
<p class="text-2xl font-bold text-black dark:text-white">{{ .Data.DatabaseInfo.DevicesSize }}</p>
<p class="text-sm text-gray-400">Devices</p>
</div>
</div>
</div>
</div> </div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
{{ range $item := .Data.Streaks }} {{ range $item := .Data.Streaks }}
<div class="w-full"> {{ template "component/streak-card" $item }}
<div class="relative w-full px-4 py-6 bg-white shadow-lg dark:bg-gray-700 rounded">
<p class="text-sm font-semibold text-gray-700 border-b border-gray-200 w-max dark:text-white dark:border-gray-500">
{{ if eq $item.Window "WEEK" }}
Weekly Read Streak
{{ else }}
Daily Read Streak
{{ end }}
</p>
<div class="flex items-end my-6 space-x-2">
<p class="text-5xl font-bold text-black dark:text-white">{{ $item.CurrentStreak }}</p>
</div>
<div class="dark:text-white">
<div class="flex items-center justify-between pb-2 mb-2 text-sm border-b border-gray-200">
<div>
<p>
{{ if eq $item.Window "WEEK" }} Current Weekly Streak {{ else }}
Current Daily Streak {{ end }}
</p>
<div class="flex items-end text-sm text-gray-400">
{{ $item.CurrentStreakStartDate }} ➞ {{ $item.CurrentStreakEndDate }}
</div>
</div>
<div class="flex items-end font-bold">{{ $item.CurrentStreak }}</div>
</div>
<div class="flex items-center justify-between pb-2 mb-2 text-sm">
<div>
<p>
{{ if eq $item.Window "WEEK" }}
Best Weekly Streak
{{ else }}
Best Daily Streak
{{ end }}
</p>
<div class="flex items-end text-sm text-gray-400">{{ $item.MaxStreakStartDate }} ➞ {{ $item.MaxStreakEndDate }}</div>
</div>
<div class="flex items-end font-bold">{{ $item.MaxStreak }}</div>
</div>
</div>
</div>
</div>
{{ end }} {{ end }}
</div> </div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">

View File

@ -39,6 +39,13 @@
{{ end }} {{ end }}
</tbody> </tbody>
</table> </table>
<!--
{{ template "component/table" (dict
"Columns" (slice "Author" "Title" "Device Name" "Percentage" "Created At")
"Keys" (slice "Author" "Title" "DeviceName" "Percentage" "CreatedAt")
"Rows" .Data
)}}
-->
</div> </div>
</div> </div>
{{ end }} {{ end }}

View File

@ -30,10 +30,12 @@
<option value="LibGen Non-fiction">LibGen Non-fiction</option> <option value="LibGen Non-fiction">LibGen Non-fiction</option>
</select> </select>
</div> </div>
<button type="submit" <div class="lg:w-60">
class="px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2"> {{ template "component/button" (dict
<span class="w-full">Search</span> "Title" "Search"
</button> "Variant" "Secondary"
) }}
</div>
</form> </form>
{{ if .SearchErrorMessage }} {{ if .SearchErrorMessage }}
<span class="text-red-400 text-xs">{{ .SearchErrorMessage }}</span> <span class="text-red-400 text-xs">{{ .SearchErrorMessage }}</span>

View File

@ -39,10 +39,12 @@
placeholder="New Password" /> placeholder="New Password" />
</div> </div>
</div> </div>
<button type="submit" <div class="lg:w-60">
class="px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2"> {{ template "component/button" (dict
<span class="w-full">Submit</span> "Title" "Submit"
</button> "Variant" "Secondary"
) }}
</div>
</form> </form>
{{ if .PasswordErrorMessage }} {{ if .PasswordErrorMessage }}
<span class="text-red-400 text-xs">{{ .PasswordErrorMessage }}</span> <span class="text-red-400 text-xs">{{ .PasswordErrorMessage }}</span>
@ -69,10 +71,12 @@
{{ end }} {{ end }}
</select> </select>
</div> </div>
<button type="submit" <div class="lg:w-60">
class="px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2"> {{ template "component/button" (dict
<span class="w-full">Submit</span> "Title" "Submit"
</button> "Variant" "Secondary"
) }}
</div>
</form> </form>
{{ if .TimeOffsetErrorMessage }} {{ if .TimeOffsetErrorMessage }}
<span class="text-red-400 text-xs">{{ .TimeOffsetErrorMessage }}</span> <span class="text-red-400 text-xs">{{ .TimeOffsetErrorMessage }}</span>