Compare commits

..

1 Commits

Author SHA1 Message Date
e68dfc445f feat(admin): adding user & importing
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-18 16:47:26 -04:00
21 changed files with 334 additions and 427 deletions

View File

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

View File

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

View File

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

View File

@ -499,8 +499,7 @@ func (api *API) koUploadExistingDocument(c *gin.Context) {
})
// Generate Storage Path
basePath := filepath.Join(api.cfg.DataPath, "documents")
safePath := filepath.Join(basePath, fileName)
safePath := filepath.Join(api.cfg.DataPath, "documents", fileName)
// Save & Prevent Overwrites
_, err = os.Stat(safePath)
@ -527,7 +526,6 @@ func (api *API) koUploadExistingDocument(c *gin.Context) {
Md5: metadataInfo.MD5,
Words: metadataInfo.WordCount,
Filepath: &fileName,
Basepath: &basePath,
}); err != nil {
log.Error("UpsertDocument DB Error:", err)
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)
}
func dict(values ...any) (map[string]any, error) {
func dict(values ...interface{}) (map[string]interface{}, error) {
if len(values)%2 != 0 {
return nil, errors.New("invalid dict call")
}
dict := make(map[string]any, len(values)/2)
dict := make(map[string]interface{}, len(values)/2)
for i := 0; i < len(values); i += 2 {
key, ok := values[i].(string)
if !ok {
@ -123,12 +123,12 @@ func dict(values ...any) (map[string]any, error) {
return dict, nil
}
func fields(value any) (map[string]any, error) {
func fields(value interface{}) (map[string]interface{}, error) {
v := reflect.Indirect(reflect.ValueOf(value))
if v.Kind() != reflect.Struct {
return nil, fmt.Errorf("%T is not a struct", value)
}
m := make(map[string]any)
m := make(map[string]interface{})
t := v.Type()
for i := 0; i < t.NumField(); i++ {
sv := t.Field(i)
@ -137,10 +137,6 @@ func fields(value any) (map[string]any, error) {
return m, nil
}
func slice(elements ...any) []any {
return elements
}
func deriveBaseFileName(metadataInfo *metadata.MetadataInfo) string {
// Derive New FileName
var newFileName string

File diff suppressed because one or more lines are too long

View File

@ -1,14 +0,0 @@
<!-- 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

@ -1,43 +0,0 @@
<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

@ -1,12 +0,0 @@
{{ 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

@ -1,20 +0,0 @@
<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

@ -1,102 +0,0 @@
{{ 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

@ -1,39 +0,0 @@
<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

@ -1,28 +0,0 @@
{{ $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,12 +17,10 @@
placeholder="JQ Filter" />
</div>
</div>
<div class="lg:w-60">
{{ template "component/button" (dict
"Title" "Filter"
"Variant" "Secondary"
) }}
</div>
<button type="submit"
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">
<span class="w-full">Filter</span>
</button>
</form>
</div>
<!-- Required for iOS "Hover" Events (onclick) -->

View File

@ -21,12 +21,10 @@
<label for="backup_documents">Documents</label>
</div>
</div>
<div class="w-40 h-10">
{{ template "component/button" (dict
"Title" "Backup"
"Variant" "Secondary"
) }}
</div>
<button type="submit"
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">
<span class="w-full">Backup</span>
</button>
</form>
<form method="POST"
enctype="multipart/form-data"
@ -36,12 +34,10 @@
<div class="flex items-center w-1/2">
<input type="file" accept=".zip" name="restore_file" class="w-full" />
</div>
<div class="w-40 h-10">
{{ template "component/button" (dict
"Title" "Restore"
"Variant" "Secondary"
) }}
</div>
<button type="submit"
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">
<span class="w-full">Restore</span>
</button>
</form>
</div>
{{ if .PasswordErrorMessage }}
@ -61,12 +57,10 @@
<td class="py-2 float-right">
<form action="./admin" method="POST">
<input type="text" name="action" value="METADATA_MATCH" class="hidden" />
<div class="w-40 h-10 text-base">
{{ template "component/button" (dict
"Title" "Run"
"Variant" "Secondary"
) }}
</div>
<button type="submit"
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">
<span class="w-full">Run</span>
</button>
</form>
</td>
</tr>
@ -77,12 +71,10 @@
<td class="py-2 float-right">
<form action="./admin" method="POST">
<input type="text" name="action" value="CACHE_TABLES" class="hidden" />
<div class="w-40 h-10 text-base">
{{ template "component/button" (dict
"Title" "Run"
"Variant" "Secondary"
) }}
</div>
<button type="submit"
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">
<span class="w-full">Run</span>
</button>
</form>
</td>
</tr>

View File

@ -33,7 +33,8 @@
action="./{{ .Data.ID }}/edit"
class="flex flex-col gap-2 w-72 text-black dark:text-white text-sm">
<input type="file" id="cover_file" name="cover_file">
{{ template "component/button" (dict "Title" "Upload Cover") }}
<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">Upload Cover</button>
</form>
<form method="POST"
action="./{{ .Data.ID }}/edit"
@ -43,7 +44,8 @@
id="remove_cover"
name="remove_cover"
class="hidden" />
{{ template "component/button" (dict "Title" "Remove Cover") }}
<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">Remove Cover</button>
</form>
</div>
<div class="relative">
@ -52,8 +54,9 @@
<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"
action="./{{ .Data.ID }}/delete"
class="text-black dark:text-white text-sm w-24">
{{ template "component/button" (dict "Title" "Delete") }}
class="text-black dark:text-white text-sm">
<button class="font-medium w-24 px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
type="submit">Delete</button>
</form>
</div>
</div>
@ -83,7 +86,8 @@
placeholder="ISBN 10 / ISBN 13"
value="{{ or .Data.Isbn13 (or .Data.Isbn10 nil) }}"
class="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white">
{{ template "component/button" (dict "Title" "Identify") }}
<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">Identify</button>
</form>
</div>
</div>
@ -96,18 +100,40 @@
</div>
</div>
<div class="grid sm:grid-cols-2 justify-between gap-4 pb-4">
{{ template "component/key-val-edit" (dict
"Title" "Title"
"Value" .Data.Title
"URL" (printf "./%s/edit" .Data.ID)
"FormValue" "title"
)}}
{{ template "component/key-val-edit" (dict
"Title" "Author"
"Value" .Data.Author
"URL" (printf "./%s/edit" .Data.ID)
"FormValue" "author"
)}}
<div class="relative">
<div class="text-gray-500 inline-flex gap-2 relative">
<p>Title</p>
<label class="my-auto" for="edit-title-button">{{ template "svg/edit" (dict "Size" 18) }}</label>
<input type="checkbox" id="edit-title-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="title" name="title" value="{{ or .Data.Title "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.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="text-gray-500 inline-flex gap-2 relative">
<p>Time Read</p>
@ -155,16 +181,119 @@
id="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>
{{ template "component/button" (dict "Title" "Save") }}
<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>
<p>{{ or .Data.Description "N/A" }}</p>
</div>
</div>
{{ template "component/metadata" (dict
"ID" .Data.ID
"Metadata" .Metadata
"Error" .MetadataError
)}}
{{ if .MetadataError }}
<div class="absolute top-0 left-0 w-full h-full z-50">
<div class="fixed top-0 left-0 bg-black opacity-50 w-screen h-screen"></div>
<div class="relative flex flex-col gap-4 p-4 max-h-[95%] bg-white dark:bg-gray-800 overflow-scroll -translate-x-2/4 -translate-y-2/4 top-1/2 left-1/2 w-5/6 overflow-hidden shadow rounded">
<div class="text-center">
<h3 class="text-lg font-bold leading-6 dark:text-gray-300">No Metadata Results Found</h3>
</div>
<a href="/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>
<style>
.css-button:checked+div {
visibility: visible;
opacity: 1;
}
.css-button+div {
visibility: hidden;
opacity: 0;
}
</style>
{{ end }}

View File

@ -18,17 +18,58 @@
placeholder="Search Author / Title" />
</div>
</div>
<div class="lg:w-60">
{{ template "component/button" (dict
"Title" "Search"
"Variant" "Secondary"
) }}
</div>
<button type="submit"
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">
<span class="w-full">Search</span>
</button>
</form>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{{ range $doc := .Data }}
{{ template "component/document-card" $doc }}
<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/{{$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 }}
</div>
<div class="w-full flex gap-4 justify-center mt-4 text-black dark:text-white">

View File

@ -41,29 +41,82 @@
</div>
</div>
<div class="grid grid-cols-2 gap-4 md:grid-cols-4">
{{ template "component/info-card" (dict
"Title" "Documents"
"Size" .Data.DatabaseInfo.DocumentsSize
"Link" "./documents"
)}}
{{ template "component/info-card" (dict
"Title" "Activity Records"
"Size" .Data.DatabaseInfo.ActivitySize
"Link" "./activity"
)}}
{{ template "component/info-card" (dict
"Title" "Progress Records"
"Size" .Data.DatabaseInfo.ProgressSize
"Link" "./progress"
)}}
{{ template "component/info-card" (dict
"Title" "Devices"
"Size" .Data.DatabaseInfo.DevicesSize
)}}
<a href="./documents" 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.DocumentsSize }}</p>
<p class="text-sm text-gray-400">Documents</p>
</div>
</div>
</a>
<a href="./activity" 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.ActivitySize }}</p>
<p class="text-sm text-gray-400">Activity Records</p>
</div>
</div>
</a>
<a href="./progress" 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.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 class="grid grid-cols-1 gap-4 md:grid-cols-2">
{{ range $item := .Data.Streaks }}
{{ template "component/streak-card" $item }}
<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 $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 }}
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">

View File

@ -39,13 +39,6 @@
{{ end }}
</tbody>
</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>
{{ end }}

View File

@ -30,12 +30,10 @@
<option value="LibGen Non-fiction">LibGen Non-fiction</option>
</select>
</div>
<div class="lg:w-60">
{{ template "component/button" (dict
"Title" "Search"
"Variant" "Secondary"
) }}
</div>
<button type="submit"
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">
<span class="w-full">Search</span>
</button>
</form>
{{ if .SearchErrorMessage }}
<span class="text-red-400 text-xs">{{ .SearchErrorMessage }}</span>

View File

@ -39,12 +39,10 @@
placeholder="New Password" />
</div>
</div>
<div class="lg:w-60">
{{ template "component/button" (dict
"Title" "Submit"
"Variant" "Secondary"
) }}
</div>
<button type="submit"
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">
<span class="w-full">Submit</span>
</button>
</form>
{{ if .PasswordErrorMessage }}
<span class="text-red-400 text-xs">{{ .PasswordErrorMessage }}</span>
@ -71,12 +69,10 @@
{{ end }}
</select>
</div>
<div class="lg:w-60">
{{ template "component/button" (dict
"Title" "Submit"
"Variant" "Secondary"
) }}
</div>
<button type="submit"
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">
<span class="w-full">Submit</span>
</button>
</form>
{{ if .TimeOffsetErrorMessage }}
<span class="text-red-400 text-xs">{{ .TimeOffsetErrorMessage }}</span>