Compare commits
1 Commits
master
...
e68dfc445f
| Author | SHA1 | Date | |
|---|---|---|---|
| e68dfc445f |
@@ -1,11 +1,7 @@
|
|||||||
kind: pipeline
|
kind: pipeline
|
||||||
type: docker
|
type: kubernetes
|
||||||
name: default
|
name: default
|
||||||
|
|
||||||
trigger:
|
|
||||||
branch:
|
|
||||||
- master
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# Unit Tests
|
# Unit Tests
|
||||||
- name: tests
|
- name: tests
|
||||||
@@ -27,8 +23,6 @@ steps:
|
|||||||
registry: gitea.va.reichard.io
|
registry: gitea.va.reichard.io
|
||||||
tags:
|
tags:
|
||||||
- dev
|
- dev
|
||||||
custom_dns:
|
|
||||||
- 8.8.8.8
|
|
||||||
username:
|
username:
|
||||||
from_secret: docker_username
|
from_secret: docker_username
|
||||||
password:
|
password:
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,4 +4,3 @@ data/
|
|||||||
build/
|
build/
|
||||||
.direnv/
|
.direnv/
|
||||||
cover.html
|
cover.html
|
||||||
node_modules
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"plugins": ["prettier-plugin-go-template"]
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ FROM alpine AS alpine
|
|||||||
RUN apk update && apk add --no-cache ca-certificates tzdata
|
RUN apk update && apk add --no-cache ca-certificates tzdata
|
||||||
|
|
||||||
# Build Image
|
# Build Image
|
||||||
FROM golang:1.24 AS build
|
FROM golang:1.21 AS build
|
||||||
|
|
||||||
# Create Package Directory
|
# Create Package Directory
|
||||||
RUN mkdir -p /opt/antholume
|
RUN mkdir -p /opt/antholume
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -27,7 +27,7 @@ docker_build_release_latest: build_tailwind
|
|||||||
--push .
|
--push .
|
||||||
|
|
||||||
build_tailwind:
|
build_tailwind:
|
||||||
tailwindcss build -o ./assets/style.css --minify
|
tailwind build -o ./assets/style.css --minify
|
||||||
|
|
||||||
dev: build_tailwind
|
dev: build_tailwind
|
||||||
GIN_MODE=release \
|
GIN_MODE=release \
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ See documentation in the `client` subfolder: [SyncNinja](https://gitea.va.reicha
|
|||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
SQLC Generation (v1.26.0):
|
SQLC Generation (v1.21.0):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
|
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
|
||||||
|
|||||||
131
api/api.go
131
api/api.go
@@ -6,7 +6,6 @@ import (
|
|||||||
"html/template"
|
"html/template"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -16,7 +15,6 @@ import (
|
|||||||
"github.com/gin-contrib/sessions/cookie"
|
"github.com/gin-contrib/sessions/cookie"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/microcosm-cc/bluemonday"
|
"github.com/microcosm-cc/bluemonday"
|
||||||
"github.com/pkg/errors"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"reichard.io/antholume/config"
|
"reichard.io/antholume/config"
|
||||||
"reichard.io/antholume/database"
|
"reichard.io/antholume/database"
|
||||||
@@ -39,7 +37,6 @@ func NewApi(db *database.DBManager, c *config.Config, assets fs.FS) *API {
|
|||||||
db: db,
|
db: db,
|
||||||
cfg: c,
|
cfg: c,
|
||||||
assets: assets,
|
assets: assets,
|
||||||
templates: make(map[string]*template.Template),
|
|
||||||
userAuthCache: make(map[string]string),
|
userAuthCache: make(map[string]string),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,11 +223,10 @@ func (api *API) registerOPDSRoutes(apiGroup *gin.RouterGroup) {
|
|||||||
|
|
||||||
func (api *API) generateTemplates() *multitemplate.Renderer {
|
func (api *API) generateTemplates() *multitemplate.Renderer {
|
||||||
// Define templates & helper functions
|
// Define templates & helper functions
|
||||||
render := multitemplate.NewRenderer()
|
|
||||||
templates := make(map[string]*template.Template)
|
templates := make(map[string]*template.Template)
|
||||||
|
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,
|
||||||
@@ -239,99 +235,55 @@ func (api *API) generateTemplates() *multitemplate.Renderer {
|
|||||||
"niceSeconds": niceSeconds,
|
"niceSeconds": niceSeconds,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load Base
|
// Load base
|
||||||
b, err := fs.ReadFile(api.assets, "templates/base.tmpl")
|
b, _ := fs.ReadFile(api.assets, "templates/base.tmpl")
|
||||||
if err != nil {
|
baseTemplate := template.Must(template.New("base").Funcs(helperFuncs).Parse(string(b)))
|
||||||
log.Errorf("error reading base template: %v", err)
|
|
||||||
return &render
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse Base
|
|
||||||
baseTemplate, err := template.New("base").Funcs(helperFuncs).Parse(string(b))
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("error parsing base template: %v", err)
|
|
||||||
return &render
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load SVGs
|
// Load SVGs
|
||||||
err = api.loadTemplates("svg", baseTemplate, templates, false)
|
svgs, _ := fs.ReadDir(api.assets, "templates/svgs")
|
||||||
if err != nil {
|
for _, item := range svgs {
|
||||||
log.Errorf("error loading svg templates: %v", err)
|
basename := item.Name()
|
||||||
return &render
|
path := fmt.Sprintf("templates/svgs/%s", basename)
|
||||||
|
name := strings.TrimSuffix(basename, filepath.Ext(basename))
|
||||||
|
|
||||||
|
b, _ := fs.ReadFile(api.assets, path)
|
||||||
|
baseTemplate = template.Must(baseTemplate.New("svg/" + name).Parse(string(b)))
|
||||||
|
templates["svg/"+name] = baseTemplate
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load Components
|
// Load components
|
||||||
err = api.loadTemplates("component", baseTemplate, templates, false)
|
components, _ := fs.ReadDir(api.assets, "templates/components")
|
||||||
if err != nil {
|
for _, item := range components {
|
||||||
log.Errorf("error loading component templates: %v", err)
|
basename := item.Name()
|
||||||
return &render
|
path := fmt.Sprintf("templates/components/%s", basename)
|
||||||
|
name := strings.TrimSuffix(basename, filepath.Ext(basename))
|
||||||
|
|
||||||
|
// Clone Base Template
|
||||||
|
b, _ := fs.ReadFile(api.assets, path)
|
||||||
|
baseTemplate = template.Must(baseTemplate.New("component/" + name).Parse(string(b)))
|
||||||
|
render.Add("component/"+name, baseTemplate)
|
||||||
|
templates["component/"+name] = baseTemplate
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load Pages
|
// Load pages
|
||||||
err = api.loadTemplates("page", baseTemplate, templates, true)
|
pages, _ := fs.ReadDir(api.assets, "templates/pages")
|
||||||
if err != nil {
|
for _, item := range pages {
|
||||||
log.Errorf("error loading page templates: %v", err)
|
basename := item.Name()
|
||||||
return &render
|
path := fmt.Sprintf("templates/pages/%s", basename)
|
||||||
|
name := strings.TrimSuffix(basename, filepath.Ext(basename))
|
||||||
|
|
||||||
|
// Clone Base Template
|
||||||
|
b, _ := fs.ReadFile(api.assets, path)
|
||||||
|
pageTemplate, _ := template.Must(baseTemplate.Clone()).New("page/" + name).Parse(string(b))
|
||||||
|
render.Add("page/"+name, pageTemplate)
|
||||||
|
templates["page/"+name] = pageTemplate
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate Renderer
|
|
||||||
api.templates = templates
|
api.templates = templates
|
||||||
for templateName, templateValue := range templates {
|
|
||||||
render.Add(templateName, templateValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &render
|
return &render
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) loadTemplates(
|
|
||||||
basePath string,
|
|
||||||
baseTemplate *template.Template,
|
|
||||||
allTemplates map[string]*template.Template,
|
|
||||||
cloneBase bool,
|
|
||||||
) error {
|
|
||||||
// Load Templates (Pluralize)
|
|
||||||
templateDirectory := fmt.Sprintf("templates/%ss", basePath)
|
|
||||||
allFiles, err := fs.ReadDir(api.assets, templateDirectory)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, fmt.Sprintf("unable to read template dir: %s", templateDirectory))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate Templates
|
|
||||||
for _, item := range allFiles {
|
|
||||||
templateFile := item.Name()
|
|
||||||
templatePath := path.Join(templateDirectory, templateFile)
|
|
||||||
templateName := fmt.Sprintf("%s/%s", basePath, strings.TrimSuffix(templateFile, filepath.Ext(templateFile)))
|
|
||||||
|
|
||||||
// Read Template
|
|
||||||
b, err := fs.ReadFile(api.assets, templatePath)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, fmt.Sprintf("unable to read template: %s", templateName))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clone? (Pages - Don't Stomp)
|
|
||||||
if cloneBase {
|
|
||||||
baseTemplate = template.Must(baseTemplate.Clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse Template
|
|
||||||
baseTemplate, err = baseTemplate.New(templateName).Parse(string(b))
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, fmt.Sprintf("unable to parse template: %s", templateName))
|
|
||||||
}
|
|
||||||
|
|
||||||
allTemplates[templateName] = baseTemplate
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) templateMiddleware(router *gin.Engine) gin.HandlerFunc {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
router.HTMLRender = *api.generateTemplates()
|
|
||||||
c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func loggingMiddleware(c *gin.Context) {
|
func loggingMiddleware(c *gin.Context) {
|
||||||
// Start timer
|
// Start timer
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
@@ -347,7 +299,7 @@ func loggingMiddleware(c *gin.Context) {
|
|||||||
logData := log.Fields{
|
logData := log.Fields{
|
||||||
"type": "access",
|
"type": "access",
|
||||||
"ip": c.ClientIP(),
|
"ip": c.ClientIP(),
|
||||||
"latency": latency.String(),
|
"latency": fmt.Sprintf("%s", latency),
|
||||||
"status": c.Writer.Status(),
|
"status": c.Writer.Status(),
|
||||||
"method": c.Request.Method,
|
"method": c.Request.Method,
|
||||||
"path": c.Request.URL.Path,
|
"path": c.Request.URL.Path,
|
||||||
@@ -367,3 +319,10 @@ func loggingMiddleware(c *gin.Context) {
|
|||||||
// Log result
|
// Log result
|
||||||
log.WithFields(logData).Info(fmt.Sprintf("%s %s", c.Request.Method, c.Request.URL.Path))
|
log.WithFields(logData).Info(fmt.Sprintf("%s %s", c.Request.Method, c.Request.URL.Path))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *API) templateMiddleware(router *gin.Engine) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
router.HTMLRender = *api.generateTemplates()
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package api
|
|||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -72,7 +71,7 @@ const (
|
|||||||
type requestAdminUpdateUser struct {
|
type requestAdminUpdateUser struct {
|
||||||
User string `form:"user"`
|
User string `form:"user"`
|
||||||
Password *string `form:"password"`
|
Password *string `form:"password"`
|
||||||
IsAdmin *bool `form:"is_admin"`
|
isAdmin *bool `form:"is_admin"`
|
||||||
Operation operationType `form:"operation"`
|
Operation operationType `form:"operation"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +112,7 @@ func (api *API) appPerformAdminAction(c *gin.Context) {
|
|||||||
// 2. Select all / deselect?
|
// 2. Select all / deselect?
|
||||||
case adminCacheTables:
|
case adminCacheTables:
|
||||||
go func() {
|
go func() {
|
||||||
err := api.db.CacheTempTables(c)
|
err := api.db.CacheTempTables()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to cache temp tables: ", err)
|
log.Error("Unable to cache temp tables: ", err)
|
||||||
}
|
}
|
||||||
@@ -123,7 +122,7 @@ func (api *API) appPerformAdminAction(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
case adminBackup:
|
case adminBackup:
|
||||||
// Vacuum
|
// Vacuum
|
||||||
_, err := api.db.DB.ExecContext(c, "VACUUM;")
|
_, err := api.db.DB.ExecContext(api.db.Ctx, "VACUUM;")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to vacuum DB: ", err)
|
log.Error("Unable to vacuum DB: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to vacuum database")
|
appErrorPage(c, http.StatusInternalServerError, "Unable to vacuum database")
|
||||||
@@ -145,7 +144,7 @@ func (api *API) appPerformAdminAction(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err := api.createBackup(c, w, directories)
|
err := api.createBackup(w, directories)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Backup Error: ", err)
|
log.Error("Backup Error: ", err)
|
||||||
}
|
}
|
||||||
@@ -175,10 +174,7 @@ func (api *API) appGetAdminLogs(c *gin.Context) {
|
|||||||
rAdminLogs.Filter = strings.TrimSpace(rAdminLogs.Filter)
|
rAdminLogs.Filter = strings.TrimSpace(rAdminLogs.Filter)
|
||||||
|
|
||||||
var jqFilter *gojq.Code
|
var jqFilter *gojq.Code
|
||||||
var basicFilter string
|
if rAdminLogs.Filter != "" {
|
||||||
if strings.HasPrefix(rAdminLogs.Filter, "\"") && strings.HasSuffix(rAdminLogs.Filter, "\"") {
|
|
||||||
basicFilter = rAdminLogs.Filter[1 : len(rAdminLogs.Filter)-1]
|
|
||||||
} else if rAdminLogs.Filter != "" {
|
|
||||||
parsed, err := gojq.Parse(rAdminLogs.Filter)
|
parsed, err := gojq.Parse(rAdminLogs.Filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to parse JQ filter")
|
log.Error("Unable to parse JQ filter")
|
||||||
@@ -210,7 +206,7 @@ func (api *API) appGetAdminLogs(c *gin.Context) {
|
|||||||
rawLog := scanner.Text()
|
rawLog := scanner.Text()
|
||||||
|
|
||||||
// Attempt JSON Pretty
|
// Attempt JSON Pretty
|
||||||
var jsonMap map[string]any
|
var jsonMap map[string]interface{}
|
||||||
err := json.Unmarshal([]byte(rawLog), &jsonMap)
|
err := json.Unmarshal([]byte(rawLog), &jsonMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logLines = append(logLines, scanner.Text())
|
logLines = append(logLines, scanner.Text())
|
||||||
@@ -224,14 +220,9 @@ func (api *API) appGetAdminLogs(c *gin.Context) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Basic Filter
|
// No Filter
|
||||||
if basicFilter != "" && strings.Contains(string(rawData), basicFilter) {
|
|
||||||
logLines = append(logLines, string(rawData))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// No JQ Filter
|
|
||||||
if jqFilter == nil {
|
if jqFilter == nil {
|
||||||
|
logLines = append(logLines, string(rawData))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,7 +253,7 @@ func (api *API) appGetAdminLogs(c *gin.Context) {
|
|||||||
func (api *API) appGetAdminUsers(c *gin.Context) {
|
func (api *API) appGetAdminUsers(c *gin.Context) {
|
||||||
templateVars, _ := api.getBaseTemplateVars("admin-users", c)
|
templateVars, _ := api.getBaseTemplateVars("admin-users", c)
|
||||||
|
|
||||||
users, err := api.db.Queries.GetUsers(c)
|
users, err := api.db.Queries.GetUsers(api.db.Ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetUsers DB Error: ", err)
|
log.Error("GetUsers DB Error: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUsers DB Error: %v", err))
|
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUsers DB Error: %v", err))
|
||||||
@@ -277,27 +268,21 @@ func (api *API) appGetAdminUsers(c *gin.Context) {
|
|||||||
func (api *API) appUpdateAdminUsers(c *gin.Context) {
|
func (api *API) appUpdateAdminUsers(c *gin.Context) {
|
||||||
templateVars, _ := api.getBaseTemplateVars("admin-users", c)
|
templateVars, _ := api.getBaseTemplateVars("admin-users", c)
|
||||||
|
|
||||||
var rUpdate requestAdminUpdateUser
|
var rAdminUserUpdate requestAdminUpdateUser
|
||||||
if err := c.ShouldBind(&rUpdate); err != nil {
|
if err := c.ShouldBind(&rAdminUserUpdate); err != nil {
|
||||||
log.Error("Invalid URI Bind")
|
log.Error("Invalid URI Bind")
|
||||||
appErrorPage(c, http.StatusNotFound, "Invalid user parameters")
|
appErrorPage(c, http.StatusNotFound, "Invalid user update")
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure Username
|
|
||||||
if rUpdate.User == "" {
|
|
||||||
appErrorPage(c, http.StatusInternalServerError, "User cannot be empty")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
switch rUpdate.Operation {
|
switch rAdminUserUpdate.Operation {
|
||||||
case opCreate:
|
case opCreate:
|
||||||
err = api.createUser(c, rUpdate.User, rUpdate.Password, rUpdate.IsAdmin)
|
err = api.createUser(rAdminUserUpdate)
|
||||||
case opUpdate:
|
case opUpdate:
|
||||||
err = api.updateUser(c, rUpdate.User, rUpdate.Password, rUpdate.IsAdmin)
|
err = api.updateUser(rAdminUserUpdate)
|
||||||
case opDelete:
|
case opDelete:
|
||||||
err = api.deleteUser(c, rUpdate.User)
|
err = api.deleteUser(rAdminUserUpdate)
|
||||||
default:
|
default:
|
||||||
appErrorPage(c, http.StatusNotFound, "Unknown user operation")
|
appErrorPage(c, http.StatusNotFound, "Unknown user operation")
|
||||||
return
|
return
|
||||||
@@ -308,7 +293,7 @@ func (api *API) appUpdateAdminUsers(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
users, err := api.db.Queries.GetUsers(c)
|
users, err := api.db.Queries.GetUsers(api.db.Ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetUsers DB Error: ", err)
|
log.Error("GetUsers DB Error: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUsers DB Error: %v", err))
|
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUsers DB Error: %v", err))
|
||||||
@@ -381,13 +366,13 @@ func (api *API) appPerformAdminImport(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get import directory
|
// Get import directory
|
||||||
importDirectory := filepath.Clean(rAdminImport.Directory)
|
baseDirectory := 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 == importDirectory {
|
if absoluteDataPath == baseDirectory {
|
||||||
appErrorPage(c, http.StatusBadRequest, "Directory is the same as data path")
|
appErrorPage(c, http.StatusBadRequest, "Directory is the same as data path")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -412,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(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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -422,8 +407,7 @@ func (api *API) appPerformAdminImport(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get relative path
|
// Get relative path
|
||||||
basePath := importDirectory
|
relFilePath, err := filepath.Rel(baseDirectory, currentPath)
|
||||||
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
|
||||||
@@ -439,7 +423,7 @@ func (api *API) appPerformAdminImport(c *gin.Context) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// Get metadata
|
// Get metadata
|
||||||
fileMeta, err := metadata.GetMetadata(importPath)
|
fileMeta, err := metadata.GetMetadata(currentPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("metadata error: %v", err)
|
log.Errorf("metadata error: %v", err)
|
||||||
iResult.Error = err
|
iResult.Error = err
|
||||||
@@ -449,51 +433,25 @@ func (api *API) appPerformAdminImport(c *gin.Context) {
|
|||||||
iResult.Name = fmt.Sprintf("%s - %s", *fileMeta.Author, *fileMeta.Title)
|
iResult.Name = fmt.Sprintf("%s - %s", *fileMeta.Author, *fileMeta.Title)
|
||||||
|
|
||||||
// Check already exists
|
// Check already exists
|
||||||
_, err = qtx.GetDocument(c, *fileMeta.PartialMD5)
|
_, err = qtx.GetDocument(api.db.Ctx, *fileMeta.PartialMD5)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
log.Warnf("document already exists: %s", *fileMeta.PartialMD5)
|
log.Warnf("document already exists: %s", *fileMeta.PartialMD5)
|
||||||
iResult.Status = importExists
|
iResult.Status = importExists
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import Copy
|
// TODO - Import Copy
|
||||||
if rAdminImport.Type == importCopy {
|
// newName := deriveBaseFileName(fileMeta)
|
||||||
// Derive & Sanitize File Name
|
|
||||||
relFilePath = deriveBaseFileName(fileMeta)
|
|
||||||
safePath := filepath.Join(api.cfg.DataPath, "documents", relFilePath)
|
|
||||||
|
|
||||||
// Open Source File
|
// Open File on Disk
|
||||||
srcFile, err := os.Open(importPath)
|
// file, err := os.Open(currentPath)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
log.Errorf("unable to open current file: %v", err)
|
// return err
|
||||||
iResult.Error = err
|
// }
|
||||||
return nil
|
// defer file.Close()
|
||||||
}
|
|
||||||
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(c, database.UpsertDocumentParams{
|
if _, err = qtx.UpsertDocument(api.db.Ctx, database.UpsertDocumentParams{
|
||||||
ID: *fileMeta.PartialMD5,
|
ID: *fileMeta.PartialMD5,
|
||||||
Title: fileMeta.Title,
|
Title: fileMeta.Title,
|
||||||
Author: fileMeta.Author,
|
Author: fileMeta.Author,
|
||||||
@@ -501,7 +459,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: &basePath,
|
Basepath: &baseDirectory,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Errorf("UpsertDocument DB Error: %v", err)
|
log.Errorf("UpsertDocument DB Error: %v", err)
|
||||||
iResult.Error = err
|
iResult.Error = err
|
||||||
@@ -626,9 +584,17 @@ func (api *API) processRestoreFile(rAdminAction requestAdminAction, c *gin.Conte
|
|||||||
}
|
}
|
||||||
defer backupFile.Close()
|
defer backupFile.Close()
|
||||||
|
|
||||||
|
// Vacuum DB
|
||||||
|
_, err = api.db.DB.ExecContext(api.db.Ctx, "VACUUM;")
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Unable to vacuum DB: ", err)
|
||||||
|
appErrorPage(c, http.StatusInternalServerError, "Unable to vacuum database")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Save Backup File
|
// Save Backup File
|
||||||
w := bufio.NewWriter(backupFile)
|
w := bufio.NewWriter(backupFile)
|
||||||
err = api.createBackup(c, w, []string{"covers", "documents"})
|
err = api.createBackup(w, []string{"covers", "documents"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to save backup file: ", err)
|
log.Error("Unable to save backup file: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to save backup file")
|
appErrorPage(c, http.StatusInternalServerError, "Unable to save backup file")
|
||||||
@@ -651,13 +617,13 @@ func (api *API) processRestoreFile(rAdminAction requestAdminAction, c *gin.Conte
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reinit DB
|
// Reinit DB
|
||||||
if err := api.db.Reload(c); err != nil {
|
if err := api.db.Reload(); err != nil {
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to reload DB")
|
appErrorPage(c, http.StatusInternalServerError, "Unable to reload DB")
|
||||||
log.Panicf("Unable to reload DB: %v", err)
|
log.Panicf("Unable to reload DB: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rotate Auth Hashes
|
// Rotate Auth Hashes
|
||||||
if err := api.rotateAllAuthHashes(c); err != nil {
|
if err := api.rotateAllAuthHashes(); err != nil {
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to rotate hashes")
|
appErrorPage(c, http.StatusInternalServerError, "Unable to rotate hashes")
|
||||||
log.Panicf("Unable to rotate auth hashes: %v", err)
|
log.Panicf("Unable to rotate auth hashes: %v", err)
|
||||||
}
|
}
|
||||||
@@ -681,14 +647,14 @@ func (api *API) restoreData(zipReader *zip.Reader) error {
|
|||||||
destPath := filepath.Join(api.cfg.DataPath, file.Name)
|
destPath := filepath.Join(api.cfg.DataPath, file.Name)
|
||||||
destFile, err := os.Create(destPath)
|
destFile, err := os.Create(destPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("error creating destination file: %v", err)
|
fmt.Println("Error creating destination file:", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer destFile.Close()
|
defer destFile.Close()
|
||||||
|
|
||||||
// Copy the contents from the zip file to the destination file.
|
// Copy the contents from the zip file to the destination file.
|
||||||
if _, err := io.Copy(destFile, rc); err != nil {
|
if _, err := io.Copy(destFile, rc); err != nil {
|
||||||
log.Errorf("Error copying file contents: %v", err)
|
fmt.Println("Error copying file contents:", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -718,14 +684,9 @@ func (api *API) removeData() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) createBackup(ctx context.Context, w io.Writer, directories []string) error {
|
func (api *API) createBackup(w io.Writer, directories []string) error {
|
||||||
// Vacuum DB
|
|
||||||
_, err := api.db.DB.ExecContext(ctx, "VACUUM;")
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "Unable to vacuum database")
|
|
||||||
}
|
|
||||||
|
|
||||||
ar := zip.NewWriter(w)
|
ar := zip.NewWriter(w)
|
||||||
|
|
||||||
exportWalker := func(currentPath string, f fs.DirEntry, err error) error {
|
exportWalker := func(currentPath string, f fs.DirEntry, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -793,43 +754,29 @@ func (api *API) createBackup(ctx context.Context, w io.Writer, directories []str
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) isLastAdmin(ctx context.Context, userID string) (bool, error) {
|
func (api *API) createUser(createRequest requestAdminUpdateUser) error {
|
||||||
allUsers, err := api.db.Queries.GetUsers(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return false, errors.Wrap(err, fmt.Sprintf("GetUsers DB Error: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
hasAdmin := false
|
|
||||||
for _, user := range allUsers {
|
|
||||||
if user.Admin && user.ID != userID {
|
|
||||||
hasAdmin = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return !hasAdmin, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) createUser(ctx context.Context, user string, rawPassword *string, isAdmin *bool) error {
|
|
||||||
// Validate Necessary Parameters
|
// Validate Necessary Parameters
|
||||||
if rawPassword == nil || *rawPassword == "" {
|
if createRequest.User == "" {
|
||||||
|
return fmt.Errorf("username can't be empty")
|
||||||
|
}
|
||||||
|
if createRequest.Password == nil || *createRequest.Password == "" {
|
||||||
return fmt.Errorf("password can't be empty")
|
return fmt.Errorf("password can't be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Base Params
|
// Base Params
|
||||||
createParams := database.CreateUserParams{
|
createParams := database.CreateUserParams{
|
||||||
ID: user,
|
ID: createRequest.User,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Admin (Explicit or False)
|
// Handle Admin (Explicit or False)
|
||||||
if isAdmin != nil {
|
if createRequest.isAdmin != nil {
|
||||||
createParams.Admin = *isAdmin
|
createParams.Admin = *createRequest.isAdmin
|
||||||
} else {
|
} else {
|
||||||
createParams.Admin = false
|
createParams.Admin = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse Password
|
// Parse Password
|
||||||
password := fmt.Sprintf("%x", md5.Sum([]byte(*rawPassword)))
|
password := fmt.Sprintf("%x", md5.Sum([]byte(*createRequest.Password)))
|
||||||
hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams)
|
hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to create hashed password")
|
return fmt.Errorf("unable to create hashed password")
|
||||||
@@ -845,7 +792,7 @@ func (api *API) createUser(ctx context.Context, user string, rawPassword *string
|
|||||||
createParams.AuthHash = &authHash
|
createParams.AuthHash = &authHash
|
||||||
|
|
||||||
// Create user in DB
|
// Create user in DB
|
||||||
if rows, err := api.db.Queries.CreateUser(ctx, createParams); err != nil {
|
if rows, err := api.db.Queries.CreateUser(api.db.Ctx, createParams); err != nil {
|
||||||
log.Error("CreateUser DB Error:", err)
|
log.Error("CreateUser DB Error:", err)
|
||||||
return fmt.Errorf("unable to create user")
|
return fmt.Errorf("unable to create user")
|
||||||
} else if rows == 0 {
|
} else if rows == 0 {
|
||||||
@@ -856,43 +803,42 @@ func (api *API) createUser(ctx context.Context, user string, rawPassword *string
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) updateUser(ctx context.Context, user string, rawPassword *string, isAdmin *bool) error {
|
func (api *API) updateUser(updateRequest requestAdminUpdateUser) error {
|
||||||
// Validate Necessary Parameters
|
// Validate Necessary Parameters
|
||||||
if rawPassword == nil && isAdmin == nil {
|
if updateRequest.User == "" {
|
||||||
|
return fmt.Errorf("username can't be empty")
|
||||||
|
}
|
||||||
|
if updateRequest.Password == nil && updateRequest.isAdmin == nil {
|
||||||
return fmt.Errorf("nothing to update")
|
return fmt.Errorf("nothing to update")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Base Params
|
// Base Params
|
||||||
updateParams := database.UpdateUserParams{
|
updateParams := database.UpdateUserParams{
|
||||||
UserID: user,
|
UserID: updateRequest.User,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Admin (Update or Existing)
|
// Handle Admin (Update or Existing)
|
||||||
if isAdmin != nil {
|
if updateRequest.isAdmin != nil {
|
||||||
updateParams.Admin = *isAdmin
|
updateParams.Admin = *updateRequest.isAdmin
|
||||||
} else {
|
} else {
|
||||||
user, err := api.db.Queries.GetUser(ctx, user)
|
user, err := api.db.Queries.GetUser(api.db.Ctx, updateRequest.User)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, fmt.Sprintf("GetUser DB Error: %v", err))
|
return errors.Wrap(err, fmt.Sprintf("GetUser DB Error: %v", err))
|
||||||
}
|
}
|
||||||
updateParams.Admin = user.Admin
|
updateParams.Admin = user.Admin
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check Admins - Disallow Demotion
|
// TODO:
|
||||||
if isLast, err := api.isLastAdmin(ctx, user); err != nil {
|
// - Validate Not Last Admin
|
||||||
return err
|
|
||||||
} else if isLast && !updateParams.Admin {
|
|
||||||
return fmt.Errorf("unable to demote %s - last admin", user)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle Password
|
// Handle Password
|
||||||
if rawPassword != nil {
|
if updateRequest.Password != nil {
|
||||||
if *rawPassword == "" {
|
if *updateRequest.Password == "" {
|
||||||
return fmt.Errorf("password can't be empty")
|
return fmt.Errorf("password can't be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse Password
|
// Parse Password
|
||||||
password := fmt.Sprintf("%x", md5.Sum([]byte(*rawPassword)))
|
password := fmt.Sprintf("%x", md5.Sum([]byte(*updateRequest.Password)))
|
||||||
hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams)
|
hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to create hashed password")
|
return fmt.Errorf("unable to create hashed password")
|
||||||
@@ -909,7 +855,7 @@ func (api *API) updateUser(ctx context.Context, user string, rawPassword *string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update User
|
// Update User
|
||||||
_, err := api.db.Queries.UpdateUser(ctx, updateParams)
|
_, err := api.db.Queries.UpdateUser(api.db.Ctx, updateParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, fmt.Sprintf("UpdateUser DB Error: %v", err))
|
return errors.Wrap(err, fmt.Sprintf("UpdateUser DB Error: %v", err))
|
||||||
}
|
}
|
||||||
@@ -917,34 +863,8 @@ func (api *API) updateUser(ctx context.Context, user string, rawPassword *string
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) deleteUser(ctx context.Context, user string) error {
|
func (api *API) deleteUser(updateRequest requestAdminUpdateUser) error {
|
||||||
// Check Admins
|
// TODO:
|
||||||
if isLast, err := api.isLastAdmin(ctx, user); err != nil {
|
// - Validate Not Last Admin
|
||||||
return err
|
return errors.New("unimplemented")
|
||||||
} else if isLast {
|
|
||||||
return fmt.Errorf("unable to delete %s - last admin", user)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create Backup File
|
|
||||||
backupFilePath := filepath.Join(api.cfg.ConfigPath, fmt.Sprintf("backups/AnthoLumeBackup_%s.zip", time.Now().Format("20060102150405")))
|
|
||||||
backupFile, err := os.Create(backupFilePath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer backupFile.Close()
|
|
||||||
|
|
||||||
// Save Backup File (DB Only)
|
|
||||||
w := bufio.NewWriter(backupFile)
|
|
||||||
err = api.createBackup(ctx, w, []string{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete User
|
|
||||||
_, err = api.db.Queries.DeleteUser(ctx, user)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, fmt.Sprintf("DeleteUser DB Error: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -23,8 +22,8 @@ import (
|
|||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
"reichard.io/antholume/database"
|
"reichard.io/antholume/database"
|
||||||
"reichard.io/antholume/metadata"
|
"reichard.io/antholume/metadata"
|
||||||
"reichard.io/antholume/pkg/ptr"
|
|
||||||
"reichard.io/antholume/search"
|
"reichard.io/antholume/search"
|
||||||
|
"reichard.io/antholume/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type backupType string
|
type backupType string
|
||||||
@@ -111,12 +110,11 @@ func (api *API) appGetDocuments(c *gin.Context) {
|
|||||||
query = &search
|
query = &search
|
||||||
}
|
}
|
||||||
|
|
||||||
documents, err := api.db.Queries.GetDocumentsWithStats(c, database.GetDocumentsWithStatsParams{
|
documents, err := api.db.Queries.GetDocumentsWithStats(api.db.Ctx, database.GetDocumentsWithStatsParams{
|
||||||
UserID: auth.UserName,
|
UserID: auth.UserName,
|
||||||
Query: query,
|
Query: query,
|
||||||
Deleted: ptr.Of(false),
|
Offset: (*qParams.Page - 1) * *qParams.Limit,
|
||||||
Offset: (*qParams.Page - 1) * *qParams.Limit,
|
Limit: *qParams.Limit,
|
||||||
Limit: *qParams.Limit,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetDocumentsWithStats DB Error: ", err)
|
log.Error("GetDocumentsWithStats DB Error: ", err)
|
||||||
@@ -124,14 +122,14 @@ func (api *API) appGetDocuments(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
length, err := api.db.Queries.GetDocumentsSize(c, query)
|
length, err := api.db.Queries.GetDocumentsSize(api.db.Ctx, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetDocumentsSize DB Error: ", err)
|
log.Error("GetDocumentsSize DB Error: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocumentsSize DB Error: %v", err))
|
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocumentsSize DB Error: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = api.getDocumentsWordCount(c, documents); err != nil {
|
if err = api.getDocumentsWordCount(documents); err != nil {
|
||||||
log.Error("Unable to Get Word Counts: ", err)
|
log.Error("Unable to Get Word Counts: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,10 +161,13 @@ func (api *API) appGetDocument(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
document, err := api.db.GetDocument(c, rDocID.DocumentID, auth.UserName)
|
document, err := api.db.Queries.GetDocumentWithStats(api.db.Ctx, database.GetDocumentWithStatsParams{
|
||||||
|
UserID: auth.UserName,
|
||||||
|
DocumentID: rDocID.DocumentID,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetDocument DB Error: ", err)
|
log.Error("GetDocumentWithStats DB Error: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocument DB Error: %v", err))
|
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocumentsWithStats DB Error: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +193,7 @@ func (api *API) appGetProgress(c *gin.Context) {
|
|||||||
progressFilter.DocumentID = *qParams.Document
|
progressFilter.DocumentID = *qParams.Document
|
||||||
}
|
}
|
||||||
|
|
||||||
progress, err := api.db.Queries.GetProgress(c, progressFilter)
|
progress, err := api.db.Queries.GetProgress(api.db.Ctx, progressFilter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetProgress DB Error: ", err)
|
log.Error("GetProgress DB Error: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetActivity DB Error: %v", err))
|
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetActivity DB Error: %v", err))
|
||||||
@@ -219,7 +220,7 @@ func (api *API) appGetActivity(c *gin.Context) {
|
|||||||
activityFilter.DocumentID = *qParams.Document
|
activityFilter.DocumentID = *qParams.Document
|
||||||
}
|
}
|
||||||
|
|
||||||
activity, err := api.db.Queries.GetActivity(c, activityFilter)
|
activity, err := api.db.Queries.GetActivity(api.db.Ctx, activityFilter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetActivity DB Error: ", err)
|
log.Error("GetActivity DB Error: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetActivity DB Error: %v", err))
|
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetActivity DB Error: %v", err))
|
||||||
@@ -235,7 +236,7 @@ func (api *API) appGetHome(c *gin.Context) {
|
|||||||
templateVars, auth := api.getBaseTemplateVars("home", c)
|
templateVars, auth := api.getBaseTemplateVars("home", c)
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
graphData, err := api.db.Queries.GetDailyReadStats(c, auth.UserName)
|
graphData, err := api.db.Queries.GetDailyReadStats(api.db.Ctx, auth.UserName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetDailyReadStats DB Error: ", err)
|
log.Error("GetDailyReadStats DB Error: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDailyReadStats DB Error: %v", err))
|
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDailyReadStats DB Error: %v", err))
|
||||||
@@ -244,7 +245,7 @@ func (api *API) appGetHome(c *gin.Context) {
|
|||||||
log.Debug("GetDailyReadStats DB Performance: ", time.Since(start))
|
log.Debug("GetDailyReadStats DB Performance: ", time.Since(start))
|
||||||
|
|
||||||
start = time.Now()
|
start = time.Now()
|
||||||
databaseInfo, err := api.db.Queries.GetDatabaseInfo(c, auth.UserName)
|
databaseInfo, err := api.db.Queries.GetDatabaseInfo(api.db.Ctx, auth.UserName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetDatabaseInfo DB Error: ", err)
|
log.Error("GetDatabaseInfo DB Error: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDatabaseInfo DB Error: %v", err))
|
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDatabaseInfo DB Error: %v", err))
|
||||||
@@ -253,7 +254,7 @@ func (api *API) appGetHome(c *gin.Context) {
|
|||||||
log.Debug("GetDatabaseInfo DB Performance: ", time.Since(start))
|
log.Debug("GetDatabaseInfo DB Performance: ", time.Since(start))
|
||||||
|
|
||||||
start = time.Now()
|
start = time.Now()
|
||||||
streaks, err := api.db.Queries.GetUserStreaks(c, auth.UserName)
|
streaks, err := api.db.Queries.GetUserStreaks(api.db.Ctx, auth.UserName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetUserStreaks DB Error: ", err)
|
log.Error("GetUserStreaks DB Error: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUserStreaks DB Error: %v", err))
|
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUserStreaks DB Error: %v", err))
|
||||||
@@ -262,7 +263,7 @@ func (api *API) appGetHome(c *gin.Context) {
|
|||||||
log.Debug("GetUserStreaks DB Performance: ", time.Since(start))
|
log.Debug("GetUserStreaks DB Performance: ", time.Since(start))
|
||||||
|
|
||||||
start = time.Now()
|
start = time.Now()
|
||||||
userStatistics, err := api.db.Queries.GetUserStatistics(c)
|
userStatistics, err := api.db.Queries.GetUserStatistics(api.db.Ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetUserStatistics DB Error: ", err)
|
log.Error("GetUserStatistics DB Error: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUserStatistics DB Error: %v", err))
|
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUserStatistics DB Error: %v", err))
|
||||||
@@ -283,14 +284,14 @@ func (api *API) appGetHome(c *gin.Context) {
|
|||||||
func (api *API) appGetSettings(c *gin.Context) {
|
func (api *API) appGetSettings(c *gin.Context) {
|
||||||
templateVars, auth := api.getBaseTemplateVars("settings", c)
|
templateVars, auth := api.getBaseTemplateVars("settings", c)
|
||||||
|
|
||||||
user, err := api.db.Queries.GetUser(c, auth.UserName)
|
user, err := api.db.Queries.GetUser(api.db.Ctx, auth.UserName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetUser DB Error: ", err)
|
log.Error("GetUser DB Error: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUser DB Error: %v", err))
|
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUser DB Error: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
devices, err := api.db.Queries.GetDevices(c, auth.UserName)
|
devices, err := api.db.Queries.GetDevices(api.db.Ctx, auth.UserName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetDevices DB Error: ", err)
|
log.Error("GetDevices DB Error: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDevices DB Error: %v", err))
|
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDevices DB Error: %v", err))
|
||||||
@@ -368,20 +369,24 @@ func (api *API) appGetDocumentProgress(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
progress, err := api.db.Queries.GetDocumentProgress(c, database.GetDocumentProgressParams{
|
progress, err := api.db.Queries.GetDocumentProgress(api.db.Ctx, database.GetDocumentProgressParams{
|
||||||
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("GetDocumentProgress DB Error: ", err)
|
log.Error("UpsertDocument DB Error: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocumentProgress DB Error: %v", err))
|
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("UpsertDocument DB Error: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
document, err := api.db.GetDocument(c, rDoc.DocumentID, auth.UserName)
|
document, err := api.db.Queries.GetDocumentWithStats(api.db.Ctx, database.GetDocumentWithStatsParams{
|
||||||
|
UserID: auth.UserName,
|
||||||
|
DocumentID: rDoc.DocumentID,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetDocument DB Error: ", err)
|
log.Error("GetDocumentWithStats DB Error: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocument DB Error: %v", err))
|
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocumentWithStats DB Error: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,7 +406,7 @@ func (api *API) appGetDevices(c *gin.Context) {
|
|||||||
auth = data.(authData)
|
auth = data.(authData)
|
||||||
}
|
}
|
||||||
|
|
||||||
devices, err := api.db.Queries.GetDevices(c, auth.UserName)
|
devices, err := api.db.Queries.GetDevices(api.db.Ctx, auth.UserName)
|
||||||
|
|
||||||
if err != nil && err != sql.ErrNoRows {
|
if err != nil && err != sql.ErrNoRows {
|
||||||
log.Error("GetDevices DB Error: ", err)
|
log.Error("GetDevices DB Error: ", err)
|
||||||
@@ -452,7 +457,7 @@ func (api *API) appUploadNewDocument(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check Already Exists
|
// Check Already Exists
|
||||||
_, err = api.db.Queries.GetDocument(c, *metadataInfo.PartialMD5)
|
_, err = api.db.Queries.GetDocument(api.db.Ctx, *metadataInfo.PartialMD5)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
log.Warnf("document already exists: %s", *metadataInfo.PartialMD5)
|
log.Warnf("document already exists: %s", *metadataInfo.PartialMD5)
|
||||||
c.Redirect(http.StatusFound, fmt.Sprintf("./documents/%s", *metadataInfo.PartialMD5))
|
c.Redirect(http.StatusFound, fmt.Sprintf("./documents/%s", *metadataInfo.PartialMD5))
|
||||||
@@ -460,8 +465,7 @@ func (api *API) appUploadNewDocument(c *gin.Context) {
|
|||||||
|
|
||||||
// Derive & Sanitize File Name
|
// Derive & Sanitize File Name
|
||||||
fileName := deriveBaseFileName(metadataInfo)
|
fileName := deriveBaseFileName(metadataInfo)
|
||||||
basePath := filepath.Join(api.cfg.DataPath, "documents")
|
safePath := filepath.Join(api.cfg.DataPath, "documents", fileName)
|
||||||
safePath := filepath.Join(basePath, fileName)
|
|
||||||
|
|
||||||
// Open Destination File
|
// Open Destination File
|
||||||
destFile, err := os.Create(safePath)
|
destFile, err := os.Create(safePath)
|
||||||
@@ -480,7 +484,7 @@ func (api *API) appUploadNewDocument(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Upsert Document
|
// Upsert Document
|
||||||
if _, err = api.db.Queries.UpsertDocument(c, database.UpsertDocumentParams{
|
if _, err = api.db.Queries.UpsertDocument(api.db.Ctx, database.UpsertDocumentParams{
|
||||||
ID: *metadataInfo.PartialMD5,
|
ID: *metadataInfo.PartialMD5,
|
||||||
Title: metadataInfo.Title,
|
Title: metadataInfo.Title,
|
||||||
Author: metadataInfo.Author,
|
Author: metadataInfo.Author,
|
||||||
@@ -488,7 +492,9 @@ 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))
|
||||||
@@ -570,7 +576,7 @@ func (api *API) appEditDocument(c *gin.Context) {
|
|||||||
|
|
||||||
coverFileName = &fileName
|
coverFileName = &fileName
|
||||||
} else if rDocEdit.CoverGBID != nil {
|
} else if rDocEdit.CoverGBID != nil {
|
||||||
coverDir := filepath.Join(api.cfg.DataPath, "covers")
|
var coverDir string = filepath.Join(api.cfg.DataPath, "covers")
|
||||||
fileName, err := metadata.CacheCover(*rDocEdit.CoverGBID, coverDir, rDocID.DocumentID, true)
|
fileName, err := metadata.CacheCover(*rDocEdit.CoverGBID, coverDir, rDocID.DocumentID, true)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
coverFileName = fileName
|
coverFileName = fileName
|
||||||
@@ -578,7 +584,7 @@ func (api *API) appEditDocument(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update Document
|
// Update Document
|
||||||
if _, err := api.db.Queries.UpsertDocument(c, database.UpsertDocumentParams{
|
if _, err := api.db.Queries.UpsertDocument(api.db.Ctx, database.UpsertDocumentParams{
|
||||||
ID: rDocID.DocumentID,
|
ID: rDocID.DocumentID,
|
||||||
Title: api.sanitizeInput(rDocEdit.Title),
|
Title: api.sanitizeInput(rDocEdit.Title),
|
||||||
Author: api.sanitizeInput(rDocEdit.Author),
|
Author: api.sanitizeInput(rDocEdit.Author),
|
||||||
@@ -602,7 +608,7 @@ func (api *API) appDeleteDocument(c *gin.Context) {
|
|||||||
appErrorPage(c, http.StatusNotFound, "Invalid document")
|
appErrorPage(c, http.StatusNotFound, "Invalid document")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
changed, err := api.db.Queries.DeleteDocument(c, rDocID.DocumentID)
|
changed, err := api.db.Queries.DeleteDocument(api.db.Ctx, rDocID.DocumentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("DeleteDocument DB Error")
|
log.Error("DeleteDocument DB Error")
|
||||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("DeleteDocument DB Error: %v", err))
|
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("DeleteDocument DB Error: %v", err))
|
||||||
@@ -664,7 +670,7 @@ func (api *API) appIdentifyDocument(c *gin.Context) {
|
|||||||
firstResult := metadataResults[0]
|
firstResult := metadataResults[0]
|
||||||
|
|
||||||
// Store First Metadata Result
|
// Store First Metadata Result
|
||||||
if _, err = api.db.Queries.AddMetadata(c, database.AddMetadataParams{
|
if _, err = api.db.Queries.AddMetadata(api.db.Ctx, database.AddMetadataParams{
|
||||||
DocumentID: rDocID.DocumentID,
|
DocumentID: rDocID.DocumentID,
|
||||||
Title: firstResult.Title,
|
Title: firstResult.Title,
|
||||||
Author: firstResult.Author,
|
Author: firstResult.Author,
|
||||||
@@ -683,10 +689,13 @@ func (api *API) appIdentifyDocument(c *gin.Context) {
|
|||||||
templateVars["MetadataError"] = "No Metadata Found"
|
templateVars["MetadataError"] = "No Metadata Found"
|
||||||
}
|
}
|
||||||
|
|
||||||
document, err := api.db.GetDocument(c, rDocID.DocumentID, auth.UserName)
|
document, err := api.db.Queries.GetDocumentWithStats(api.db.Ctx, database.GetDocumentWithStatsParams{
|
||||||
|
UserID: auth.UserName,
|
||||||
|
DocumentID: rDocID.DocumentID,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetDocument DB Error: ", err)
|
log.Error("GetDocumentWithStats DB Error: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocument DB Error: %v", err))
|
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocumentWithStats DB Error: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -733,50 +742,57 @@ func (api *API) appSaveNewDocument(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send Message
|
// Send Message
|
||||||
sendDownloadMessage("Downloading document...", gin.H{"Progress": 1})
|
sendDownloadMessage("Downloading document...", gin.H{"Progress": 10})
|
||||||
|
|
||||||
// Scaled Download Function
|
|
||||||
lastTime := time.Now()
|
|
||||||
downloadFunc := func(p float32) {
|
|
||||||
nowTime := time.Now()
|
|
||||||
if nowTime.Before(lastTime.Add(time.Millisecond * 500)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
scaledProgress := int((p * 95 / 100) + 2)
|
|
||||||
sendDownloadMessage("Downloading document...", gin.H{"Progress": scaledProgress})
|
|
||||||
lastTime = nowTime
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save Book
|
// Save Book
|
||||||
tempFilePath, metadata, err := search.SaveBook(rDocAdd.ID, rDocAdd.Source, downloadFunc)
|
tempFilePath, err := search.SaveBook(rDocAdd.ID, rDocAdd.Source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Save Book Error: ", err)
|
log.Warn("Temp File Error: ", err)
|
||||||
sendDownloadMessage("Unable to download file", gin.H{"Error": true})
|
sendDownloadMessage("Unable to download file", gin.H{"Error": true})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send Message
|
// Send Message
|
||||||
sendDownloadMessage("Saving document...", gin.H{"Progress": 98})
|
sendDownloadMessage("Calculating partial MD5...", gin.H{"Progress": 60})
|
||||||
|
|
||||||
// Derive Author / Title
|
// Calculate Partial MD5 ID
|
||||||
docAuthor := "Unknown"
|
partialMD5, err := utils.CalculatePartialMD5(tempFilePath)
|
||||||
if *metadata.Author != "" {
|
if err != nil {
|
||||||
docAuthor = *metadata.Author
|
log.Warn("Partial MD5 Error: ", err)
|
||||||
} else if *rDocAdd.Author != "" {
|
sendDownloadMessage("Unable to calculate partial MD5", gin.H{"Error": true})
|
||||||
docAuthor = *rDocAdd.Author
|
|
||||||
}
|
}
|
||||||
|
|
||||||
docTitle := "Unknown"
|
// Send Message
|
||||||
if *metadata.Title != "" {
|
sendDownloadMessage("Saving file...", gin.H{"Progress": 60})
|
||||||
docTitle = *metadata.Title
|
|
||||||
} else if *rDocAdd.Title != "" {
|
// Derive Extension on MIME
|
||||||
docTitle = *rDocAdd.Title
|
fileMime, err := mimetype.DetectFile(tempFilePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("MIME Detect Error: ", err)
|
||||||
|
sendDownloadMessage("Unable to download file", gin.H{"Error": true})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fileExtension := fileMime.Extension()
|
||||||
|
|
||||||
|
// Derive Filename
|
||||||
|
var fileName string
|
||||||
|
if *rDocAdd.Author != "" {
|
||||||
|
fileName = fileName + *rDocAdd.Author
|
||||||
|
} else {
|
||||||
|
fileName = fileName + "Unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove Slashes & Sanitize File Name
|
if *rDocAdd.Title != "" {
|
||||||
fileName := fmt.Sprintf("%s - %s", docAuthor, docTitle)
|
fileName = fileName + " - " + *rDocAdd.Title
|
||||||
|
} else {
|
||||||
|
fileName = fileName + " - Unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove Slashes
|
||||||
fileName = strings.ReplaceAll(fileName, "/", "")
|
fileName = strings.ReplaceAll(fileName, "/", "")
|
||||||
fileName = "." + filepath.Clean(fmt.Sprintf("/%s [%s]%s", fileName, *metadata.PartialMD5, metadata.Type))
|
|
||||||
|
// Derive & Sanitize File Name
|
||||||
|
fileName = "." + filepath.Clean(fmt.Sprintf("/%s [%s]%s", fileName, *partialMD5, fileExtension))
|
||||||
|
|
||||||
// Open Source File
|
// Open Source File
|
||||||
sourceFile, err := os.Open(tempFilePath)
|
sourceFile, err := os.Open(tempFilePath)
|
||||||
@@ -789,9 +805,7 @@ func (api *API) appSaveNewDocument(c *gin.Context) {
|
|||||||
defer sourceFile.Close()
|
defer sourceFile.Close()
|
||||||
|
|
||||||
// Generate Storage Path & Open File
|
// Generate Storage Path & Open File
|
||||||
basePath := filepath.Join(api.cfg.DataPath, "documents")
|
safePath := filepath.Join(api.cfg.DataPath, "documents", fileName)
|
||||||
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)
|
||||||
@@ -808,17 +822,38 @@ func (api *API) appSaveNewDocument(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send Message
|
// Send Message
|
||||||
sendDownloadMessage("Saving to database...", gin.H{"Progress": 99})
|
sendDownloadMessage("Calculating MD5...", gin.H{"Progress": 70})
|
||||||
|
|
||||||
|
// Get MD5 Hash
|
||||||
|
fileHash, err := getFileMD5(safePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Hash Failure: ", err)
|
||||||
|
sendDownloadMessage("Unable to calculate MD5", gin.H{"Error": true})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send Message
|
||||||
|
sendDownloadMessage("Calculating word count...", gin.H{"Progress": 80})
|
||||||
|
|
||||||
|
// Get Word Count
|
||||||
|
wordCount, err := metadata.GetWordCount(safePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Word Count Failure: ", err)
|
||||||
|
sendDownloadMessage("Unable to calculate word count", gin.H{"Error": true})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send Message
|
||||||
|
sendDownloadMessage("Saving to database...", gin.H{"Progress": 90})
|
||||||
|
|
||||||
// Upsert Document
|
// Upsert Document
|
||||||
if _, err = api.db.Queries.UpsertDocument(c, database.UpsertDocumentParams{
|
if _, err = api.db.Queries.UpsertDocument(api.db.Ctx, database.UpsertDocumentParams{
|
||||||
ID: *metadata.PartialMD5,
|
ID: *partialMD5,
|
||||||
Title: &docTitle,
|
Title: rDocAdd.Title,
|
||||||
Author: &docAuthor,
|
Author: rDocAdd.Author,
|
||||||
Md5: metadata.MD5,
|
Md5: fileHash,
|
||||||
Words: metadata.WordCount,
|
|
||||||
Filepath: &fileName,
|
Filepath: &fileName,
|
||||||
Basepath: &basePath,
|
Words: wordCount,
|
||||||
}); 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})
|
||||||
@@ -829,7 +864,7 @@ func (api *API) appSaveNewDocument(c *gin.Context) {
|
|||||||
sendDownloadMessage("Download Success", gin.H{
|
sendDownloadMessage("Download Success", gin.H{
|
||||||
"Progress": 100,
|
"Progress": 100,
|
||||||
"ButtonText": "Go to Book",
|
"ButtonText": "Go to Book",
|
||||||
"ButtonHref": fmt.Sprintf("./documents/%s", *metadata.PartialMD5),
|
"ButtonHref": fmt.Sprintf("./documents/%s", *partialMD5),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -858,7 +893,7 @@ func (api *API) appEditSettings(c *gin.Context) {
|
|||||||
// Set New Password
|
// Set New Password
|
||||||
if rUserSettings.Password != nil && rUserSettings.NewPassword != nil {
|
if rUserSettings.Password != nil && rUserSettings.NewPassword != nil {
|
||||||
password := fmt.Sprintf("%x", md5.Sum([]byte(*rUserSettings.Password)))
|
password := fmt.Sprintf("%x", md5.Sum([]byte(*rUserSettings.Password)))
|
||||||
data := api.authorizeCredentials(c, auth.UserName, password)
|
data := api.authorizeCredentials(auth.UserName, password)
|
||||||
if data == nil {
|
if data == nil {
|
||||||
templateVars["PasswordErrorMessage"] = "Invalid Password"
|
templateVars["PasswordErrorMessage"] = "Invalid Password"
|
||||||
} else {
|
} else {
|
||||||
@@ -880,7 +915,7 @@ func (api *API) appEditSettings(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update User
|
// Update User
|
||||||
_, err := api.db.Queries.UpdateUser(c, newUserSettings)
|
_, err := api.db.Queries.UpdateUser(api.db.Ctx, newUserSettings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("UpdateUser DB Error: ", err)
|
log.Error("UpdateUser DB Error: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("UpdateUser DB Error: %v", err))
|
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("UpdateUser DB Error: %v", err))
|
||||||
@@ -888,7 +923,7 @@ func (api *API) appEditSettings(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get User
|
// Get User
|
||||||
user, err := api.db.Queries.GetUser(c, auth.UserName)
|
user, err := api.db.Queries.GetUser(api.db.Ctx, auth.UserName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetUser DB Error: ", err)
|
log.Error("GetUser DB Error: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUser DB Error: %v", err))
|
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUser DB Error: %v", err))
|
||||||
@@ -896,7 +931,7 @@ func (api *API) appEditSettings(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get Devices
|
// Get Devices
|
||||||
devices, err := api.db.Queries.GetDevices(c, auth.UserName)
|
devices, err := api.db.Queries.GetDevices(api.db.Ctx, auth.UserName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetDevices DB Error: ", err)
|
log.Error("GetDevices DB Error: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDevices DB Error: %v", err))
|
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDevices DB Error: %v", err))
|
||||||
@@ -915,7 +950,7 @@ func (api *API) appDemoModeError(c *gin.Context) {
|
|||||||
appErrorPage(c, http.StatusUnauthorized, "Not Allowed in Demo Mode")
|
appErrorPage(c, http.StatusUnauthorized, "Not Allowed in Demo Mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) getDocumentsWordCount(ctx context.Context, documents []database.GetDocumentsWithStatsRow) error {
|
func (api *API) getDocumentsWordCount(documents []database.GetDocumentsWithStatsRow) error {
|
||||||
// Do Transaction
|
// Do Transaction
|
||||||
tx, err := api.db.DB.Begin()
|
tx, err := api.db.DB.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -938,7 +973,7 @@ func (api *API) getDocumentsWordCount(ctx context.Context, documents []database.
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Word Count Error: ", err)
|
log.Warn("Word Count Error: ", err)
|
||||||
} else {
|
} else {
|
||||||
if _, err := qtx.UpsertDocument(ctx, database.UpsertDocumentParams{
|
if _, err := qtx.UpsertDocument(api.db.Ctx, database.UpsertDocumentParams{
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
Words: wordCount,
|
Words: wordCount,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
@@ -999,7 +1034,7 @@ func bindQueryParams(c *gin.Context, defaultLimit int64) queryParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func appErrorPage(c *gin.Context, errorCode int, errorMessage string) {
|
func appErrorPage(c *gin.Context, errorCode int, errorMessage string) {
|
||||||
errorHuman := "We're not even sure what happened."
|
var errorHuman string = "We're not even sure what happened."
|
||||||
|
|
||||||
switch errorCode {
|
switch errorCode {
|
||||||
case http.StatusInternalServerError:
|
case http.StatusInternalServerError:
|
||||||
@@ -1021,11 +1056,11 @@ func appErrorPage(c *gin.Context, errorCode int, errorMessage string) {
|
|||||||
|
|
||||||
func arrangeUserStatistics(userStatistics []database.GetUserStatisticsRow) gin.H {
|
func arrangeUserStatistics(userStatistics []database.GetUserStatisticsRow) gin.H {
|
||||||
// Item Sorter
|
// Item Sorter
|
||||||
sortItem := func(userStatistics []database.GetUserStatisticsRow, key string, less func(i int, j int) bool) []map[string]any {
|
sortItem := func(userStatistics []database.GetUserStatisticsRow, key string, less func(i int, j int) bool) []map[string]interface{} {
|
||||||
sortedData := append([]database.GetUserStatisticsRow(nil), userStatistics...)
|
sortedData := append([]database.GetUserStatisticsRow(nil), userStatistics...)
|
||||||
sort.SliceStable(sortedData, less)
|
sort.SliceStable(sortedData, less)
|
||||||
|
|
||||||
newData := make([]map[string]any, 0)
|
newData := make([]map[string]interface{}, 0)
|
||||||
for _, item := range sortedData {
|
for _, item := range sortedData {
|
||||||
v := reflect.Indirect(reflect.ValueOf(item))
|
v := reflect.Indirect(reflect.ValueOf(item))
|
||||||
|
|
||||||
@@ -1041,7 +1076,7 @@ func arrangeUserStatistics(userStatistics []database.GetUserStatisticsRow) gin.H
|
|||||||
value = niceNumbers(rawVal)
|
value = niceNumbers(rawVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
newData = append(newData, map[string]any{
|
newData = append(newData, map[string]interface{}{
|
||||||
"UserID": item.UserID,
|
"UserID": item.UserID,
|
||||||
"Value": value,
|
"Value": value,
|
||||||
})
|
})
|
||||||
|
|||||||
39
api/auth.go
39
api/auth.go
@@ -1,7 +1,6 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -29,8 +28,8 @@ type authKOHeader struct {
|
|||||||
AuthKey string `header:"x-auth-key"`
|
AuthKey string `header:"x-auth-key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) authorizeCredentials(ctx context.Context, username string, password string) (auth *authData) {
|
func (api *API) authorizeCredentials(username string, password string) (auth *authData) {
|
||||||
user, err := api.db.Queries.GetUser(ctx, username)
|
user, err := api.db.Queries.GetUser(api.db.Ctx, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -53,7 +52,7 @@ func (api *API) authKOMiddleware(c *gin.Context) {
|
|||||||
session := sessions.Default(c)
|
session := sessions.Default(c)
|
||||||
|
|
||||||
// Check Session First
|
// Check Session First
|
||||||
if auth, ok := api.getSession(c, session); ok {
|
if auth, ok := api.getSession(session); ok {
|
||||||
c.Set("Authorization", auth)
|
c.Set("Authorization", auth)
|
||||||
c.Header("Cache-Control", "private")
|
c.Header("Cache-Control", "private")
|
||||||
c.Next()
|
c.Next()
|
||||||
@@ -72,7 +71,7 @@ func (api *API) authKOMiddleware(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
authData := api.authorizeCredentials(c, rHeader.AuthUser, rHeader.AuthKey)
|
authData := api.authorizeCredentials(rHeader.AuthUser, rHeader.AuthKey)
|
||||||
if authData == nil {
|
if authData == nil {
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||||
return
|
return
|
||||||
@@ -101,7 +100,7 @@ func (api *API) authOPDSMiddleware(c *gin.Context) {
|
|||||||
|
|
||||||
// Validate Auth
|
// Validate Auth
|
||||||
password := fmt.Sprintf("%x", md5.Sum([]byte(rawPassword)))
|
password := fmt.Sprintf("%x", md5.Sum([]byte(rawPassword)))
|
||||||
authData := api.authorizeCredentials(c, user, password)
|
authData := api.authorizeCredentials(user, password)
|
||||||
if authData == nil {
|
if authData == nil {
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||||
return
|
return
|
||||||
@@ -116,7 +115,7 @@ func (api *API) authWebAppMiddleware(c *gin.Context) {
|
|||||||
session := sessions.Default(c)
|
session := sessions.Default(c)
|
||||||
|
|
||||||
// Check Session
|
// Check Session
|
||||||
if auth, ok := api.getSession(c, session); ok {
|
if auth, ok := api.getSession(session); ok {
|
||||||
c.Set("Authorization", auth)
|
c.Set("Authorization", auth)
|
||||||
c.Header("Cache-Control", "private")
|
c.Header("Cache-Control", "private")
|
||||||
c.Next()
|
c.Next()
|
||||||
@@ -154,7 +153,7 @@ func (api *API) appAuthLogin(c *gin.Context) {
|
|||||||
|
|
||||||
// MD5 - KOSync Compatiblity
|
// MD5 - KOSync Compatiblity
|
||||||
password := fmt.Sprintf("%x", md5.Sum([]byte(rawPassword)))
|
password := fmt.Sprintf("%x", md5.Sum([]byte(rawPassword)))
|
||||||
authData := api.authorizeCredentials(c, username, password)
|
authData := api.authorizeCredentials(username, password)
|
||||||
if authData == nil {
|
if authData == nil {
|
||||||
templateVars["Error"] = "Invalid Credentials"
|
templateVars["Error"] = "Invalid Credentials"
|
||||||
c.HTML(http.StatusUnauthorized, "page/login", templateVars)
|
c.HTML(http.StatusUnauthorized, "page/login", templateVars)
|
||||||
@@ -209,7 +208,7 @@ func (api *API) appAuthRegister(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get current users
|
// Get current users
|
||||||
currentUsers, err := api.db.Queries.GetUsers(c)
|
currentUsers, err := api.db.Queries.GetUsers(api.db.Ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Failed to check all users: ", err)
|
log.Error("Failed to check all users: ", err)
|
||||||
templateVars["Error"] = "Failed to Create User"
|
templateVars["Error"] = "Failed to Create User"
|
||||||
@@ -225,7 +224,7 @@ func (api *API) appAuthRegister(c *gin.Context) {
|
|||||||
|
|
||||||
// Create user in DB
|
// Create user in DB
|
||||||
authHash := fmt.Sprintf("%x", rawAuthHash)
|
authHash := fmt.Sprintf("%x", rawAuthHash)
|
||||||
if rows, err := api.db.Queries.CreateUser(c, database.CreateUserParams{
|
if rows, err := api.db.Queries.CreateUser(api.db.Ctx, database.CreateUserParams{
|
||||||
ID: username,
|
ID: username,
|
||||||
Pass: &hashedPassword,
|
Pass: &hashedPassword,
|
||||||
AuthHash: &authHash,
|
AuthHash: &authHash,
|
||||||
@@ -243,7 +242,7 @@ func (api *API) appAuthRegister(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get user
|
// Get user
|
||||||
user, err := api.db.Queries.GetUser(c, username)
|
user, err := api.db.Queries.GetUser(api.db.Ctx, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetUser DB Error:", err)
|
log.Error("GetUser DB Error:", err)
|
||||||
templateVars["Error"] = "Registration Disabled or User Already Exists"
|
templateVars["Error"] = "Registration Disabled or User Already Exists"
|
||||||
@@ -313,7 +312,7 @@ func (api *API) koAuthRegister(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get current users
|
// Get current users
|
||||||
currentUsers, err := api.db.Queries.GetUsers(c)
|
currentUsers, err := api.db.Queries.GetUsers(api.db.Ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Failed to check all users: ", err)
|
log.Error("Failed to check all users: ", err)
|
||||||
apiErrorPage(c, http.StatusBadRequest, "Failed to Create User")
|
apiErrorPage(c, http.StatusBadRequest, "Failed to Create User")
|
||||||
@@ -328,7 +327,7 @@ func (api *API) koAuthRegister(c *gin.Context) {
|
|||||||
|
|
||||||
// Create user
|
// Create user
|
||||||
authHash := fmt.Sprintf("%x", rawAuthHash)
|
authHash := fmt.Sprintf("%x", rawAuthHash)
|
||||||
if rows, err := api.db.Queries.CreateUser(c, database.CreateUserParams{
|
if rows, err := api.db.Queries.CreateUser(api.db.Ctx, database.CreateUserParams{
|
||||||
ID: rUser.Username,
|
ID: rUser.Username,
|
||||||
Pass: &hashedPassword,
|
Pass: &hashedPassword,
|
||||||
AuthHash: &authHash,
|
AuthHash: &authHash,
|
||||||
@@ -348,7 +347,7 @@ func (api *API) koAuthRegister(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) getSession(ctx context.Context, session sessions.Session) (auth authData, ok bool) {
|
func (api *API) getSession(session sessions.Session) (auth authData, ok bool) {
|
||||||
// Get Session
|
// Get Session
|
||||||
authorizedUser := session.Get("authorizedUser")
|
authorizedUser := session.Get("authorizedUser")
|
||||||
isAdmin := session.Get("isAdmin")
|
isAdmin := session.Get("isAdmin")
|
||||||
@@ -366,7 +365,7 @@ func (api *API) getSession(ctx context.Context, session sessions.Session) (auth
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate Auth Hash
|
// Validate Auth Hash
|
||||||
correctAuthHash, err := api.getUserAuthHash(ctx, auth.UserName)
|
correctAuthHash, err := api.getUserAuthHash(auth.UserName)
|
||||||
if err != nil || correctAuthHash != auth.AuthHash {
|
if err != nil || correctAuthHash != auth.AuthHash {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -394,14 +393,14 @@ func (api *API) setSession(session sessions.Session, auth authData) error {
|
|||||||
return session.Save()
|
return session.Save()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) getUserAuthHash(ctx context.Context, username string) (string, error) {
|
func (api *API) getUserAuthHash(username string) (string, error) {
|
||||||
// Return Cache
|
// Return Cache
|
||||||
if api.userAuthCache[username] != "" {
|
if api.userAuthCache[username] != "" {
|
||||||
return api.userAuthCache[username], nil
|
return api.userAuthCache[username], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get DB
|
// Get DB
|
||||||
user, err := api.db.Queries.GetUser(ctx, username)
|
user, err := api.db.Queries.GetUser(api.db.Ctx, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetUser DB Error:", err)
|
log.Error("GetUser DB Error:", err)
|
||||||
return "", err
|
return "", err
|
||||||
@@ -413,7 +412,7 @@ func (api *API) getUserAuthHash(ctx context.Context, username string) (string, e
|
|||||||
return api.userAuthCache[username], nil
|
return api.userAuthCache[username], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) rotateAllAuthHashes(ctx context.Context) error {
|
func (api *API) rotateAllAuthHashes() error {
|
||||||
// Do Transaction
|
// Do Transaction
|
||||||
tx, err := api.db.DB.Begin()
|
tx, err := api.db.DB.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -429,7 +428,7 @@ func (api *API) rotateAllAuthHashes(ctx context.Context) error {
|
|||||||
}()
|
}()
|
||||||
qtx := api.db.Queries.WithTx(tx)
|
qtx := api.db.Queries.WithTx(tx)
|
||||||
|
|
||||||
users, err := qtx.GetUsers(ctx)
|
users, err := qtx.GetUsers(api.db.Ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -445,7 +444,7 @@ func (api *API) rotateAllAuthHashes(ctx context.Context) error {
|
|||||||
|
|
||||||
// Update User
|
// Update User
|
||||||
authHash := fmt.Sprintf("%x", rawAuthHash)
|
authHash := fmt.Sprintf("%x", rawAuthHash)
|
||||||
if _, err = qtx.UpdateUser(ctx, database.UpdateUserParams{
|
if _, err = qtx.UpdateUser(api.db.Ctx, database.UpdateUserParams{
|
||||||
UserID: user.ID,
|
UserID: user.ID,
|
||||||
AuthHash: &authHash,
|
AuthHash: &authHash,
|
||||||
Admin: user.Admin,
|
Admin: user.Admin,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ func (api *API) createDownloadDocumentHandler(errorFunc func(*gin.Context, int,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get Document
|
// Get Document
|
||||||
document, err := api.db.Queries.GetDocument(c, rDoc.DocumentID)
|
document, err := api.db.Queries.GetDocument(api.db.Ctx, rDoc.DocumentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetDocument DB Error:", err)
|
log.Error("GetDocument DB Error:", err)
|
||||||
errorFunc(c, http.StatusBadRequest, "Unknown Document")
|
errorFunc(c, http.StatusBadRequest, "Unknown Document")
|
||||||
@@ -68,7 +68,7 @@ func (api *API) createGetCoverHandler(errorFunc func(*gin.Context, int, string))
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate Document Exists in DB
|
// Validate Document Exists in DB
|
||||||
document, err := api.db.Queries.GetDocument(c, rDoc.DocumentID)
|
document, err := api.db.Queries.GetDocument(api.db.Ctx, rDoc.DocumentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetDocument DB Error:", err)
|
log.Error("GetDocument DB Error:", err)
|
||||||
errorFunc(c, http.StatusInternalServerError, fmt.Sprintf("GetDocument DB Error: %v", err))
|
errorFunc(c, http.StatusInternalServerError, fmt.Sprintf("GetDocument DB Error: %v", err))
|
||||||
@@ -117,7 +117,7 @@ func (api *API) createGetCoverHandler(errorFunc func(*gin.Context, int, string))
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Store First Metadata Result
|
// Store First Metadata Result
|
||||||
if _, err = api.db.Queries.AddMetadata(c, database.AddMetadataParams{
|
if _, err = api.db.Queries.AddMetadata(api.db.Ctx, database.AddMetadataParams{
|
||||||
DocumentID: document.ID,
|
DocumentID: document.ID,
|
||||||
Title: firstResult.Title,
|
Title: firstResult.Title,
|
||||||
Author: firstResult.Author,
|
Author: firstResult.Author,
|
||||||
@@ -132,7 +132,7 @@ func (api *API) createGetCoverHandler(errorFunc func(*gin.Context, int, string))
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Upsert Document
|
// Upsert Document
|
||||||
if _, err = api.db.Queries.UpsertDocument(c, database.UpsertDocumentParams{
|
if _, err = api.db.Queries.UpsertDocument(api.db.Ctx, database.UpsertDocumentParams{
|
||||||
ID: document.ID,
|
ID: document.ID,
|
||||||
Coverfile: &coverFile,
|
Coverfile: &coverFile,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ type requestDocumentID struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) koAuthorizeUser(c *gin.Context) {
|
func (api *API) koAuthorizeUser(c *gin.Context) {
|
||||||
koJSON(c, 200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"authorized": "OK",
|
"authorized": "OK",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -91,7 +91,7 @@ func (api *API) koSetProgress(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Upsert Device
|
// Upsert Device
|
||||||
if _, err := api.db.Queries.UpsertDevice(c, database.UpsertDeviceParams{
|
if _, err := api.db.Queries.UpsertDevice(api.db.Ctx, database.UpsertDeviceParams{
|
||||||
ID: rPosition.DeviceID,
|
ID: rPosition.DeviceID,
|
||||||
UserID: auth.UserName,
|
UserID: auth.UserName,
|
||||||
DeviceName: rPosition.Device,
|
DeviceName: rPosition.Device,
|
||||||
@@ -101,14 +101,14 @@ func (api *API) koSetProgress(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Upsert Document
|
// Upsert Document
|
||||||
if _, err := api.db.Queries.UpsertDocument(c, database.UpsertDocumentParams{
|
if _, err := api.db.Queries.UpsertDocument(api.db.Ctx, database.UpsertDocumentParams{
|
||||||
ID: rPosition.DocumentID,
|
ID: rPosition.DocumentID,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Error("UpsertDocument DB Error:", err)
|
log.Error("UpsertDocument DB Error:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create or Replace Progress
|
// Create or Replace Progress
|
||||||
progress, err := api.db.Queries.UpdateProgress(c, database.UpdateProgressParams{
|
progress, err := api.db.Queries.UpdateProgress(api.db.Ctx, database.UpdateProgressParams{
|
||||||
Percentage: rPosition.Percentage,
|
Percentage: rPosition.Percentage,
|
||||||
DocumentID: rPosition.DocumentID,
|
DocumentID: rPosition.DocumentID,
|
||||||
DeviceID: rPosition.DeviceID,
|
DeviceID: rPosition.DeviceID,
|
||||||
@@ -121,7 +121,7 @@ func (api *API) koSetProgress(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
koJSON(c, http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"document": progress.DocumentID,
|
"document": progress.DocumentID,
|
||||||
"timestamp": progress.CreatedAt,
|
"timestamp": progress.CreatedAt,
|
||||||
})
|
})
|
||||||
@@ -140,14 +140,14 @@ func (api *API) koGetProgress(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
progress, err := api.db.Queries.GetDocumentProgress(c, database.GetDocumentProgressParams{
|
progress, err := api.db.Queries.GetDocumentProgress(api.db.Ctx, database.GetDocumentProgressParams{
|
||||||
DocumentID: rDocID.DocumentID,
|
DocumentID: rDocID.DocumentID,
|
||||||
UserID: auth.UserName,
|
UserID: auth.UserName,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
// Not Found
|
// Not Found
|
||||||
koJSON(c, http.StatusOK, gin.H{})
|
c.JSON(http.StatusOK, gin.H{})
|
||||||
return
|
return
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
log.Error("GetDocumentProgress DB Error:", err)
|
log.Error("GetDocumentProgress DB Error:", err)
|
||||||
@@ -155,7 +155,7 @@ func (api *API) koGetProgress(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
koJSON(c, http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"document": progress.DocumentID,
|
"document": progress.DocumentID,
|
||||||
"percentage": progress.Percentage,
|
"percentage": progress.Percentage,
|
||||||
"progress": progress.Progress,
|
"progress": progress.Progress,
|
||||||
@@ -202,7 +202,7 @@ func (api *API) koAddActivities(c *gin.Context) {
|
|||||||
|
|
||||||
// Upsert Documents
|
// Upsert Documents
|
||||||
for _, doc := range allDocuments {
|
for _, doc := range allDocuments {
|
||||||
if _, err := qtx.UpsertDocument(c, database.UpsertDocumentParams{
|
if _, err := qtx.UpsertDocument(api.db.Ctx, database.UpsertDocumentParams{
|
||||||
ID: doc,
|
ID: doc,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Error("UpsertDocument DB Error:", err)
|
log.Error("UpsertDocument DB Error:", err)
|
||||||
@@ -212,7 +212,7 @@ func (api *API) koAddActivities(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Upsert Device
|
// Upsert Device
|
||||||
if _, err = qtx.UpsertDevice(c, database.UpsertDeviceParams{
|
if _, err = qtx.UpsertDevice(api.db.Ctx, database.UpsertDeviceParams{
|
||||||
ID: rActivity.DeviceID,
|
ID: rActivity.DeviceID,
|
||||||
UserID: auth.UserName,
|
UserID: auth.UserName,
|
||||||
DeviceName: rActivity.Device,
|
DeviceName: rActivity.Device,
|
||||||
@@ -225,7 +225,7 @@ func (api *API) koAddActivities(c *gin.Context) {
|
|||||||
|
|
||||||
// Add All Activity
|
// Add All Activity
|
||||||
for _, item := range rActivity.Activity {
|
for _, item := range rActivity.Activity {
|
||||||
if _, err := qtx.AddActivity(c, database.AddActivityParams{
|
if _, err := qtx.AddActivity(api.db.Ctx, database.AddActivityParams{
|
||||||
UserID: auth.UserName,
|
UserID: auth.UserName,
|
||||||
DocumentID: item.DocumentID,
|
DocumentID: item.DocumentID,
|
||||||
DeviceID: rActivity.DeviceID,
|
DeviceID: rActivity.DeviceID,
|
||||||
@@ -247,7 +247,7 @@ func (api *API) koAddActivities(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
koJSON(c, http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"added": len(rActivity.Activity),
|
"added": len(rActivity.Activity),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -266,7 +266,7 @@ func (api *API) koCheckActivitySync(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Upsert Device
|
// Upsert Device
|
||||||
if _, err := api.db.Queries.UpsertDevice(c, database.UpsertDeviceParams{
|
if _, err := api.db.Queries.UpsertDevice(api.db.Ctx, database.UpsertDeviceParams{
|
||||||
ID: rCheckActivity.DeviceID,
|
ID: rCheckActivity.DeviceID,
|
||||||
UserID: auth.UserName,
|
UserID: auth.UserName,
|
||||||
DeviceName: rCheckActivity.Device,
|
DeviceName: rCheckActivity.Device,
|
||||||
@@ -278,7 +278,7 @@ func (api *API) koCheckActivitySync(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get Last Device Activity
|
// Get Last Device Activity
|
||||||
lastActivity, err := api.db.Queries.GetLastActivity(c, database.GetLastActivityParams{
|
lastActivity, err := api.db.Queries.GetLastActivity(api.db.Ctx, database.GetLastActivityParams{
|
||||||
UserID: auth.UserName,
|
UserID: auth.UserName,
|
||||||
DeviceID: rCheckActivity.DeviceID,
|
DeviceID: rCheckActivity.DeviceID,
|
||||||
})
|
})
|
||||||
@@ -298,7 +298,7 @@ func (api *API) koCheckActivitySync(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
koJSON(c, http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"last_sync": parsedTime.Unix(),
|
"last_sync": parsedTime.Unix(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -329,7 +329,7 @@ func (api *API) koAddDocuments(c *gin.Context) {
|
|||||||
|
|
||||||
// Upsert Documents
|
// Upsert Documents
|
||||||
for _, doc := range rNewDocs.Documents {
|
for _, doc := range rNewDocs.Documents {
|
||||||
_, err := qtx.UpsertDocument(c, database.UpsertDocumentParams{
|
_, err := qtx.UpsertDocument(api.db.Ctx, database.UpsertDocumentParams{
|
||||||
ID: doc.ID,
|
ID: doc.ID,
|
||||||
Title: api.sanitizeInput(doc.Title),
|
Title: api.sanitizeInput(doc.Title),
|
||||||
Author: api.sanitizeInput(doc.Author),
|
Author: api.sanitizeInput(doc.Author),
|
||||||
@@ -352,7 +352,7 @@ func (api *API) koAddDocuments(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
koJSON(c, http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"changed": len(rNewDocs.Documents),
|
"changed": len(rNewDocs.Documents),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -371,7 +371,7 @@ func (api *API) koCheckDocumentsSync(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Upsert Device
|
// Upsert Device
|
||||||
_, err := api.db.Queries.UpsertDevice(c, database.UpsertDeviceParams{
|
_, err := api.db.Queries.UpsertDevice(api.db.Ctx, database.UpsertDeviceParams{
|
||||||
ID: rCheckDocs.DeviceID,
|
ID: rCheckDocs.DeviceID,
|
||||||
UserID: auth.UserName,
|
UserID: auth.UserName,
|
||||||
DeviceName: rCheckDocs.Device,
|
DeviceName: rCheckDocs.Device,
|
||||||
@@ -384,7 +384,7 @@ func (api *API) koCheckDocumentsSync(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get Missing Documents
|
// Get Missing Documents
|
||||||
missingDocs, err := api.db.Queries.GetMissingDocuments(c, rCheckDocs.Have)
|
missingDocs, err := api.db.Queries.GetMissingDocuments(api.db.Ctx, rCheckDocs.Have)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetMissingDocuments DB Error", err)
|
log.Error("GetMissingDocuments DB Error", err)
|
||||||
apiErrorPage(c, http.StatusBadRequest, "Invalid Request")
|
apiErrorPage(c, http.StatusBadRequest, "Invalid Request")
|
||||||
@@ -392,7 +392,7 @@ func (api *API) koCheckDocumentsSync(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get Deleted Documents
|
// Get Deleted Documents
|
||||||
deletedDocIDs, err := api.db.Queries.GetDeletedDocuments(c, rCheckDocs.Have)
|
deletedDocIDs, err := api.db.Queries.GetDeletedDocuments(api.db.Ctx, rCheckDocs.Have)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetDeletedDocuments DB Error", err)
|
log.Error("GetDeletedDocuments DB Error", err)
|
||||||
apiErrorPage(c, http.StatusBadRequest, "Invalid Request")
|
apiErrorPage(c, http.StatusBadRequest, "Invalid Request")
|
||||||
@@ -407,7 +407,7 @@ func (api *API) koCheckDocumentsSync(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
wantedDocs, err := api.db.Queries.GetWantedDocuments(c, string(jsonHaves))
|
wantedDocs, err := api.db.Queries.GetWantedDocuments(api.db.Ctx, string(jsonHaves))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetWantedDocuments DB Error", err)
|
log.Error("GetWantedDocuments DB Error", err)
|
||||||
apiErrorPage(c, http.StatusBadRequest, "Invalid Request")
|
apiErrorPage(c, http.StatusBadRequest, "Invalid Request")
|
||||||
@@ -447,7 +447,7 @@ func (api *API) koCheckDocumentsSync(c *gin.Context) {
|
|||||||
rCheckDocSync.Delete = deletedDocIDs
|
rCheckDocSync.Delete = deletedDocIDs
|
||||||
}
|
}
|
||||||
|
|
||||||
koJSON(c, http.StatusOK, rCheckDocSync)
|
c.JSON(http.StatusOK, rCheckDocSync)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) koUploadExistingDocument(c *gin.Context) {
|
func (api *API) koUploadExistingDocument(c *gin.Context) {
|
||||||
@@ -467,7 +467,7 @@ func (api *API) koUploadExistingDocument(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate Document Exists in DB
|
// Validate Document Exists in DB
|
||||||
document, err := api.db.Queries.GetDocument(c, rDoc.DocumentID)
|
document, err := api.db.Queries.GetDocument(api.db.Ctx, rDoc.DocumentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetDocument DB Error:", err)
|
log.Error("GetDocument DB Error:", err)
|
||||||
apiErrorPage(c, http.StatusBadRequest, "Unknown Document")
|
apiErrorPage(c, http.StatusBadRequest, "Unknown Document")
|
||||||
@@ -499,8 +499,7 @@ func (api *API) koUploadExistingDocument(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Generate Storage Path
|
// Generate Storage Path
|
||||||
basePath := filepath.Join(api.cfg.DataPath, "documents")
|
safePath := filepath.Join(api.cfg.DataPath, "documents", fileName)
|
||||||
safePath := filepath.Join(basePath, fileName)
|
|
||||||
|
|
||||||
// Save & Prevent Overwrites
|
// Save & Prevent Overwrites
|
||||||
_, err = os.Stat(safePath)
|
_, err = os.Stat(safePath)
|
||||||
@@ -522,19 +521,18 @@ func (api *API) koUploadExistingDocument(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Upsert Document
|
// Upsert Document
|
||||||
if _, err = api.db.Queries.UpsertDocument(c, database.UpsertDocumentParams{
|
if _, err = api.db.Queries.UpsertDocument(api.db.Ctx, database.UpsertDocumentParams{
|
||||||
ID: document.ID,
|
ID: document.ID,
|
||||||
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")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
koJSON(c, http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -589,10 +587,3 @@ func getFileMD5(filePath string) (*string, error) {
|
|||||||
|
|
||||||
return &fileHash, nil
|
return &fileHash, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// koJSON forces koJSON Content-Type to only return `application/json`. This is addressing
|
|
||||||
// the following issue: https://github.com/koreader/koreader/issues/13629
|
|
||||||
func koJSON(c *gin.Context, code int, obj any) {
|
|
||||||
c.Header("Content-Type", "application/json")
|
|
||||||
c.JSON(code, obj)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"reichard.io/antholume/database"
|
"reichard.io/antholume/database"
|
||||||
"reichard.io/antholume/opds"
|
"reichard.io/antholume/opds"
|
||||||
"reichard.io/antholume/pkg/ptr"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var mimeMapping map[string]string = map[string]string{
|
var mimeMapping map[string]string = map[string]string{
|
||||||
@@ -78,12 +77,11 @@ func (api *API) opdsDocuments(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get Documents
|
// Get Documents
|
||||||
documents, err := api.db.Queries.GetDocumentsWithStats(c, database.GetDocumentsWithStatsParams{
|
documents, err := api.db.Queries.GetDocumentsWithStats(api.db.Ctx, database.GetDocumentsWithStatsParams{
|
||||||
UserID: auth.UserName,
|
UserID: auth.UserName,
|
||||||
Query: query,
|
Query: query,
|
||||||
Deleted: ptr.Of(false),
|
Offset: (*qParams.Page - 1) * *qParams.Limit,
|
||||||
Offset: (*qParams.Page - 1) * *qParams.Limit,
|
Limit: *qParams.Limit,
|
||||||
Limit: *qParams.Limit,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetDocumentsWithStats DB Error:", err)
|
log.Error("GetDocumentsWithStats DB Error:", err)
|
||||||
|
|||||||
30
api/utils.go
30
api/utils.go
@@ -13,7 +13,6 @@ import (
|
|||||||
"reichard.io/antholume/metadata"
|
"reichard.io/antholume/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
// getTimeZones returns a string slice of IANA timezones.
|
|
||||||
func getTimeZones() []string {
|
func getTimeZones() []string {
|
||||||
return []string{
|
return []string{
|
||||||
"Africa/Cairo",
|
"Africa/Cairo",
|
||||||
@@ -53,9 +52,6 @@ func getTimeZones() []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// niceSeconds takes in an int (in seconds) and returns a string readable
|
|
||||||
// representation. For example 1928371 -> "22d 7h 39m 31s".
|
|
||||||
// Deprecated: Use formatters.FormatDuration
|
|
||||||
func niceSeconds(input int64) (result string) {
|
func niceSeconds(input int64) (result string) {
|
||||||
if input == 0 {
|
if input == 0 {
|
||||||
return "N/A"
|
return "N/A"
|
||||||
@@ -84,9 +80,6 @@ func niceSeconds(input int64) (result string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// niceNumbers takes in an int and returns a string representation. For example
|
|
||||||
// 19823 -> "19.8k".
|
|
||||||
// Deprecated: Use formatters.FormatNumber
|
|
||||||
func niceNumbers(input int64) string {
|
func niceNumbers(input int64) string {
|
||||||
if input == 0 {
|
if input == 0 {
|
||||||
return "0"
|
return "0"
|
||||||
@@ -105,8 +98,7 @@ func niceNumbers(input int64) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getSVGGraphData builds SVGGraphData from the provided stats, width and height.
|
// Convert Database Array -> Int64 Array
|
||||||
// It is used exclusively in templates to generate the daily read stats graph.
|
|
||||||
func getSVGGraphData(inputData []database.GetDailyReadStatsRow, svgWidth int, svgHeight int) graph.SVGGraphData {
|
func getSVGGraphData(inputData []database.GetDailyReadStatsRow, svgWidth int, svgHeight int) graph.SVGGraphData {
|
||||||
var intData []int64
|
var intData []int64
|
||||||
for _, item := range inputData {
|
for _, item := range inputData {
|
||||||
@@ -116,13 +108,11 @@ func getSVGGraphData(inputData []database.GetDailyReadStatsRow, svgWidth int, sv
|
|||||||
return graph.GetSVGGraphData(intData, svgWidth, svgHeight)
|
return graph.GetSVGGraphData(intData, svgWidth, svgHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
// dict returns a map[string]any dict. Each pair of two is a key & value
|
func dict(values ...interface{}) (map[string]interface{}, error) {
|
||||||
// respectively. It's primarily utilized in templates.
|
|
||||||
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]any, len(values)/2)
|
dict := make(map[string]interface{}, 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 {
|
||||||
@@ -133,14 +123,12 @@ func dict(values ...any) (map[string]any, error) {
|
|||||||
return dict, nil
|
return dict, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// fields returns a map[string]any of the provided struct. It's primarily
|
func fields(value interface{}) (map[string]interface{}, error) {
|
||||||
// utilized in templates.
|
|
||||||
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]any)
|
m := make(map[string]interface{})
|
||||||
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)
|
||||||
@@ -149,13 +137,6 @@ func fields(value any) (map[string]any, error) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// slice returns a slice of the provided arguments. It's primarily utilized in
|
|
||||||
// templates.
|
|
||||||
func slice(elements ...any) []any {
|
|
||||||
return elements
|
|
||||||
}
|
|
||||||
|
|
||||||
// deriveBaseFileName builds the base filename for a given MetadataInfo object.
|
|
||||||
func deriveBaseFileName(metadataInfo *metadata.MetadataInfo) string {
|
func deriveBaseFileName(metadataInfo *metadata.MetadataInfo) string {
|
||||||
// Derive New FileName
|
// Derive New FileName
|
||||||
var newFileName string
|
var newFileName string
|
||||||
@@ -175,7 +156,6 @@ func deriveBaseFileName(metadataInfo *metadata.MetadataInfo) string {
|
|||||||
return "." + filepath.Clean(fmt.Sprintf("/%s [%s]%s", fileName, *metadataInfo.PartialMD5, metadataInfo.Type))
|
return "." + filepath.Clean(fmt.Sprintf("/%s [%s]%s", fileName, *metadataInfo.PartialMD5, metadataInfo.Type))
|
||||||
}
|
}
|
||||||
|
|
||||||
// importStatusPriority returns the order priority for import status in the UI.
|
|
||||||
func importStatusPriority(status importStatus) int {
|
func importStatusPriority(status importStatus) int {
|
||||||
switch status {
|
switch status {
|
||||||
case importFailed:
|
case importFailed:
|
||||||
|
|||||||
@@ -82,30 +82,13 @@
|
|||||||
id="top-bar"
|
id="top-bar"
|
||||||
class="transition-all duration-200 absolute z-10 bg-gray-100 dark:bg-gray-800 w-full px-2"
|
class="transition-all duration-200 absolute z-10 bg-gray-100 dark:bg-gray-800 w-full px-2"
|
||||||
>
|
>
|
||||||
<div class="max-h-[75vh] w-full flex flex-col items-center justify-around relative dark:text-white">
|
<div class="w-full h-32 flex items-center justify-around relative">
|
||||||
<div class="h-32">
|
<div class="text-gray-500 absolute top-6 left-4 flex flex-col gap-4">
|
||||||
<div class="text-gray-500 absolute top-6 left-4 flex flex-col gap-4">
|
<a href="#">
|
||||||
<a href="#">
|
|
||||||
<svg
|
|
||||||
width="32"
|
|
||||||
height="32"
|
|
||||||
class="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="M20.5355 3.46447C19.0711 2 16.714 2 12 2C7.28595 2 4.92893 2 3.46447 3.46447C2 4.92893 2 7.28595 2 12C2 16.714 2 19.0711 3.46447 20.5355C4.92893 22 7.28595 22 12 22C16.714 22 19.0711 22 20.5355 20.5355C22 19.0711 22 16.714 22 12C22 7.28595 22 4.92893 20.5355 3.46447ZM14.0303 8.46967C14.3232 8.76256 14.3232 9.23744 14.0303 9.53033L11.5607 12L14.0303 14.4697C14.3232 14.7626 14.3232 15.2374 14.0303 15.5303C13.7374 15.8232 13.2626 15.8232 12.9697 15.5303L9.96967 12.5303C9.82902 12.3897 9.75 12.1989 9.75 12C9.75 11.8011 9.82902 11.6103 9.96967 11.4697L12.9697 8.46967C13.2626 8.17678 13.7374 8.17678 14.0303 8.46967Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
width="32"
|
width="32"
|
||||||
height="32"
|
height="32"
|
||||||
class="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100 close-top-bar"
|
class="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -113,47 +96,61 @@
|
|||||||
<path
|
<path
|
||||||
fill-rule="evenodd"
|
fill-rule="evenodd"
|
||||||
clip-rule="evenodd"
|
clip-rule="evenodd"
|
||||||
d="M12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22ZM8.96965 8.96967C9.26254 8.67678 9.73742 8.67678 10.0303 8.96967L12 10.9394L13.9696 8.96969C14.2625 8.6768 14.7374 8.6768 15.0303 8.96969C15.3232 9.26258 15.3232 9.73746 15.0303 10.0303L13.0606 12L15.0303 13.9697C15.3232 14.2625 15.3232 14.7374 15.0303 15.0303C14.7374 15.3232 14.2625 15.3232 13.9696 15.0303L12 13.0607L10.0303 15.0303C9.73744 15.3232 9.26256 15.3232 8.96967 15.0303C8.67678 14.7374 8.67678 14.2626 8.96967 13.9697L10.9393 12L8.96965 10.0303C8.67676 9.73744 8.67676 9.26256 8.96965 8.96967Z"
|
d="M20.5355 3.46447C19.0711 2 16.714 2 12 2C7.28595 2 4.92893 2 3.46447 3.46447C2 4.92893 2 7.28595 2 12C2 16.714 2 19.0711 3.46447 20.5355C4.92893 22 7.28595 22 12 22C16.714 22 19.0711 22 20.5355 20.5355C22 19.0711 22 16.714 22 12C22 7.28595 22 4.92893 20.5355 3.46447ZM14.0303 8.46967C14.3232 8.76256 14.3232 9.23744 14.0303 9.53033L11.5607 12L14.0303 14.4697C14.3232 14.7626 14.3232 15.2374 14.0303 15.5303C13.7374 15.8232 13.2626 15.8232 12.9697 15.5303L9.96967 12.5303C9.82902 12.3897 9.75 12.1989 9.75 12C9.75 11.8011 9.82902 11.6103 9.96967 11.4697L12.9697 8.46967C13.2626 8.17678 13.7374 8.17678 14.0303 8.46967Z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</a>
|
||||||
|
|
||||||
<div class="flex gap-10 h-full p-4 pl-14 rounded">
|
<svg
|
||||||
<div class="h-full my-auto relative">
|
width="32"
|
||||||
<a href="#">
|
height="32"
|
||||||
<img
|
class="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100 close-top-bar"
|
||||||
class="rounded object-cover h-full"
|
viewBox="0 0 24 24"
|
||||||
src="/assets/images/no-cover.jpg"
|
fill="currentColor"
|
||||||
/>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</a>
|
>
|
||||||
</div>
|
<path
|
||||||
<div class="flex gap-7 justify-around dark:text-white text-sm">
|
fill-rule="evenodd"
|
||||||
<div class="flex flex-col gap-4">
|
clip-rule="evenodd"
|
||||||
<div class="inline-flex shrink-0 items-center">
|
d="M12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22ZM8.96965 8.96967C9.26254 8.67678 9.73742 8.67678 10.0303 8.96967L12 10.9394L13.9696 8.96969C14.2625 8.6768 14.7374 8.6768 15.0303 8.96969C15.3232 9.26258 15.3232 9.73746 15.0303 10.0303L13.0606 12L15.0303 13.9697C15.3232 14.2625 15.3232 14.7374 15.0303 15.0303C14.7374 15.3232 14.2625 15.3232 13.9696 15.0303L12 13.0607L10.0303 15.0303C9.73744 15.3232 9.26256 15.3232 8.96967 15.0303C8.67678 14.7374 8.67678 14.2626 8.96967 13.9697L10.9393 12L8.96965 10.0303C8.67676 9.73744 8.67676 9.26256 8.96965 8.96967Z"
|
||||||
<div>
|
/>
|
||||||
<p class="text-gray-400">Title</p>
|
</svg>
|
||||||
<p
|
</div>
|
||||||
class="font-medium whitespace-nowrap text-ellipsis overflow-hidden max-w-[50dvw]"
|
|
||||||
>
|
<div class="flex gap-10 h-full p-4 pl-14 rounded">
|
||||||
"N/A"
|
<div class="h-full my-auto relative">
|
||||||
</p>
|
<a href="#">
|
||||||
</div>
|
<img
|
||||||
|
class="rounded object-cover h-full"
|
||||||
|
src="/assets/images/no-cover.jpg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-7 justify-around dark:text-white text-sm">
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="inline-flex shrink-0 items-center">
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-400">Title</p>
|
||||||
|
<p
|
||||||
|
class="font-medium whitespace-nowrap text-ellipsis overflow-hidden max-w-[50dvw]"
|
||||||
|
>
|
||||||
|
"N/A"
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="inline-flex shrink-0 items-center">
|
</div>
|
||||||
<div>
|
<div class="inline-flex shrink-0 items-center">
|
||||||
<p class="text-gray-400">Author</p>
|
<div>
|
||||||
<p
|
<p class="text-gray-400">Author</p>
|
||||||
class="font-medium whitespace-nowrap text-ellipsis overflow-hidden max-w-[50dvw]"
|
<p
|
||||||
>
|
class="font-medium whitespace-nowrap text-ellipsis overflow-hidden max-w-[50dvw]"
|
||||||
"N/A"
|
>
|
||||||
</p>
|
"N/A"
|
||||||
</div>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="toc" class="w-full text-center max-h-[50%] overflow-scroll no-scrollbar"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -66,56 +66,6 @@ function populateMetadata(data) {
|
|||||||
authorEl.innerText = data.author;
|
authorEl.innerText = data.author;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Populate the Table of Contents
|
|
||||||
**/
|
|
||||||
function populateTOC() {
|
|
||||||
if (!currentReader.book.navigation.toc) {
|
|
||||||
console.warn("[populateTOC] No TOC");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let tocEl = document.querySelector("#toc");
|
|
||||||
if (!tocEl) {
|
|
||||||
console.warn("[populateTOC] No TOC Element");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the Table of Contents
|
|
||||||
let parsedTOC = currentReader.book.navigation.toc.reduce((agg, item) => {
|
|
||||||
let sectionTitle = item.label.trim();
|
|
||||||
agg.push({ title: sectionTitle, href: item.href });
|
|
||||||
if (item.subitems.length == 0) {
|
|
||||||
return agg;
|
|
||||||
}
|
|
||||||
|
|
||||||
let allSubSections = item.subitems.map(item => {
|
|
||||||
let itemTitle = item.label.trim();
|
|
||||||
if (sectionTitle != "") {
|
|
||||||
itemTitle = sectionTitle + " - " + item.label.trim();
|
|
||||||
}
|
|
||||||
return { title: itemTitle, href: item.href };
|
|
||||||
});
|
|
||||||
agg.push(...allSubSections);
|
|
||||||
|
|
||||||
return agg;
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Add Table of Contents to DOM
|
|
||||||
let listEl = document.createElement("ul");
|
|
||||||
listEl.classList.add("m-4")
|
|
||||||
parsedTOC.forEach(item => {
|
|
||||||
let listItem = document.createElement("li");
|
|
||||||
listItem.style.cursor = "pointer";
|
|
||||||
listItem.addEventListener("click", () => {
|
|
||||||
currentReader.rendition.display(item.href);
|
|
||||||
});
|
|
||||||
listItem.textContent = item.title;
|
|
||||||
listEl.appendChild(listItem);
|
|
||||||
});
|
|
||||||
tocEl.appendChild(listEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the main reader class. All functionality is wrapped in this class.
|
* This is the main reader class. All functionality is wrapped in this class.
|
||||||
* Responsible for handling gesture / clicks, flushing progress & activity,
|
* Responsible for handling gesture / clicks, flushing progress & activity,
|
||||||
@@ -489,7 +439,6 @@ class EBookReader {
|
|||||||
// ------------------------------------------------ //
|
// ------------------------------------------------ //
|
||||||
// ----------------- Swipe Helpers ---------------- //
|
// ----------------- Swipe Helpers ---------------- //
|
||||||
// ------------------------------------------------ //
|
// ------------------------------------------------ //
|
||||||
let disablePagination = false;
|
|
||||||
let touchStartX,
|
let touchStartX,
|
||||||
touchStartY,
|
touchStartY,
|
||||||
touchEndX,
|
touchEndX,
|
||||||
@@ -510,38 +459,25 @@ class EBookReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Swipe Left
|
// Swipe Left
|
||||||
if (!disablePagination && touchEndX + drasticity < touchStartX) {
|
if (touchEndX + drasticity < touchStartX) {
|
||||||
nextPage();
|
nextPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Swipe Right
|
// Swipe Right
|
||||||
if (!disablePagination && touchEndX - drasticity > touchStartX) {
|
if (touchEndX - drasticity > touchStartX) {
|
||||||
prevPage();
|
prevPage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSwipeDown() {
|
function handleSwipeDown() {
|
||||||
if (bottomBar.classList.contains("bottom-0")) {
|
if (bottomBar.classList.contains("bottom-0"))
|
||||||
bottomBar.classList.remove("bottom-0");
|
bottomBar.classList.remove("bottom-0");
|
||||||
disablePagination = false;
|
else topBar.classList.add("top-0");
|
||||||
} else {
|
|
||||||
topBar.classList.add("top-0");
|
|
||||||
populateTOC()
|
|
||||||
disablePagination = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSwipeUp() {
|
function handleSwipeUp() {
|
||||||
if (topBar.classList.contains("top-0")) {
|
if (topBar.classList.contains("top-0")) topBar.classList.remove("top-0");
|
||||||
topBar.classList.remove("top-0");
|
else bottomBar.classList.add("bottom-0");
|
||||||
disablePagination = false;
|
|
||||||
|
|
||||||
const tocEl = document.querySelector("#toc");
|
|
||||||
if (tocEl) tocEl.innerHTML = "";
|
|
||||||
} else {
|
|
||||||
bottomBar.classList.add("bottom-0");
|
|
||||||
disablePagination = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.rendition.hooks.render.register(function (doc, data) {
|
this.rendition.hooks.render.register(function (doc, data) {
|
||||||
@@ -587,8 +523,8 @@ class EBookReader {
|
|||||||
// Handle Event
|
// Handle Event
|
||||||
if (yCoord < top) handleSwipeDown();
|
if (yCoord < top) handleSwipeDown();
|
||||||
else if (yCoord > bottom) handleSwipeUp();
|
else if (yCoord > bottom) handleSwipeUp();
|
||||||
else if (!disablePagination && xCoord < left) prevPage();
|
else if (xCoord < left) prevPage();
|
||||||
else if (!disablePagination && xCoord > right) nextPage();
|
else if (xCoord > right) nextPage();
|
||||||
else {
|
else {
|
||||||
bottomBar.classList.remove("bottom-0");
|
bottomBar.classList.remove("bottom-0");
|
||||||
topBar.classList.remove("top-0");
|
topBar.classList.remove("top-0");
|
||||||
@@ -734,9 +670,6 @@ class EBookReader {
|
|||||||
// Close Top Bar
|
// Close Top Bar
|
||||||
document.querySelector(".close-top-bar").addEventListener("click", () => {
|
document.querySelector(".close-top-bar").addEventListener("click", () => {
|
||||||
topBar.classList.remove("top-0");
|
topBar.classList.remove("top-0");
|
||||||
|
|
||||||
const tocEl = document.querySelector("#toc");
|
|
||||||
if (tocEl) tocEl.innerHTML = "";
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1016,16 +949,10 @@ class EBookReader {
|
|||||||
**/
|
**/
|
||||||
async getXPathFromCFI(cfi) {
|
async getXPathFromCFI(cfi) {
|
||||||
// Get DocFragment (Spine Index)
|
// Get DocFragment (Spine Index)
|
||||||
let cfiBaseMatch = cfi.match(/\(([^!]+)/);
|
let startCFI = cfi.replace("epubcfi(", "");
|
||||||
if (!cfiBaseMatch) {
|
|
||||||
console.error("[getXPathFromCFI] No CFI Match");
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
let startCFI = cfiBaseMatch[1];
|
|
||||||
|
|
||||||
let docFragmentIndex =
|
let docFragmentIndex =
|
||||||
this.book.spine.spineItems.find((item) =>
|
this.book.spine.spineItems.find((item) =>
|
||||||
item.cfiBase == startCFI
|
startCFI.startsWith(item.cfiBase),
|
||||||
).index + 1;
|
).index + 1;
|
||||||
|
|
||||||
// Base Progress
|
// Base Progress
|
||||||
@@ -1102,6 +1029,10 @@ class EBookReader {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Match Item Index
|
||||||
|
let indexMatch = xpath.match(/\.(\d+)$/);
|
||||||
|
let itemIndex = indexMatch ? parseInt(indexMatch[1]) : 0;
|
||||||
|
|
||||||
// Get Spine Item
|
// Get Spine Item
|
||||||
let spinePosition = parseInt(fragMatch[1]) - 1;
|
let spinePosition = parseInt(fragMatch[1]) - 1;
|
||||||
let sectionItem = this.book.spine.get(spinePosition);
|
let sectionItem = this.book.spine.get(spinePosition);
|
||||||
@@ -1193,11 +1124,6 @@ class EBookReader {
|
|||||||
let element = docSearch.iterateNext() || derivedSelectorElement;
|
let element = docSearch.iterateNext() || derivedSelectorElement;
|
||||||
let cfi = sectionItem.cfiFromElement(element);
|
let cfi = sectionItem.cfiFromElement(element);
|
||||||
|
|
||||||
// Hack - epub.js crashes sometimes when its a bare section with no element
|
|
||||||
// so just return the first.
|
|
||||||
if (cfi.endsWith("!/)"))
|
|
||||||
cfi = cfi.slice(0, -1) + "0)"
|
|
||||||
|
|
||||||
return { cfi, element };
|
return { cfi, element };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1345,3 +1271,14 @@ class EBookReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", initReader);
|
document.addEventListener("DOMContentLoaded", initReader);
|
||||||
|
|
||||||
|
// WIP
|
||||||
|
async function getTOC() {
|
||||||
|
let toc = currentReader.book.navigation.toc;
|
||||||
|
|
||||||
|
// Alternatively:
|
||||||
|
// let nav = await currentReader.book.loaded.navigation;
|
||||||
|
// let toc = nav.toc;
|
||||||
|
|
||||||
|
currentReader.rendition.display(nav.toc[10].href);
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// sqlc v1.29.0
|
// sqlc v1.25.0
|
||||||
|
|
||||||
package database
|
package database
|
||||||
|
|
||||||
|
|||||||
@@ -1,151 +0,0 @@
|
|||||||
WITH grouped_activity AS (
|
|
||||||
SELECT
|
|
||||||
ga.user_id,
|
|
||||||
ga.document_id,
|
|
||||||
MAX(ga.created_at) AS created_at,
|
|
||||||
MAX(ga.start_time) AS start_time,
|
|
||||||
MIN(ga.start_percentage) AS start_percentage,
|
|
||||||
MAX(ga.end_percentage) AS end_percentage,
|
|
||||||
|
|
||||||
-- Total Duration & Percentage
|
|
||||||
SUM(ga.duration) AS total_time_seconds,
|
|
||||||
SUM(ga.end_percentage - ga.start_percentage) AS total_read_percentage,
|
|
||||||
|
|
||||||
-- Yearly Duration
|
|
||||||
SUM(
|
|
||||||
CASE
|
|
||||||
WHEN
|
|
||||||
ga.start_time >= DATE('now', '-1 year')
|
|
||||||
THEN ga.duration
|
|
||||||
ELSE 0
|
|
||||||
END
|
|
||||||
)
|
|
||||||
AS yearly_time_seconds,
|
|
||||||
|
|
||||||
-- Yearly Percentage
|
|
||||||
SUM(
|
|
||||||
CASE
|
|
||||||
WHEN
|
|
||||||
ga.start_time >= DATE('now', '-1 year')
|
|
||||||
THEN ga.end_percentage - ga.start_percentage
|
|
||||||
ELSE 0
|
|
||||||
END
|
|
||||||
)
|
|
||||||
AS yearly_read_percentage,
|
|
||||||
|
|
||||||
-- Monthly Duration
|
|
||||||
SUM(
|
|
||||||
CASE
|
|
||||||
WHEN
|
|
||||||
ga.start_time >= DATE('now', '-1 month')
|
|
||||||
THEN ga.duration
|
|
||||||
ELSE 0
|
|
||||||
END
|
|
||||||
)
|
|
||||||
AS monthly_time_seconds,
|
|
||||||
|
|
||||||
-- Monthly Percentage
|
|
||||||
SUM(
|
|
||||||
CASE
|
|
||||||
WHEN
|
|
||||||
ga.start_time >= DATE('now', '-1 month')
|
|
||||||
THEN ga.end_percentage - ga.start_percentage
|
|
||||||
ELSE 0
|
|
||||||
END
|
|
||||||
)
|
|
||||||
AS monthly_read_percentage,
|
|
||||||
|
|
||||||
-- Weekly Duration
|
|
||||||
SUM(
|
|
||||||
CASE
|
|
||||||
WHEN
|
|
||||||
ga.start_time >= DATE('now', '-7 days')
|
|
||||||
THEN ga.duration
|
|
||||||
ELSE 0
|
|
||||||
END
|
|
||||||
)
|
|
||||||
AS weekly_time_seconds,
|
|
||||||
|
|
||||||
-- Weekly Percentage
|
|
||||||
SUM(
|
|
||||||
CASE
|
|
||||||
WHEN
|
|
||||||
ga.start_time >= DATE('now', '-7 days')
|
|
||||||
THEN ga.end_percentage - ga.start_percentage
|
|
||||||
ELSE 0
|
|
||||||
END
|
|
||||||
)
|
|
||||||
AS weekly_read_percentage
|
|
||||||
|
|
||||||
FROM activity AS ga
|
|
||||||
GROUP BY ga.user_id, ga.document_id
|
|
||||||
),
|
|
||||||
|
|
||||||
current_progress AS (
|
|
||||||
SELECT
|
|
||||||
user_id,
|
|
||||||
document_id,
|
|
||||||
COALESCE((
|
|
||||||
SELECT dp.percentage
|
|
||||||
FROM document_progress AS dp
|
|
||||||
WHERE
|
|
||||||
dp.user_id = iga.user_id
|
|
||||||
AND dp.document_id = iga.document_id
|
|
||||||
ORDER BY dp.created_at DESC
|
|
||||||
LIMIT 1
|
|
||||||
), end_percentage) AS percentage
|
|
||||||
FROM grouped_activity AS iga
|
|
||||||
)
|
|
||||||
|
|
||||||
INSERT INTO document_user_statistics
|
|
||||||
SELECT
|
|
||||||
ga.document_id,
|
|
||||||
ga.user_id,
|
|
||||||
cp.percentage,
|
|
||||||
MAX(ga.start_time) AS last_read,
|
|
||||||
MAX(ga.created_at) AS last_seen,
|
|
||||||
SUM(ga.total_read_percentage) AS read_percentage,
|
|
||||||
|
|
||||||
-- All Time WPM
|
|
||||||
SUM(ga.total_time_seconds) AS total_time_seconds,
|
|
||||||
(CAST(COALESCE(d.words, 0.0) AS REAL) * SUM(ga.total_read_percentage))
|
|
||||||
AS total_words_read,
|
|
||||||
(CAST(COALESCE(d.words, 0.0) AS REAL) * SUM(ga.total_read_percentage))
|
|
||||||
/ (SUM(ga.total_time_seconds) / 60.0) AS total_wpm,
|
|
||||||
|
|
||||||
-- Yearly WPM
|
|
||||||
ga.yearly_time_seconds,
|
|
||||||
CAST(COALESCE(d.words, 0.0) AS REAL) * ga.yearly_read_percentage
|
|
||||||
AS yearly_words_read,
|
|
||||||
COALESCE(
|
|
||||||
(CAST(COALESCE(d.words, 0.0) AS REAL) * ga.yearly_read_percentage)
|
|
||||||
/ (ga.yearly_time_seconds / 60), 0.0)
|
|
||||||
AS yearly_wpm,
|
|
||||||
|
|
||||||
-- Monthly WPM
|
|
||||||
ga.monthly_time_seconds,
|
|
||||||
CAST(COALESCE(d.words, 0.0) AS REAL) * ga.monthly_read_percentage
|
|
||||||
AS monthly_words_read,
|
|
||||||
COALESCE(
|
|
||||||
(CAST(COALESCE(d.words, 0.0) AS REAL) * ga.monthly_read_percentage)
|
|
||||||
/ (ga.monthly_time_seconds / 60), 0.0)
|
|
||||||
AS monthly_wpm,
|
|
||||||
|
|
||||||
-- Weekly WPM
|
|
||||||
ga.weekly_time_seconds,
|
|
||||||
CAST(COALESCE(d.words, 0.0) AS REAL) * ga.weekly_read_percentage
|
|
||||||
AS weekly_words_read,
|
|
||||||
COALESCE(
|
|
||||||
(CAST(COALESCE(d.words, 0.0) AS REAL) * ga.weekly_read_percentage)
|
|
||||||
/ (ga.weekly_time_seconds / 60), 0.0)
|
|
||||||
AS weekly_wpm
|
|
||||||
|
|
||||||
FROM grouped_activity AS ga
|
|
||||||
INNER JOIN
|
|
||||||
current_progress AS cp
|
|
||||||
ON ga.user_id = cp.user_id AND ga.document_id = cp.document_id
|
|
||||||
INNER JOIN
|
|
||||||
documents AS d
|
|
||||||
ON ga.document_id = d.id
|
|
||||||
GROUP BY ga.document_id, ga.user_id
|
|
||||||
ORDER BY total_wpm DESC;
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"reichard.io/antholume/pkg/ptr"
|
|
||||||
"reichard.io/antholume/pkg/sliceutils"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (d *DBManager) GetDocument(ctx context.Context, docID, userID string) (*GetDocumentsWithStatsRow, error) {
|
|
||||||
documents, err := d.Queries.GetDocumentsWithStats(ctx, GetDocumentsWithStatsParams{
|
|
||||||
ID: ptr.Of(docID),
|
|
||||||
UserID: userID,
|
|
||||||
Limit: 1,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
document, found := sliceutils.First(documents)
|
|
||||||
if !found {
|
|
||||||
return nil, fmt.Errorf("document not found: %s", docID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &document, nil
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
|
||||||
|
|
||||||
"reichard.io/antholume/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DocumentsTestSuite struct {
|
|
||||||
suite.Suite
|
|
||||||
dbm *DBManager
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDocuments(t *testing.T) {
|
|
||||||
suite.Run(t, new(DocumentsTestSuite))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *DocumentsTestSuite) SetupTest() {
|
|
||||||
cfg := config.Config{
|
|
||||||
DBType: "memory",
|
|
||||||
}
|
|
||||||
|
|
||||||
suite.dbm = NewMgr(&cfg)
|
|
||||||
|
|
||||||
// Create Document
|
|
||||||
_, err := suite.dbm.Queries.UpsertDocument(context.Background(), UpsertDocumentParams{
|
|
||||||
ID: documentID,
|
|
||||||
Title: &documentTitle,
|
|
||||||
Author: &documentAuthor,
|
|
||||||
Words: &documentWords,
|
|
||||||
})
|
|
||||||
suite.NoError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DOCUMENT - TODO:
|
|
||||||
// - (q *Queries) GetDocumentProgress
|
|
||||||
// - (q *Queries) GetDocumentWithStats
|
|
||||||
// - (q *Queries) GetDocumentsSize
|
|
||||||
// - (q *Queries) GetDocumentsWithStats
|
|
||||||
// - (q *Queries) GetMissingDocuments
|
|
||||||
func (suite *DocumentsTestSuite) TestGetDocument() {
|
|
||||||
doc, err := suite.dbm.Queries.GetDocument(context.Background(), documentID)
|
|
||||||
suite.Nil(err, "should have nil err")
|
|
||||||
suite.Equal(documentID, doc.ID, "should have changed the document")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *DocumentsTestSuite) TestUpsertDocument() {
|
|
||||||
testDocID := "docid1"
|
|
||||||
|
|
||||||
doc, err := suite.dbm.Queries.UpsertDocument(context.Background(), UpsertDocumentParams{
|
|
||||||
ID: testDocID,
|
|
||||||
Title: &documentTitle,
|
|
||||||
Author: &documentAuthor,
|
|
||||||
})
|
|
||||||
|
|
||||||
suite.Nil(err, "should have nil err")
|
|
||||||
suite.Equal(testDocID, doc.ID, "should have document id")
|
|
||||||
suite.Equal(documentTitle, *doc.Title, "should have document title")
|
|
||||||
suite.Equal(documentAuthor, *doc.Author, "should have document author")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *DocumentsTestSuite) TestDeleteDocument() {
|
|
||||||
changed, err := suite.dbm.Queries.DeleteDocument(context.Background(), documentID)
|
|
||||||
suite.Nil(err, "should have nil err")
|
|
||||||
suite.Equal(int64(1), changed, "should have changed the document")
|
|
||||||
|
|
||||||
doc, err := suite.dbm.Queries.GetDocument(context.Background(), documentID)
|
|
||||||
suite.Nil(err, "should have nil err")
|
|
||||||
suite.True(doc.Deleted, "should have deleted the document")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *DocumentsTestSuite) TestGetDeletedDocuments() {
|
|
||||||
changed, err := suite.dbm.Queries.DeleteDocument(context.Background(), documentID)
|
|
||||||
suite.Nil(err, "should have nil err")
|
|
||||||
suite.Equal(int64(1), changed, "should have changed the document")
|
|
||||||
|
|
||||||
deletedDocs, err := suite.dbm.Queries.GetDeletedDocuments(context.Background(), []string{documentID})
|
|
||||||
suite.Nil(err, "should have nil err")
|
|
||||||
suite.Len(deletedDocs, 1, "should have one deleted document")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO - Convert GetWantedDocuments -> (sqlc.slice('document_ids'));
|
|
||||||
func (suite *DocumentsTestSuite) TestGetWantedDocuments() {
|
|
||||||
wantedDocs, err := suite.dbm.Queries.GetWantedDocuments(context.Background(), fmt.Sprintf("[\"%s\"]", documentID))
|
|
||||||
suite.Nil(err, "should have nil err")
|
|
||||||
suite.Len(wantedDocs, 1, "should have one wanted document")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *DocumentsTestSuite) TestGetMissingDocuments() {
|
|
||||||
// Create Document
|
|
||||||
_, err := suite.dbm.Queries.UpsertDocument(context.Background(), UpsertDocumentParams{
|
|
||||||
ID: documentID,
|
|
||||||
Filepath: &documentFilepath,
|
|
||||||
})
|
|
||||||
suite.NoError(err)
|
|
||||||
|
|
||||||
missingDocs, err := suite.dbm.Queries.GetMissingDocuments(context.Background(), []string{documentID})
|
|
||||||
suite.Nil(err, "should have nil err")
|
|
||||||
suite.Len(missingDocs, 0, "should have no wanted document")
|
|
||||||
|
|
||||||
missingDocs, err = suite.dbm.Queries.GetMissingDocuments(context.Background(), []string{"other"})
|
|
||||||
suite.Nil(err, "should have nil err")
|
|
||||||
suite.Len(missingDocs, 1, "should have one missing document")
|
|
||||||
suite.Equal(documentID, missingDocs[0].ID, "should have missing doc")
|
|
||||||
|
|
||||||
// TODO - https://github.com/sqlc-dev/sqlc/issues/3451
|
|
||||||
// missingDocs, err = suite.dbm.Queries.GetMissingDocuments(context.Background(), []string{})
|
|
||||||
// suite.Nil(err, "should have nil err")
|
|
||||||
// suite.Len(missingDocs, 1, "should have one missing document")
|
|
||||||
// suite.Equal(documentID, missingDocs[0].ID, "should have missing doc")
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"database/sql/driver"
|
"database/sql/driver"
|
||||||
"embed"
|
"embed"
|
||||||
|
_ "embed"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -19,6 +20,7 @@ import (
|
|||||||
|
|
||||||
type DBManager struct {
|
type DBManager struct {
|
||||||
DB *sql.DB
|
DB *sql.DB
|
||||||
|
Ctx context.Context
|
||||||
Queries *Queries
|
Queries *Queries
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
}
|
}
|
||||||
@@ -26,11 +28,8 @@ type DBManager struct {
|
|||||||
//go:embed schema.sql
|
//go:embed schema.sql
|
||||||
var ddl string
|
var ddl string
|
||||||
|
|
||||||
//go:embed user_streaks.sql
|
//go:embed views.sql
|
||||||
var user_streaks string
|
var views string
|
||||||
|
|
||||||
//go:embed document_user_statistics.sql
|
|
||||||
var document_user_statistics string
|
|
||||||
|
|
||||||
//go:embed migrations/*
|
//go:embed migrations/*
|
||||||
var migrations embed.FS
|
var migrations embed.FS
|
||||||
@@ -42,27 +41,25 @@ func init() {
|
|||||||
Deterministic: true,
|
Deterministic: true,
|
||||||
Scalar: localTime,
|
Scalar: localTime,
|
||||||
})
|
})
|
||||||
sqlite.MustRegisterFunction("LOCAL_DATE", &sqlite.FunctionImpl{
|
|
||||||
NArgs: 2,
|
|
||||||
Deterministic: true,
|
|
||||||
Scalar: localDate,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMgr Returns an initialized manager
|
// Returns an initialized manager
|
||||||
func NewMgr(c *config.Config) *DBManager {
|
func NewMgr(c *config.Config) *DBManager {
|
||||||
// Create Manager
|
// Create Manager
|
||||||
dbm := &DBManager{cfg: c}
|
dbm := &DBManager{
|
||||||
|
Ctx: context.Background(),
|
||||||
|
cfg: c,
|
||||||
|
}
|
||||||
|
|
||||||
if err := dbm.init(context.Background()); err != nil {
|
if err := dbm.init(); err != nil {
|
||||||
log.Panic("Unable to init DB")
|
log.Panic("Unable to init DB")
|
||||||
}
|
}
|
||||||
|
|
||||||
return dbm
|
return dbm
|
||||||
}
|
}
|
||||||
|
|
||||||
// init loads the DB manager
|
// Init manager
|
||||||
func (dbm *DBManager) init(ctx context.Context) error {
|
func (dbm *DBManager) init() error {
|
||||||
// Build DB Location
|
// Build DB Location
|
||||||
var dbLocation string
|
var dbLocation string
|
||||||
switch dbm.cfg.DBType {
|
switch dbm.cfg.DBType {
|
||||||
@@ -107,23 +104,27 @@ func (dbm *DBManager) init(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Execute views
|
||||||
|
if _, err := dbm.DB.Exec(views, nil); err != nil {
|
||||||
|
log.Panicf("Error executing views: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Update settings
|
// Update settings
|
||||||
err = dbm.updateSettings(ctx)
|
err = dbm.updateSettings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panicf("Error running DB settings update: %v", err)
|
log.Panicf("Error running DB settings update: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache tables
|
// Cache tables
|
||||||
if err := dbm.CacheTempTables(ctx); err != nil {
|
go dbm.CacheTempTables()
|
||||||
log.Warn("Refreshing temp table cache failed: ", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload closes the DB & reinits
|
// Reload manager (close DB & reinit)
|
||||||
func (dbm *DBManager) Reload(ctx context.Context) error {
|
func (dbm *DBManager) Reload() error {
|
||||||
// Close handle
|
// Close handle
|
||||||
err := dbm.DB.Close()
|
err := dbm.DB.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -131,23 +132,30 @@ func (dbm *DBManager) Reload(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reinit DB
|
// Reinit DB
|
||||||
if err := dbm.init(ctx); err != nil {
|
if err := dbm.init(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CacheTempTables clears existing statistics and recalculates
|
func (dbm *DBManager) CacheTempTables() error {
|
||||||
func (dbm *DBManager) CacheTempTables(ctx context.Context) error {
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
if _, err := dbm.DB.ExecContext(ctx, user_streaks); err != nil {
|
user_streaks_sql := `
|
||||||
|
DELETE FROM user_streaks;
|
||||||
|
INSERT INTO user_streaks SELECT * FROM view_user_streaks;
|
||||||
|
`
|
||||||
|
if _, err := dbm.DB.ExecContext(dbm.Ctx, user_streaks_sql); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Debug("Cached 'user_streaks' in: ", time.Since(start))
|
log.Debug("Cached 'user_streaks' in: ", time.Since(start))
|
||||||
|
|
||||||
start = time.Now()
|
start = time.Now()
|
||||||
if _, err := dbm.DB.ExecContext(ctx, document_user_statistics); err != nil {
|
document_statistics_sql := `
|
||||||
|
DELETE FROM document_user_statistics;
|
||||||
|
INSERT INTO document_user_statistics SELECT * FROM view_document_user_statistics;
|
||||||
|
`
|
||||||
|
if _, err := dbm.DB.ExecContext(dbm.Ctx, document_statistics_sql); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Debug("Cached 'document_user_statistics' in: ", time.Since(start))
|
log.Debug("Cached 'document_user_statistics' in: ", time.Since(start))
|
||||||
@@ -155,9 +163,7 @@ func (dbm *DBManager) CacheTempTables(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateSettings ensures that we're enforcing foreign keys and enable journal
|
func (dbm *DBManager) updateSettings() error {
|
||||||
// mode.
|
|
||||||
func (dbm *DBManager) updateSettings(ctx context.Context) error {
|
|
||||||
// Set SQLite PRAGMA Settings
|
// Set SQLite PRAGMA Settings
|
||||||
pragmaQuery := `
|
pragmaQuery := `
|
||||||
PRAGMA foreign_keys = ON;
|
PRAGMA foreign_keys = ON;
|
||||||
@@ -169,7 +175,7 @@ func (dbm *DBManager) updateSettings(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update Antholume Version in DB
|
// Update Antholume Version in DB
|
||||||
if _, err := dbm.Queries.UpdateSettings(ctx, UpdateSettingsParams{
|
if _, err := dbm.Queries.UpdateSettings(dbm.Ctx, UpdateSettingsParams{
|
||||||
Name: "version",
|
Name: "version",
|
||||||
Value: dbm.cfg.Version,
|
Value: dbm.cfg.Version,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
@@ -180,10 +186,9 @@ func (dbm *DBManager) updateSettings(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// performMigrations runs all migrations
|
|
||||||
func (dbm *DBManager) performMigrations(isNew bool) error {
|
func (dbm *DBManager) performMigrations(isNew bool) error {
|
||||||
// Create context
|
// Create context
|
||||||
ctx := context.WithValue(context.Background(), "isNew", isNew) // nolint
|
ctx := context.WithValue(context.Background(), "isNew", isNew)
|
||||||
|
|
||||||
// Set DB migration
|
// Set DB migration
|
||||||
goose.SetBaseFS(migrations)
|
goose.SetBaseFS(migrations)
|
||||||
@@ -197,7 +202,7 @@ func (dbm *DBManager) performMigrations(isNew bool) error {
|
|||||||
return goose.UpContext(ctx, dbm.DB, "migrations")
|
return goose.UpContext(ctx, dbm.DB, "migrations")
|
||||||
}
|
}
|
||||||
|
|
||||||
// isEmpty determines whether the database is empty
|
// Determines whether the database is empty
|
||||||
func isEmpty(db *sql.DB) (bool, error) {
|
func isEmpty(db *sql.DB) (bool, error) {
|
||||||
var tableCount int
|
var tableCount int
|
||||||
err := db.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type='table';").Scan(&tableCount)
|
err := db.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type='table';").Scan(&tableCount)
|
||||||
@@ -207,7 +212,7 @@ func isEmpty(db *sql.DB) (bool, error) {
|
|||||||
return tableCount == 0, nil
|
return tableCount == 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// localTime is a custom SQL function that is registered as LOCAL_TIME in the init function
|
// LOCAL_TIME custom SQL function
|
||||||
func localTime(ctx *sqlite.FunctionContext, args []driver.Value) (driver.Value, error) {
|
func localTime(ctx *sqlite.FunctionContext, args []driver.Value) (driver.Value, error) {
|
||||||
timeStr, ok := args[0].(string)
|
timeStr, ok := args[0].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -229,30 +234,5 @@ func localTime(ctx *sqlite.FunctionContext, args []driver.Value) (driver.Value,
|
|||||||
return nil, errors.New("unable to parse time")
|
return nil, errors.New("unable to parse time")
|
||||||
}
|
}
|
||||||
|
|
||||||
return formattedTime.In(timeZone).Format(time.RFC3339), nil
|
return formattedTime.In(timeZone).Format("2006-01-02 15:04:05.000"), nil
|
||||||
}
|
|
||||||
|
|
||||||
// localDate is a custom SQL function that is registered as LOCAL_DATE in the init function
|
|
||||||
func localDate(ctx *sqlite.FunctionContext, args []driver.Value) (driver.Value, error) {
|
|
||||||
timeStr, ok := args[0].(string)
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("both arguments to TZTime must be strings")
|
|
||||||
}
|
|
||||||
|
|
||||||
timeZoneStr, ok := args[1].(string)
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("both arguments to TZTime must be strings")
|
|
||||||
}
|
|
||||||
|
|
||||||
timeZone, err := time.LoadLocation(timeZoneStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("unable to parse timezone")
|
|
||||||
}
|
|
||||||
|
|
||||||
formattedTime, err := time.ParseInLocation(time.RFC3339, timeStr, time.UTC)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("unable to parse time")
|
|
||||||
}
|
|
||||||
|
|
||||||
return formattedTime.In(timeZone).Format("2006-01-02"), nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,171 +1,168 @@
|
|||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"reichard.io/antholume/config"
|
"reichard.io/antholume/config"
|
||||||
"reichard.io/antholume/utils"
|
"reichard.io/antholume/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
type databaseTest struct {
|
||||||
userID string = "testUser"
|
*testing.T
|
||||||
userPass string = "testPass"
|
|
||||||
deviceID string = "testDevice"
|
|
||||||
deviceName string = "testDeviceName"
|
|
||||||
documentID string = "testDocument"
|
|
||||||
documentTitle string = "testTitle"
|
|
||||||
documentAuthor string = "testAuthor"
|
|
||||||
documentFilepath string = "./testPath.epub"
|
|
||||||
documentWords int64 = 5000
|
|
||||||
)
|
|
||||||
|
|
||||||
type DatabaseTestSuite struct {
|
|
||||||
suite.Suite
|
|
||||||
dbm *DBManager
|
dbm *DBManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDatabase(t *testing.T) {
|
var userID string = "testUser"
|
||||||
suite.Run(t, new(DatabaseTestSuite))
|
var userPass string = "testPass"
|
||||||
}
|
var deviceID string = "testDevice"
|
||||||
|
var deviceName string = "testDeviceName"
|
||||||
|
var documentID string = "testDocument"
|
||||||
|
var documentTitle string = "testTitle"
|
||||||
|
var documentAuthor string = "testAuthor"
|
||||||
|
|
||||||
// PROGRESS - TODO:
|
func TestNewMgr(t *testing.T) {
|
||||||
// - (q *Queries) GetProgress
|
|
||||||
// - (q *Queries) UpdateProgress
|
|
||||||
|
|
||||||
func (suite *DatabaseTestSuite) SetupTest() {
|
|
||||||
cfg := config.Config{
|
cfg := config.Config{
|
||||||
DBType: "memory",
|
DBType: "memory",
|
||||||
}
|
}
|
||||||
|
|
||||||
suite.dbm = NewMgr(&cfg)
|
dbm := NewMgr(&cfg)
|
||||||
|
assert.NotNil(t, dbm, "should not have nil dbm")
|
||||||
|
|
||||||
// Create User
|
t.Run("Database", func(t *testing.T) {
|
||||||
rawAuthHash, _ := utils.GenerateToken(64)
|
dt := databaseTest{t, dbm}
|
||||||
authHash := fmt.Sprintf("%x", rawAuthHash)
|
dt.TestUser()
|
||||||
_, err := suite.dbm.Queries.CreateUser(context.Background(), CreateUserParams{
|
dt.TestDocument()
|
||||||
ID: userID,
|
dt.TestDevice()
|
||||||
Pass: &userPass,
|
dt.TestActivity()
|
||||||
AuthHash: &authHash,
|
dt.TestDailyReadStats()
|
||||||
})
|
})
|
||||||
suite.NoError(err)
|
}
|
||||||
|
|
||||||
// Create Document
|
func (dt *databaseTest) TestUser() {
|
||||||
_, err = suite.dbm.Queries.UpsertDocument(context.Background(), UpsertDocumentParams{
|
dt.Run("User", func(t *testing.T) {
|
||||||
ID: documentID,
|
// Generate Auth Hash
|
||||||
Title: &documentTitle,
|
rawAuthHash, err := utils.GenerateToken(64)
|
||||||
Author: &documentAuthor,
|
assert.Nil(t, err, "should have nil err")
|
||||||
Filepath: &documentFilepath,
|
|
||||||
Words: &documentWords,
|
|
||||||
})
|
|
||||||
suite.NoError(err)
|
|
||||||
|
|
||||||
// Create Device
|
authHash := fmt.Sprintf("%x", rawAuthHash)
|
||||||
_, err = suite.dbm.Queries.UpsertDevice(context.Background(), UpsertDeviceParams{
|
changed, err := dt.dbm.Queries.CreateUser(dt.dbm.Ctx, CreateUserParams{
|
||||||
ID: deviceID,
|
ID: userID,
|
||||||
UserID: userID,
|
Pass: &userPass,
|
||||||
DeviceName: deviceName,
|
AuthHash: &authHash,
|
||||||
})
|
|
||||||
suite.NoError(err)
|
|
||||||
|
|
||||||
// Create Activity
|
|
||||||
end := time.Now()
|
|
||||||
start := end.AddDate(0, 0, -9)
|
|
||||||
var counter int64 = 0
|
|
||||||
|
|
||||||
for d := start; d.After(end) == false; d = d.AddDate(0, 0, 1) {
|
|
||||||
counter += 1
|
|
||||||
|
|
||||||
// Add Item
|
|
||||||
activity, err := suite.dbm.Queries.AddActivity(context.Background(), AddActivityParams{
|
|
||||||
DocumentID: documentID,
|
|
||||||
DeviceID: deviceID,
|
|
||||||
UserID: userID,
|
|
||||||
StartTime: d.UTC().Format(time.RFC3339),
|
|
||||||
Duration: 60,
|
|
||||||
StartPercentage: float64(counter) / 100.0,
|
|
||||||
EndPercentage: float64(counter+1) / 100.0,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
suite.Nil(err, fmt.Sprintf("[%d] should have nil err for add activity", counter))
|
assert.Nil(t, err, "should have nil err")
|
||||||
suite.Equal(counter, activity.ID, fmt.Sprintf("[%d] should have correct id for add activity", counter))
|
assert.Equal(t, int64(1), changed)
|
||||||
}
|
|
||||||
|
|
||||||
// Initiate Cache
|
user, err := dt.dbm.Queries.GetUser(dt.dbm.Ctx, userID)
|
||||||
err = suite.dbm.CacheTempTables(context.Background())
|
|
||||||
suite.NoError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DEVICES - TODO:
|
assert.Nil(t, err, "should have nil err")
|
||||||
// - (q *Queries) GetDevice
|
assert.Equal(t, userPass, *user.Pass)
|
||||||
// - (q *Queries) GetDevices
|
|
||||||
// - (q *Queries) UpsertDevice
|
|
||||||
func (suite *DatabaseTestSuite) TestDevice() {
|
|
||||||
testDevice := "dev123"
|
|
||||||
device, err := suite.dbm.Queries.UpsertDevice(context.Background(), UpsertDeviceParams{
|
|
||||||
ID: testDevice,
|
|
||||||
UserID: userID,
|
|
||||||
DeviceName: deviceName,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
suite.Nil(err, "should have nil err")
|
|
||||||
suite.Equal(testDevice, device.ID, "should have device id")
|
|
||||||
suite.Equal(userID, device.UserID, "should have user id")
|
|
||||||
suite.Equal(deviceName, device.DeviceName, "should have device name")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ACTIVITY - TODO:
|
func (dt *databaseTest) TestDocument() {
|
||||||
// - (q *Queries) AddActivity
|
dt.Run("Document", func(t *testing.T) {
|
||||||
// - (q *Queries) GetActivity
|
doc, err := dt.dbm.Queries.UpsertDocument(dt.dbm.Ctx, UpsertDocumentParams{
|
||||||
// - (q *Queries) GetLastActivity
|
ID: documentID,
|
||||||
func (suite *DatabaseTestSuite) TestActivity() {
|
Title: &documentTitle,
|
||||||
// Validate Exists
|
Author: &documentAuthor,
|
||||||
existsRows, err := suite.dbm.Queries.GetActivity(context.Background(), GetActivityParams{
|
})
|
||||||
UserID: userID,
|
|
||||||
Offset: 0,
|
assert.Nil(t, err, "should have nil err")
|
||||||
Limit: 50,
|
assert.Equal(t, documentID, doc.ID, "should have document id")
|
||||||
|
assert.Equal(t, documentTitle, *doc.Title, "should have document title")
|
||||||
|
assert.Equal(t, documentAuthor, *doc.Author, "should have document author")
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
suite.Nil(err, "should have nil err for get activity")
|
func (dt *databaseTest) TestDevice() {
|
||||||
suite.Len(existsRows, 10, "should have correct number of rows get activity")
|
dt.Run("Device", func(t *testing.T) {
|
||||||
|
device, err := dt.dbm.Queries.UpsertDevice(dt.dbm.Ctx, UpsertDeviceParams{
|
||||||
|
ID: deviceID,
|
||||||
|
UserID: userID,
|
||||||
|
DeviceName: deviceName,
|
||||||
|
})
|
||||||
|
|
||||||
// Validate Doesn't Exist
|
assert.Nil(t, err, "should have nil err")
|
||||||
doesntExistsRows, err := suite.dbm.Queries.GetActivity(context.Background(), GetActivityParams{
|
assert.Equal(t, deviceID, device.ID, "should have device id")
|
||||||
UserID: userID,
|
assert.Equal(t, userID, device.UserID, "should have user id")
|
||||||
DocumentID: "unknownDoc",
|
assert.Equal(t, deviceName, device.DeviceName, "should have device name")
|
||||||
DocFilter: true,
|
|
||||||
Offset: 0,
|
|
||||||
Limit: 50,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
suite.Nil(err, "should have nil err for get activity")
|
|
||||||
suite.Len(doesntExistsRows, 0, "should have no rows")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MISC - TODO:
|
func (dt *databaseTest) TestActivity() {
|
||||||
// - (q *Queries) AddMetadata
|
dt.Run("Progress", func(t *testing.T) {
|
||||||
// - (q *Queries) GetDailyReadStats
|
// 10 Activities, 10 Days
|
||||||
// - (q *Queries) GetDatabaseInfo
|
end := time.Now()
|
||||||
// - (q *Queries) UpdateSettings
|
start := end.AddDate(0, 0, -9)
|
||||||
func (suite *DatabaseTestSuite) TestGetDailyReadStats() {
|
var counter int64 = 0
|
||||||
readStats, err := suite.dbm.Queries.GetDailyReadStats(context.Background(), userID)
|
|
||||||
|
|
||||||
suite.Nil(err, "should have nil err")
|
for d := start; d.After(end) == false; d = d.AddDate(0, 0, 1) {
|
||||||
suite.Len(readStats, 30, "should have length of 30")
|
counter += 1
|
||||||
|
|
||||||
// Validate 1 Minute / Day - Last 10 Days
|
// Add Item
|
||||||
for i := 0; i < 10; i++ {
|
activity, err := dt.dbm.Queries.AddActivity(dt.dbm.Ctx, AddActivityParams{
|
||||||
stat := readStats[i]
|
DocumentID: documentID,
|
||||||
suite.Equal(int64(1), stat.MinutesRead, "should have one minute read")
|
DeviceID: deviceID,
|
||||||
}
|
UserID: userID,
|
||||||
|
StartTime: d.UTC().Format(time.RFC3339),
|
||||||
|
Duration: 60,
|
||||||
|
StartPercentage: float64(counter) / 100.0,
|
||||||
|
EndPercentage: float64(counter+1) / 100.0,
|
||||||
|
})
|
||||||
|
|
||||||
// Validate 0 Minute / Day - Remaining 20 Days
|
assert.Nil(t, err, fmt.Sprintf("[%d] should have nil err for add activity", counter))
|
||||||
for i := 10; i < 30; i++ {
|
assert.Equal(t, counter, activity.ID, fmt.Sprintf("[%d] should have correct id for add activity", counter))
|
||||||
stat := readStats[i]
|
}
|
||||||
suite.Equal(int64(0), stat.MinutesRead, "should have zero minutes read")
|
|
||||||
}
|
// Initiate Cache
|
||||||
|
dt.dbm.CacheTempTables()
|
||||||
|
|
||||||
|
// Validate Exists
|
||||||
|
existsRows, err := dt.dbm.Queries.GetActivity(dt.dbm.Ctx, GetActivityParams{
|
||||||
|
UserID: userID,
|
||||||
|
Offset: 0,
|
||||||
|
Limit: 50,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Nil(t, err, "should have nil err for get activity")
|
||||||
|
assert.Len(t, existsRows, 10, "should have correct number of rows get activity")
|
||||||
|
|
||||||
|
// Validate Doesn't Exist
|
||||||
|
doesntExistsRows, err := dt.dbm.Queries.GetActivity(dt.dbm.Ctx, GetActivityParams{
|
||||||
|
UserID: userID,
|
||||||
|
DocumentID: "unknownDoc",
|
||||||
|
DocFilter: true,
|
||||||
|
Offset: 0,
|
||||||
|
Limit: 50,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Nil(t, err, "should have nil err for get activity")
|
||||||
|
assert.Len(t, doesntExistsRows, 0, "should have no rows")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dt *databaseTest) TestDailyReadStats() {
|
||||||
|
dt.Run("DailyReadStats", func(t *testing.T) {
|
||||||
|
readStats, err := dt.dbm.Queries.GetDailyReadStats(dt.dbm.Ctx, userID)
|
||||||
|
|
||||||
|
assert.Nil(t, err, "should have nil err")
|
||||||
|
assert.Len(t, readStats, 30, "should have length of 30")
|
||||||
|
|
||||||
|
// Validate 1 Minute / Day - Last 10 Days
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
stat := readStats[i]
|
||||||
|
assert.Equal(t, int64(1), stat.MinutesRead, "should have one minute read")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate 0 Minute / Day - Remaining 20 Days
|
||||||
|
for i := 10; i < 30; i++ {
|
||||||
|
stat := readStats[i]
|
||||||
|
assert.Equal(t, int64(0), stat.MinutesRead, "should have zero minutes read")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// sqlc v1.29.0
|
// sqlc v1.25.0
|
||||||
|
|
||||||
package database
|
package database
|
||||||
|
|
||||||
|
import ()
|
||||||
|
|
||||||
type Activity struct {
|
type Activity struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
@@ -62,7 +64,6 @@ type DocumentUserStatistic struct {
|
|||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
Percentage float64 `json:"percentage"`
|
Percentage float64 `json:"percentage"`
|
||||||
LastRead string `json:"last_read"`
|
LastRead string `json:"last_read"`
|
||||||
LastSeen string `json:"last_seen"`
|
|
||||||
ReadPercentage float64 `json:"read_percentage"`
|
ReadPercentage float64 `json:"read_percentage"`
|
||||||
TotalTimeSeconds int64 `json:"total_time_seconds"`
|
TotalTimeSeconds int64 `json:"total_time_seconds"`
|
||||||
TotalWordsRead int64 `json:"total_words_read"`
|
TotalWordsRead int64 `json:"total_words_read"`
|
||||||
@@ -78,7 +79,7 @@ type DocumentUserStatistic struct {
|
|||||||
WeeklyWpm float64 `json:"weekly_wpm"`
|
WeeklyWpm float64 `json:"weekly_wpm"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Metadata struct {
|
type Metadatum struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
DocumentID string `json:"document_id"`
|
DocumentID string `json:"document_id"`
|
||||||
Title *string `json:"title"`
|
Title *string `json:"title"`
|
||||||
@@ -116,8 +117,4 @@ type UserStreak struct {
|
|||||||
CurrentStreak int64 `json:"current_streak"`
|
CurrentStreak int64 `json:"current_streak"`
|
||||||
CurrentStreakStartDate string `json:"current_streak_start_date"`
|
CurrentStreakStartDate string `json:"current_streak_start_date"`
|
||||||
CurrentStreakEndDate string `json:"current_streak_end_date"`
|
CurrentStreakEndDate string `json:"current_streak_end_date"`
|
||||||
LastTimezone string `json:"last_timezone"`
|
|
||||||
LastSeen string `json:"last_seen"`
|
|
||||||
LastRecord string `json:"last_record"`
|
|
||||||
LastCalculated string `json:"last_calculated"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,9 +30,6 @@ INSERT INTO users (id, pass, auth_hash, admin)
|
|||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?)
|
||||||
ON CONFLICT DO NOTHING;
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
-- name: DeleteUser :execrows
|
|
||||||
DELETE FROM users WHERE id = $id;
|
|
||||||
|
|
||||||
-- name: DeleteDocument :execrows
|
-- name: DeleteDocument :execrows
|
||||||
UPDATE documents
|
UPDATE documents
|
||||||
SET
|
SET
|
||||||
@@ -67,7 +64,7 @@ WITH filtered_activity AS (
|
|||||||
SELECT
|
SELECT
|
||||||
document_id,
|
document_id,
|
||||||
device_id,
|
device_id,
|
||||||
LOCAL_TIME(activity.start_time, users.timezone) AS start_time,
|
CAST(STRFTIME('%Y-%m-%d %H:%M:%S', LOCAL_TIME(activity.start_time, users.timezone)) AS TEXT) AS start_time,
|
||||||
title,
|
title,
|
||||||
author,
|
author,
|
||||||
duration,
|
duration,
|
||||||
@@ -80,7 +77,7 @@ LEFT JOIN users ON users.id = activity.user_id;
|
|||||||
|
|
||||||
-- name: GetDailyReadStats :many
|
-- name: GetDailyReadStats :many
|
||||||
WITH RECURSIVE last_30_days AS (
|
WITH RECURSIVE last_30_days AS (
|
||||||
SELECT LOCAL_DATE(STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'), timezone) AS date
|
SELECT DATE(LOCAL_TIME(STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'), timezone)) AS date
|
||||||
FROM users WHERE users.id = $user_id
|
FROM users WHERE users.id = $user_id
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT DATE(date, '-1 days')
|
SELECT DATE(date, '-1 days')
|
||||||
@@ -99,7 +96,7 @@ filtered_activity AS (
|
|||||||
activity_days AS (
|
activity_days AS (
|
||||||
SELECT
|
SELECT
|
||||||
SUM(duration) AS seconds_read,
|
SUM(duration) AS seconds_read,
|
||||||
LOCAL_DATE(start_time, timezone) AS day
|
DATE(LOCAL_TIME(start_time, timezone)) AS day
|
||||||
FROM filtered_activity AS activity
|
FROM filtered_activity AS activity
|
||||||
LEFT JOIN users ON users.id = activity.user_id
|
LEFT JOIN users ON users.id = activity.user_id
|
||||||
GROUP BY day
|
GROUP BY day
|
||||||
@@ -138,8 +135,8 @@ WHERE id = $device_id LIMIT 1;
|
|||||||
SELECT
|
SELECT
|
||||||
devices.id,
|
devices.id,
|
||||||
devices.device_name,
|
devices.device_name,
|
||||||
LOCAL_TIME(devices.created_at, users.timezone) AS created_at,
|
CAST(STRFTIME('%Y-%m-%d %H:%M:%S', LOCAL_TIME(devices.created_at, users.timezone)) AS TEXT) AS created_at,
|
||||||
LOCAL_TIME(devices.last_synced, users.timezone) AS last_synced
|
CAST(STRFTIME('%Y-%m-%d %H:%M:%S', LOCAL_TIME(devices.last_synced, users.timezone)) AS TEXT) AS last_synced
|
||||||
FROM devices
|
FROM devices
|
||||||
JOIN users ON users.id = devices.user_id
|
JOIN users ON users.id = devices.user_id
|
||||||
WHERE users.id = $user_id
|
WHERE users.id = $user_id
|
||||||
@@ -163,6 +160,42 @@ ORDER BY
|
|||||||
DESC
|
DESC
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- name: GetDocumentWithStats :one
|
||||||
|
SELECT
|
||||||
|
docs.id,
|
||||||
|
docs.title,
|
||||||
|
docs.author,
|
||||||
|
docs.description,
|
||||||
|
docs.isbn10,
|
||||||
|
docs.isbn13,
|
||||||
|
docs.filepath,
|
||||||
|
docs.words,
|
||||||
|
|
||||||
|
CAST(COALESCE(dus.total_wpm, 0.0) AS INTEGER) AS wpm,
|
||||||
|
COALESCE(dus.read_percentage, 0) AS read_percentage,
|
||||||
|
COALESCE(dus.total_time_seconds, 0) AS total_time_seconds,
|
||||||
|
STRFTIME('%Y-%m-%d %H:%M:%S', LOCAL_TIME(COALESCE(dus.last_read, STRFTIME('%Y-%m-%dT%H:%M:%SZ', 0, 'unixepoch')), users.timezone))
|
||||||
|
AS last_read,
|
||||||
|
ROUND(CAST(CASE
|
||||||
|
WHEN dus.percentage IS NULL THEN 0.0
|
||||||
|
WHEN (dus.percentage * 100.0) > 97.0 THEN 100.0
|
||||||
|
ELSE dus.percentage * 100.0
|
||||||
|
END AS REAL), 2) AS percentage,
|
||||||
|
CAST(CASE
|
||||||
|
WHEN dus.total_time_seconds IS NULL THEN 0.0
|
||||||
|
ELSE
|
||||||
|
CAST(dus.total_time_seconds AS REAL)
|
||||||
|
/ (dus.read_percentage * 100.0)
|
||||||
|
END AS INTEGER) AS seconds_per_percent
|
||||||
|
FROM documents AS docs
|
||||||
|
LEFT JOIN users ON users.id = $user_id
|
||||||
|
LEFT JOIN
|
||||||
|
document_user_statistics AS dus
|
||||||
|
ON dus.document_id = docs.id AND dus.user_id = $user_id
|
||||||
|
WHERE users.id = $user_id
|
||||||
|
AND docs.id = $document_id
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
-- name: GetDocuments :many
|
-- name: GetDocuments :many
|
||||||
SELECT * FROM documents
|
SELECT * FROM documents
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
@@ -200,25 +233,26 @@ SELECT
|
|||||||
WHEN (dus.percentage * 100.0) > 97.0 THEN 100.0
|
WHEN (dus.percentage * 100.0) > 97.0 THEN 100.0
|
||||||
ELSE dus.percentage * 100.0
|
ELSE dus.percentage * 100.0
|
||||||
END AS REAL), 2) AS percentage,
|
END AS REAL), 2) AS percentage,
|
||||||
CAST(CASE
|
|
||||||
|
CASE
|
||||||
WHEN dus.total_time_seconds IS NULL THEN 0.0
|
WHEN dus.total_time_seconds IS NULL THEN 0.0
|
||||||
ELSE
|
ELSE
|
||||||
CAST(dus.total_time_seconds AS REAL)
|
ROUND(
|
||||||
/ (dus.read_percentage * 100.0)
|
CAST(dus.total_time_seconds AS REAL)
|
||||||
END AS INTEGER) AS seconds_per_percent
|
/ (dus.read_percentage * 100.0)
|
||||||
|
)
|
||||||
|
END AS seconds_per_percent
|
||||||
FROM documents AS docs
|
FROM documents AS docs
|
||||||
LEFT JOIN users ON users.id = $user_id
|
LEFT JOIN users ON users.id = $user_id
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
document_user_statistics AS dus
|
document_user_statistics AS dus
|
||||||
ON dus.document_id = docs.id AND dus.user_id = $user_id
|
ON dus.document_id = docs.id AND dus.user_id = $user_id
|
||||||
WHERE
|
WHERE
|
||||||
(docs.id = sqlc.narg('id') OR $id IS NULL)
|
docs.deleted = false AND (
|
||||||
AND (docs.deleted = sqlc.narg(deleted) OR $deleted IS NULL)
|
$query IS NULL OR (
|
||||||
AND (
|
docs.title LIKE $query OR
|
||||||
(
|
|
||||||
docs.title LIKE sqlc.narg('query') OR
|
|
||||||
docs.author LIKE $query
|
docs.author LIKE $query
|
||||||
) OR $query IS NULL
|
)
|
||||||
)
|
)
|
||||||
ORDER BY dus.last_read DESC, docs.created_at DESC
|
ORDER BY dus.last_read DESC, docs.created_at DESC
|
||||||
LIMIT $limit
|
LIMIT $limit
|
||||||
@@ -246,7 +280,7 @@ SELECT
|
|||||||
ROUND(CAST(progress.percentage AS REAL) * 100, 2) AS percentage,
|
ROUND(CAST(progress.percentage AS REAL) * 100, 2) AS percentage,
|
||||||
progress.document_id,
|
progress.document_id,
|
||||||
progress.user_id,
|
progress.user_id,
|
||||||
LOCAL_TIME(progress.created_at, users.timezone) AS created_at
|
CAST(STRFTIME('%Y-%m-%d %H:%M:%S', LOCAL_TIME(progress.created_at, users.timezone)) AS TEXT) AS created_at
|
||||||
FROM document_progress AS progress
|
FROM document_progress AS progress
|
||||||
LEFT JOIN users ON progress.user_id = users.id
|
LEFT JOIN users ON progress.user_id = users.id
|
||||||
LEFT JOIN devices ON progress.device_id = devices.id
|
LEFT JOIN devices ON progress.device_id = devices.id
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// sqlc v1.29.0
|
// sqlc v1.25.0
|
||||||
// source: query.sql
|
// source: query.sql
|
||||||
|
|
||||||
package database
|
package database
|
||||||
@@ -85,7 +85,7 @@ type AddMetadataParams struct {
|
|||||||
Isbn13 *string `json:"isbn13"`
|
Isbn13 *string `json:"isbn13"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AddMetadata(ctx context.Context, arg AddMetadataParams) (Metadata, error) {
|
func (q *Queries) AddMetadata(ctx context.Context, arg AddMetadataParams) (Metadatum, error) {
|
||||||
row := q.db.QueryRowContext(ctx, addMetadata,
|
row := q.db.QueryRowContext(ctx, addMetadata,
|
||||||
arg.DocumentID,
|
arg.DocumentID,
|
||||||
arg.Title,
|
arg.Title,
|
||||||
@@ -96,7 +96,7 @@ func (q *Queries) AddMetadata(ctx context.Context, arg AddMetadataParams) (Metad
|
|||||||
arg.Isbn10,
|
arg.Isbn10,
|
||||||
arg.Isbn13,
|
arg.Isbn13,
|
||||||
)
|
)
|
||||||
var i Metadata
|
var i Metadatum
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.DocumentID,
|
&i.DocumentID,
|
||||||
@@ -153,18 +153,6 @@ func (q *Queries) DeleteDocument(ctx context.Context, id string) (int64, error)
|
|||||||
return result.RowsAffected()
|
return result.RowsAffected()
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteUser = `-- name: DeleteUser :execrows
|
|
||||||
DELETE FROM users WHERE id = ?1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) DeleteUser(ctx context.Context, id string) (int64, error) {
|
|
||||||
result, err := q.db.ExecContext(ctx, deleteUser, id)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return result.RowsAffected()
|
|
||||||
}
|
|
||||||
|
|
||||||
const getActivity = `-- name: GetActivity :many
|
const getActivity = `-- name: GetActivity :many
|
||||||
WITH filtered_activity AS (
|
WITH filtered_activity AS (
|
||||||
SELECT
|
SELECT
|
||||||
@@ -193,7 +181,7 @@ WITH filtered_activity AS (
|
|||||||
SELECT
|
SELECT
|
||||||
document_id,
|
document_id,
|
||||||
device_id,
|
device_id,
|
||||||
LOCAL_TIME(activity.start_time, users.timezone) AS start_time,
|
CAST(STRFTIME('%Y-%m-%d %H:%M:%S', LOCAL_TIME(activity.start_time, users.timezone)) AS TEXT) AS start_time,
|
||||||
title,
|
title,
|
||||||
author,
|
author,
|
||||||
duration,
|
duration,
|
||||||
@@ -214,15 +202,15 @@ type GetActivityParams struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GetActivityRow struct {
|
type GetActivityRow struct {
|
||||||
DocumentID string `json:"document_id"`
|
DocumentID string `json:"document_id"`
|
||||||
DeviceID string `json:"device_id"`
|
DeviceID string `json:"device_id"`
|
||||||
StartTime interface{} `json:"start_time"`
|
StartTime string `json:"start_time"`
|
||||||
Title *string `json:"title"`
|
Title *string `json:"title"`
|
||||||
Author *string `json:"author"`
|
Author *string `json:"author"`
|
||||||
Duration int64 `json:"duration"`
|
Duration int64 `json:"duration"`
|
||||||
StartPercentage float64 `json:"start_percentage"`
|
StartPercentage float64 `json:"start_percentage"`
|
||||||
EndPercentage float64 `json:"end_percentage"`
|
EndPercentage float64 `json:"end_percentage"`
|
||||||
ReadPercentage float64 `json:"read_percentage"`
|
ReadPercentage float64 `json:"read_percentage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetActivity(ctx context.Context, arg GetActivityParams) ([]GetActivityRow, error) {
|
func (q *Queries) GetActivity(ctx context.Context, arg GetActivityParams) ([]GetActivityRow, error) {
|
||||||
@@ -266,7 +254,7 @@ func (q *Queries) GetActivity(ctx context.Context, arg GetActivityParams) ([]Get
|
|||||||
|
|
||||||
const getDailyReadStats = `-- name: GetDailyReadStats :many
|
const getDailyReadStats = `-- name: GetDailyReadStats :many
|
||||||
WITH RECURSIVE last_30_days AS (
|
WITH RECURSIVE last_30_days AS (
|
||||||
SELECT LOCAL_DATE(STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'), timezone) AS date
|
SELECT DATE(LOCAL_TIME(STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'), timezone)) AS date
|
||||||
FROM users WHERE users.id = ?1
|
FROM users WHERE users.id = ?1
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT DATE(date, '-1 days')
|
SELECT DATE(date, '-1 days')
|
||||||
@@ -285,7 +273,7 @@ filtered_activity AS (
|
|||||||
activity_days AS (
|
activity_days AS (
|
||||||
SELECT
|
SELECT
|
||||||
SUM(duration) AS seconds_read,
|
SUM(duration) AS seconds_read,
|
||||||
LOCAL_DATE(start_time, timezone) AS day
|
DATE(LOCAL_TIME(start_time, timezone)) AS day
|
||||||
FROM filtered_activity AS activity
|
FROM filtered_activity AS activity
|
||||||
LEFT JOIN users ON users.id = activity.user_id
|
LEFT JOIN users ON users.id = activity.user_id
|
||||||
GROUP BY day
|
GROUP BY day
|
||||||
@@ -422,8 +410,8 @@ const getDevices = `-- name: GetDevices :many
|
|||||||
SELECT
|
SELECT
|
||||||
devices.id,
|
devices.id,
|
||||||
devices.device_name,
|
devices.device_name,
|
||||||
LOCAL_TIME(devices.created_at, users.timezone) AS created_at,
|
CAST(STRFTIME('%Y-%m-%d %H:%M:%S', LOCAL_TIME(devices.created_at, users.timezone)) AS TEXT) AS created_at,
|
||||||
LOCAL_TIME(devices.last_synced, users.timezone) AS last_synced
|
CAST(STRFTIME('%Y-%m-%d %H:%M:%S', LOCAL_TIME(devices.last_synced, users.timezone)) AS TEXT) AS last_synced
|
||||||
FROM devices
|
FROM devices
|
||||||
JOIN users ON users.id = devices.user_id
|
JOIN users ON users.id = devices.user_id
|
||||||
WHERE users.id = ?1
|
WHERE users.id = ?1
|
||||||
@@ -431,10 +419,10 @@ ORDER BY devices.last_synced DESC
|
|||||||
`
|
`
|
||||||
|
|
||||||
type GetDevicesRow struct {
|
type GetDevicesRow struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
DeviceName string `json:"device_name"`
|
DeviceName string `json:"device_name"`
|
||||||
CreatedAt interface{} `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
LastSynced interface{} `json:"last_synced"`
|
LastSynced string `json:"last_synced"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetDevices(ctx context.Context, userID string) ([]GetDevicesRow, error) {
|
func (q *Queries) GetDevices(ctx context.Context, userID string) ([]GetDevicesRow, error) {
|
||||||
@@ -543,6 +531,87 @@ func (q *Queries) GetDocumentProgress(ctx context.Context, arg GetDocumentProgre
|
|||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getDocumentWithStats = `-- name: GetDocumentWithStats :one
|
||||||
|
SELECT
|
||||||
|
docs.id,
|
||||||
|
docs.title,
|
||||||
|
docs.author,
|
||||||
|
docs.description,
|
||||||
|
docs.isbn10,
|
||||||
|
docs.isbn13,
|
||||||
|
docs.filepath,
|
||||||
|
docs.words,
|
||||||
|
|
||||||
|
CAST(COALESCE(dus.total_wpm, 0.0) AS INTEGER) AS wpm,
|
||||||
|
COALESCE(dus.read_percentage, 0) AS read_percentage,
|
||||||
|
COALESCE(dus.total_time_seconds, 0) AS total_time_seconds,
|
||||||
|
STRFTIME('%Y-%m-%d %H:%M:%S', LOCAL_TIME(COALESCE(dus.last_read, STRFTIME('%Y-%m-%dT%H:%M:%SZ', 0, 'unixepoch')), users.timezone))
|
||||||
|
AS last_read,
|
||||||
|
ROUND(CAST(CASE
|
||||||
|
WHEN dus.percentage IS NULL THEN 0.0
|
||||||
|
WHEN (dus.percentage * 100.0) > 97.0 THEN 100.0
|
||||||
|
ELSE dus.percentage * 100.0
|
||||||
|
END AS REAL), 2) AS percentage,
|
||||||
|
CAST(CASE
|
||||||
|
WHEN dus.total_time_seconds IS NULL THEN 0.0
|
||||||
|
ELSE
|
||||||
|
CAST(dus.total_time_seconds AS REAL)
|
||||||
|
/ (dus.read_percentage * 100.0)
|
||||||
|
END AS INTEGER) AS seconds_per_percent
|
||||||
|
FROM documents AS docs
|
||||||
|
LEFT JOIN users ON users.id = ?1
|
||||||
|
LEFT JOIN
|
||||||
|
document_user_statistics AS dus
|
||||||
|
ON dus.document_id = docs.id AND dus.user_id = ?1
|
||||||
|
WHERE users.id = ?1
|
||||||
|
AND docs.id = ?2
|
||||||
|
LIMIT 1
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetDocumentWithStatsParams struct {
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
DocumentID string `json:"document_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetDocumentWithStatsRow struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title *string `json:"title"`
|
||||||
|
Author *string `json:"author"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
Isbn10 *string `json:"isbn10"`
|
||||||
|
Isbn13 *string `json:"isbn13"`
|
||||||
|
Filepath *string `json:"filepath"`
|
||||||
|
Words *int64 `json:"words"`
|
||||||
|
Wpm int64 `json:"wpm"`
|
||||||
|
ReadPercentage float64 `json:"read_percentage"`
|
||||||
|
TotalTimeSeconds int64 `json:"total_time_seconds"`
|
||||||
|
LastRead interface{} `json:"last_read"`
|
||||||
|
Percentage float64 `json:"percentage"`
|
||||||
|
SecondsPerPercent int64 `json:"seconds_per_percent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetDocumentWithStats(ctx context.Context, arg GetDocumentWithStatsParams) (GetDocumentWithStatsRow, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getDocumentWithStats, arg.UserID, arg.DocumentID)
|
||||||
|
var i GetDocumentWithStatsRow
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Title,
|
||||||
|
&i.Author,
|
||||||
|
&i.Description,
|
||||||
|
&i.Isbn10,
|
||||||
|
&i.Isbn13,
|
||||||
|
&i.Filepath,
|
||||||
|
&i.Words,
|
||||||
|
&i.Wpm,
|
||||||
|
&i.ReadPercentage,
|
||||||
|
&i.TotalTimeSeconds,
|
||||||
|
&i.LastRead,
|
||||||
|
&i.Percentage,
|
||||||
|
&i.SecondsPerPercent,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const getDocuments = `-- name: GetDocuments :many
|
const getDocuments = `-- name: GetDocuments :many
|
||||||
SELECT id, md5, basepath, filepath, coverfile, title, author, series, series_index, lang, description, words, gbid, olid, isbn10, isbn13, synced, deleted, updated_at, created_at FROM documents
|
SELECT id, md5, basepath, filepath, coverfile, title, author, series, series_index, lang, description, words, gbid, olid, isbn10, isbn13, synced, deleted, updated_at, created_at FROM documents
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
@@ -638,38 +707,37 @@ SELECT
|
|||||||
WHEN (dus.percentage * 100.0) > 97.0 THEN 100.0
|
WHEN (dus.percentage * 100.0) > 97.0 THEN 100.0
|
||||||
ELSE dus.percentage * 100.0
|
ELSE dus.percentage * 100.0
|
||||||
END AS REAL), 2) AS percentage,
|
END AS REAL), 2) AS percentage,
|
||||||
CAST(CASE
|
|
||||||
|
CASE
|
||||||
WHEN dus.total_time_seconds IS NULL THEN 0.0
|
WHEN dus.total_time_seconds IS NULL THEN 0.0
|
||||||
ELSE
|
ELSE
|
||||||
CAST(dus.total_time_seconds AS REAL)
|
ROUND(
|
||||||
/ (dus.read_percentage * 100.0)
|
CAST(dus.total_time_seconds AS REAL)
|
||||||
END AS INTEGER) AS seconds_per_percent
|
/ (dus.read_percentage * 100.0)
|
||||||
|
)
|
||||||
|
END AS seconds_per_percent
|
||||||
FROM documents AS docs
|
FROM documents AS docs
|
||||||
LEFT JOIN users ON users.id = ?1
|
LEFT JOIN users ON users.id = ?1
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
document_user_statistics AS dus
|
document_user_statistics AS dus
|
||||||
ON dus.document_id = docs.id AND dus.user_id = ?1
|
ON dus.document_id = docs.id AND dus.user_id = ?1
|
||||||
WHERE
|
WHERE
|
||||||
(docs.id = ?2 OR ?2 IS NULL)
|
docs.deleted = false AND (
|
||||||
AND (docs.deleted = ?3 OR ?3 IS NULL)
|
?2 IS NULL OR (
|
||||||
AND (
|
docs.title LIKE ?2 OR
|
||||||
(
|
docs.author LIKE ?2
|
||||||
docs.title LIKE ?4 OR
|
)
|
||||||
docs.author LIKE ?4
|
|
||||||
) OR ?4 IS NULL
|
|
||||||
)
|
)
|
||||||
ORDER BY dus.last_read DESC, docs.created_at DESC
|
ORDER BY dus.last_read DESC, docs.created_at DESC
|
||||||
LIMIT ?6
|
LIMIT ?4
|
||||||
OFFSET ?5
|
OFFSET ?3
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetDocumentsWithStatsParams struct {
|
type GetDocumentsWithStatsParams struct {
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
ID *string `json:"id"`
|
Query interface{} `json:"query"`
|
||||||
Deleted *bool `json:"-"`
|
Offset int64 `json:"offset"`
|
||||||
Query *string `json:"query"`
|
Limit int64 `json:"limit"`
|
||||||
Offset int64 `json:"offset"`
|
|
||||||
Limit int64 `json:"limit"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetDocumentsWithStatsRow struct {
|
type GetDocumentsWithStatsRow struct {
|
||||||
@@ -686,14 +754,12 @@ type GetDocumentsWithStatsRow struct {
|
|||||||
TotalTimeSeconds int64 `json:"total_time_seconds"`
|
TotalTimeSeconds int64 `json:"total_time_seconds"`
|
||||||
LastRead interface{} `json:"last_read"`
|
LastRead interface{} `json:"last_read"`
|
||||||
Percentage float64 `json:"percentage"`
|
Percentage float64 `json:"percentage"`
|
||||||
SecondsPerPercent int64 `json:"seconds_per_percent"`
|
SecondsPerPercent interface{} `json:"seconds_per_percent"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetDocumentsWithStats(ctx context.Context, arg GetDocumentsWithStatsParams) ([]GetDocumentsWithStatsRow, error) {
|
func (q *Queries) GetDocumentsWithStats(ctx context.Context, arg GetDocumentsWithStatsParams) ([]GetDocumentsWithStatsRow, error) {
|
||||||
rows, err := q.db.QueryContext(ctx, getDocumentsWithStats,
|
rows, err := q.db.QueryContext(ctx, getDocumentsWithStats,
|
||||||
arg.UserID,
|
arg.UserID,
|
||||||
arg.ID,
|
|
||||||
arg.Deleted,
|
|
||||||
arg.Query,
|
arg.Query,
|
||||||
arg.Offset,
|
arg.Offset,
|
||||||
arg.Limit,
|
arg.Limit,
|
||||||
@@ -824,7 +890,7 @@ SELECT
|
|||||||
ROUND(CAST(progress.percentage AS REAL) * 100, 2) AS percentage,
|
ROUND(CAST(progress.percentage AS REAL) * 100, 2) AS percentage,
|
||||||
progress.document_id,
|
progress.document_id,
|
||||||
progress.user_id,
|
progress.user_id,
|
||||||
LOCAL_TIME(progress.created_at, users.timezone) AS created_at
|
CAST(STRFTIME('%Y-%m-%d %H:%M:%S', LOCAL_TIME(progress.created_at, users.timezone)) AS TEXT) AS created_at
|
||||||
FROM document_progress AS progress
|
FROM document_progress AS progress
|
||||||
LEFT JOIN users ON progress.user_id = users.id
|
LEFT JOIN users ON progress.user_id = users.id
|
||||||
LEFT JOIN devices ON progress.device_id = devices.id
|
LEFT JOIN devices ON progress.device_id = devices.id
|
||||||
@@ -851,13 +917,13 @@ type GetProgressParams struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GetProgressRow struct {
|
type GetProgressRow struct {
|
||||||
Title *string `json:"title"`
|
Title *string `json:"title"`
|
||||||
Author *string `json:"author"`
|
Author *string `json:"author"`
|
||||||
DeviceName string `json:"device_name"`
|
DeviceName string `json:"device_name"`
|
||||||
Percentage float64 `json:"percentage"`
|
Percentage float64 `json:"percentage"`
|
||||||
DocumentID string `json:"document_id"`
|
DocumentID string `json:"document_id"`
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
CreatedAt interface{} `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetProgress(ctx context.Context, arg GetProgressParams) ([]GetProgressRow, error) {
|
func (q *Queries) GetProgress(ctx context.Context, arg GetProgressParams) ([]GetProgressRow, error) {
|
||||||
@@ -1000,7 +1066,7 @@ func (q *Queries) GetUserStatistics(ctx context.Context) ([]GetUserStatisticsRow
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getUserStreaks = `-- name: GetUserStreaks :many
|
const getUserStreaks = `-- name: GetUserStreaks :many
|
||||||
SELECT user_id, "window", max_streak, max_streak_start_date, max_streak_end_date, current_streak, current_streak_start_date, current_streak_end_date, last_timezone, last_seen, last_record, last_calculated FROM user_streaks
|
SELECT user_id, "window", max_streak, max_streak_start_date, max_streak_end_date, current_streak, current_streak_start_date, current_streak_end_date FROM user_streaks
|
||||||
WHERE user_id = ?1
|
WHERE user_id = ?1
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -1022,10 +1088,6 @@ func (q *Queries) GetUserStreaks(ctx context.Context, userID string) ([]UserStre
|
|||||||
&i.CurrentStreak,
|
&i.CurrentStreak,
|
||||||
&i.CurrentStreakStartDate,
|
&i.CurrentStreakStartDate,
|
||||||
&i.CurrentStreakEndDate,
|
&i.CurrentStreakEndDate,
|
||||||
&i.LastTimezone,
|
|
||||||
&i.LastSeen,
|
|
||||||
&i.LastRecord,
|
|
||||||
&i.LastCalculated,
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,13 +118,30 @@ CREATE TABLE IF NOT EXISTS settings (
|
|||||||
created_at DATETIME NOT NULL DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
created_at DATETIME NOT NULL DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Document User Statistics Table
|
---------------------------------------------------------------
|
||||||
CREATE TABLE IF NOT EXISTS document_user_statistics (
|
----------------------- Temporary Tables ----------------------
|
||||||
|
---------------------------------------------------------------
|
||||||
|
|
||||||
|
-- Temporary User Streaks Table (Cached from View)
|
||||||
|
CREATE TEMPORARY TABLE IF NOT EXISTS user_streaks (
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
window TEXT NOT NULL,
|
||||||
|
|
||||||
|
max_streak INTEGER NOT NULL,
|
||||||
|
max_streak_start_date TEXT NOT NULL,
|
||||||
|
max_streak_end_date TEXT NOT NULL,
|
||||||
|
|
||||||
|
current_streak INTEGER NOT NULL,
|
||||||
|
current_streak_start_date TEXT NOT NULL,
|
||||||
|
current_streak_end_date TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Temporary Document User Statistics Table (Cached from View)
|
||||||
|
CREATE TEMPORARY TABLE IF NOT EXISTS document_user_statistics (
|
||||||
document_id TEXT NOT NULL,
|
document_id TEXT NOT NULL,
|
||||||
user_id TEXT NOT NULL,
|
user_id TEXT NOT NULL,
|
||||||
percentage REAL NOT NULL,
|
percentage REAL NOT NULL,
|
||||||
last_read DATETIME NOT NULL,
|
last_read TEXT NOT NULL,
|
||||||
last_seen DATETIME NOT NULL,
|
|
||||||
read_percentage REAL NOT NULL,
|
read_percentage REAL NOT NULL,
|
||||||
|
|
||||||
total_time_seconds INTEGER NOT NULL,
|
total_time_seconds INTEGER NOT NULL,
|
||||||
@@ -146,39 +163,21 @@ CREATE TABLE IF NOT EXISTS document_user_statistics (
|
|||||||
UNIQUE(document_id, user_id) ON CONFLICT REPLACE
|
UNIQUE(document_id, user_id) ON CONFLICT REPLACE
|
||||||
);
|
);
|
||||||
|
|
||||||
-- User Streaks Table
|
|
||||||
CREATE TABLE IF NOT EXISTS user_streaks (
|
|
||||||
user_id TEXT NOT NULL,
|
|
||||||
window TEXT NOT NULL,
|
|
||||||
|
|
||||||
max_streak INTEGER NOT NULL,
|
|
||||||
max_streak_start_date TEXT NOT NULL,
|
|
||||||
max_streak_end_date TEXT NOT NULL,
|
|
||||||
|
|
||||||
current_streak INTEGER NOT NULL,
|
|
||||||
current_streak_start_date TEXT NOT NULL,
|
|
||||||
current_streak_end_date TEXT NOT NULL,
|
|
||||||
|
|
||||||
last_timezone TEXT NOT NULL,
|
|
||||||
last_seen TEXT NOT NULL,
|
|
||||||
last_record TEXT NOT NULL,
|
|
||||||
last_calculated TEXT NOT NULL,
|
|
||||||
|
|
||||||
UNIQUE(user_id, window) ON CONFLICT REPLACE
|
|
||||||
);
|
|
||||||
|
|
||||||
---------------------------------------------------------------
|
---------------------------------------------------------------
|
||||||
--------------------------- Indexes ---------------------------
|
--------------------------- Indexes ---------------------------
|
||||||
---------------------------------------------------------------
|
---------------------------------------------------------------
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS activity_start_time ON activity (start_time);
|
CREATE INDEX IF NOT EXISTS activity_start_time ON activity (start_time);
|
||||||
CREATE INDEX IF NOT EXISTS activity_created_at ON activity (created_at);
|
|
||||||
CREATE INDEX IF NOT EXISTS activity_user_id ON activity (user_id);
|
CREATE INDEX IF NOT EXISTS activity_user_id ON activity (user_id);
|
||||||
CREATE INDEX IF NOT EXISTS activity_user_id_document_id ON activity (
|
CREATE INDEX IF NOT EXISTS activity_user_id_document_id ON activity (
|
||||||
user_id,
|
user_id,
|
||||||
document_id
|
document_id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
DROP VIEW IF EXISTS view_user_streaks;
|
||||||
|
DROP VIEW IF EXISTS view_document_user_statistics;
|
||||||
|
|
||||||
---------------------------------------------------------------
|
---------------------------------------------------------------
|
||||||
--------------------------- Triggers --------------------------
|
--------------------------- Triggers --------------------------
|
||||||
---------------------------------------------------------------
|
---------------------------------------------------------------
|
||||||
@@ -190,11 +189,3 @@ UPDATE documents
|
|||||||
SET updated_at = STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now')
|
SET updated_at = STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now')
|
||||||
WHERE id = old.id;
|
WHERE id = old.id;
|
||||||
END;
|
END;
|
||||||
|
|
||||||
-- Delete User
|
|
||||||
CREATE TRIGGER IF NOT EXISTS user_deleted
|
|
||||||
BEFORE DELETE ON users BEGIN
|
|
||||||
DELETE FROM activity WHERE activity.user_id=OLD.id;
|
|
||||||
DELETE FROM devices WHERE devices.user_id=OLD.id;
|
|
||||||
DELETE FROM document_progress WHERE document_progress.user_id=OLD.id;
|
|
||||||
END;
|
|
||||||
|
|||||||
@@ -1,154 +0,0 @@
|
|||||||
WITH updated_users AS (
|
|
||||||
SELECT a.user_id
|
|
||||||
FROM activity AS a
|
|
||||||
LEFT JOIN users AS u ON u.id = a.user_id
|
|
||||||
LEFT JOIN user_streaks AS s ON a.user_id = s.user_id AND s.window = 'DAY'
|
|
||||||
WHERE
|
|
||||||
a.created_at > COALESCE(s.last_seen, '1970-01-01')
|
|
||||||
AND LOCAL_DATE(s.last_record, u.timezone) != LOCAL_DATE(a.start_time, u.timezone)
|
|
||||||
GROUP BY a.user_id
|
|
||||||
),
|
|
||||||
|
|
||||||
outdated_users AS (
|
|
||||||
SELECT
|
|
||||||
a.user_id,
|
|
||||||
u.timezone AS last_timezone,
|
|
||||||
MAX(a.created_at) AS last_seen,
|
|
||||||
MAX(a.start_time) AS last_record,
|
|
||||||
STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now') AS last_calculated
|
|
||||||
FROM activity AS a
|
|
||||||
LEFT JOIN users AS u ON u.id = a.user_id
|
|
||||||
LEFT JOIN user_streaks AS s ON a.user_id = s.user_id AND s.window = 'DAY'
|
|
||||||
GROUP BY a.user_id
|
|
||||||
HAVING
|
|
||||||
-- User Changed Timezones
|
|
||||||
s.last_timezone != u.timezone
|
|
||||||
|
|
||||||
-- Users Date Changed
|
|
||||||
OR LOCAL_DATE(COALESCE(s.last_calculated, '1970-01-01T00:00:00Z'), u.timezone) !=
|
|
||||||
LOCAL_DATE(STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'), u.timezone)
|
|
||||||
|
|
||||||
-- User Added New Data
|
|
||||||
OR a.user_id IN updated_users
|
|
||||||
),
|
|
||||||
|
|
||||||
document_windows AS (
|
|
||||||
SELECT
|
|
||||||
activity.user_id,
|
|
||||||
users.timezone,
|
|
||||||
DATE(
|
|
||||||
LOCAL_DATE(activity.start_time, users.timezone),
|
|
||||||
'weekday 0', '-7 day'
|
|
||||||
) AS weekly_read,
|
|
||||||
LOCAL_DATE(activity.start_time, users.timezone) AS daily_read
|
|
||||||
FROM activity
|
|
||||||
INNER JOIN outdated_users ON outdated_users.user_id = activity.user_id
|
|
||||||
LEFT JOIN users ON users.id = activity.user_id
|
|
||||||
GROUP BY activity.user_id, weekly_read, daily_read
|
|
||||||
),
|
|
||||||
|
|
||||||
weekly_partitions AS (
|
|
||||||
SELECT
|
|
||||||
user_id,
|
|
||||||
timezone,
|
|
||||||
'WEEK' AS "window",
|
|
||||||
weekly_read AS read_window,
|
|
||||||
ROW_NUMBER() OVER (
|
|
||||||
PARTITION BY user_id ORDER BY weekly_read DESC
|
|
||||||
) AS seqnum
|
|
||||||
FROM document_windows
|
|
||||||
GROUP BY user_id, weekly_read
|
|
||||||
),
|
|
||||||
|
|
||||||
daily_partitions AS (
|
|
||||||
SELECT
|
|
||||||
user_id,
|
|
||||||
timezone,
|
|
||||||
'DAY' AS "window",
|
|
||||||
daily_read AS read_window,
|
|
||||||
ROW_NUMBER() OVER (
|
|
||||||
PARTITION BY user_id ORDER BY daily_read DESC
|
|
||||||
) AS seqnum
|
|
||||||
FROM document_windows
|
|
||||||
GROUP BY user_id, daily_read
|
|
||||||
),
|
|
||||||
|
|
||||||
streaks AS (
|
|
||||||
SELECT
|
|
||||||
COUNT(*) AS streak,
|
|
||||||
MIN(read_window) AS start_date,
|
|
||||||
MAX(read_window) AS end_date,
|
|
||||||
window,
|
|
||||||
user_id,
|
|
||||||
timezone
|
|
||||||
FROM daily_partitions
|
|
||||||
GROUP BY
|
|
||||||
timezone,
|
|
||||||
user_id,
|
|
||||||
DATE(read_window, '+' || seqnum || ' day')
|
|
||||||
|
|
||||||
UNION ALL
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
COUNT(*) AS streak,
|
|
||||||
MIN(read_window) AS start_date,
|
|
||||||
MAX(read_window) AS end_date,
|
|
||||||
window,
|
|
||||||
user_id,
|
|
||||||
timezone
|
|
||||||
FROM weekly_partitions
|
|
||||||
GROUP BY
|
|
||||||
timezone,
|
|
||||||
user_id,
|
|
||||||
DATE(read_window, '+' || (seqnum * 7) || ' day')
|
|
||||||
),
|
|
||||||
|
|
||||||
max_streak AS (
|
|
||||||
SELECT
|
|
||||||
MAX(streak) AS max_streak,
|
|
||||||
start_date AS max_streak_start_date,
|
|
||||||
end_date AS max_streak_end_date,
|
|
||||||
window,
|
|
||||||
user_id
|
|
||||||
FROM streaks
|
|
||||||
GROUP BY user_id, window
|
|
||||||
),
|
|
||||||
|
|
||||||
current_streak AS (
|
|
||||||
SELECT
|
|
||||||
streak AS current_streak,
|
|
||||||
start_date AS current_streak_start_date,
|
|
||||||
end_date AS current_streak_end_date,
|
|
||||||
window,
|
|
||||||
user_id
|
|
||||||
FROM streaks
|
|
||||||
WHERE CASE
|
|
||||||
WHEN window = "WEEK" THEN
|
|
||||||
DATE(LOCAL_DATE(STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'), timezone), 'weekday 0', '-14 day') = current_streak_end_date
|
|
||||||
OR DATE(LOCAL_DATE(STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'), timezone), 'weekday 0', '-7 day') = current_streak_end_date
|
|
||||||
WHEN window = "DAY" THEN
|
|
||||||
DATE(LOCAL_DATE(STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'), timezone), '-1 day') = current_streak_end_date
|
|
||||||
OR DATE(LOCAL_DATE(STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'), timezone)) = current_streak_end_date
|
|
||||||
END
|
|
||||||
GROUP BY user_id, window
|
|
||||||
)
|
|
||||||
|
|
||||||
INSERT INTO user_streaks
|
|
||||||
SELECT
|
|
||||||
max_streak.user_id,
|
|
||||||
max_streak.window,
|
|
||||||
IFNULL(max_streak, 0) AS max_streak,
|
|
||||||
IFNULL(max_streak_start_date, "N/A") AS max_streak_start_date,
|
|
||||||
IFNULL(max_streak_end_date, "N/A") AS max_streak_end_date,
|
|
||||||
IFNULL(current_streak.current_streak, 0) AS current_streak,
|
|
||||||
IFNULL(current_streak.current_streak_start_date, "N/A") AS current_streak_start_date,
|
|
||||||
IFNULL(current_streak.current_streak_end_date, "N/A") AS current_streak_end_date,
|
|
||||||
outdated_users.last_timezone AS last_timezone,
|
|
||||||
outdated_users.last_seen AS last_seen,
|
|
||||||
outdated_users.last_record AS last_record,
|
|
||||||
outdated_users.last_calculated AS last_calculated
|
|
||||||
FROM max_streak
|
|
||||||
JOIN outdated_users ON max_streak.user_id = outdated_users.user_id
|
|
||||||
LEFT JOIN current_streak ON
|
|
||||||
current_streak.user_id = max_streak.user_id
|
|
||||||
AND current_streak.window = max_streak.window;
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
|
||||||
|
|
||||||
"reichard.io/antholume/config"
|
|
||||||
"reichard.io/antholume/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
testUserID string = "testUser"
|
|
||||||
testUserPass string = "testPass"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UsersTestSuite struct {
|
|
||||||
suite.Suite
|
|
||||||
dbm *DBManager
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUsers(t *testing.T) {
|
|
||||||
suite.Run(t, new(UsersTestSuite))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *UsersTestSuite) SetupTest() {
|
|
||||||
cfg := config.Config{
|
|
||||||
DBType: "memory",
|
|
||||||
}
|
|
||||||
|
|
||||||
suite.dbm = NewMgr(&cfg)
|
|
||||||
|
|
||||||
// Create User
|
|
||||||
rawAuthHash, _ := utils.GenerateToken(64)
|
|
||||||
authHash := fmt.Sprintf("%x", rawAuthHash)
|
|
||||||
_, err := suite.dbm.Queries.CreateUser(context.Background(), CreateUserParams{
|
|
||||||
ID: testUserID,
|
|
||||||
Pass: &testUserPass,
|
|
||||||
AuthHash: &authHash,
|
|
||||||
})
|
|
||||||
suite.NoError(err)
|
|
||||||
|
|
||||||
// Create Document
|
|
||||||
_, err = suite.dbm.Queries.UpsertDocument(context.Background(), UpsertDocumentParams{
|
|
||||||
ID: documentID,
|
|
||||||
Title: &documentTitle,
|
|
||||||
Author: &documentAuthor,
|
|
||||||
Words: &documentWords,
|
|
||||||
})
|
|
||||||
suite.NoError(err)
|
|
||||||
|
|
||||||
// Create Device
|
|
||||||
_, err = suite.dbm.Queries.UpsertDevice(context.Background(), UpsertDeviceParams{
|
|
||||||
ID: deviceID,
|
|
||||||
UserID: testUserID,
|
|
||||||
DeviceName: deviceName,
|
|
||||||
})
|
|
||||||
suite.NoError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *UsersTestSuite) TestGetUser() {
|
|
||||||
user, err := suite.dbm.Queries.GetUser(context.Background(), testUserID)
|
|
||||||
suite.Nil(err, "should have nil err")
|
|
||||||
suite.Equal(testUserPass, *user.Pass)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *UsersTestSuite) TestCreateUser() {
|
|
||||||
testUser := "user1"
|
|
||||||
testPass := "pass1"
|
|
||||||
|
|
||||||
// Generate Auth Hash
|
|
||||||
rawAuthHash, err := utils.GenerateToken(64)
|
|
||||||
suite.Nil(err, "should have nil err")
|
|
||||||
|
|
||||||
authHash := fmt.Sprintf("%x", rawAuthHash)
|
|
||||||
changed, err := suite.dbm.Queries.CreateUser(context.Background(), CreateUserParams{
|
|
||||||
ID: testUser,
|
|
||||||
Pass: &testPass,
|
|
||||||
AuthHash: &authHash,
|
|
||||||
})
|
|
||||||
|
|
||||||
suite.Nil(err, "should have nil err")
|
|
||||||
suite.Equal(int64(1), changed)
|
|
||||||
|
|
||||||
user, err := suite.dbm.Queries.GetUser(context.Background(), testUser)
|
|
||||||
suite.Nil(err, "should have nil err")
|
|
||||||
suite.Equal(testPass, *user.Pass)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *UsersTestSuite) TestDeleteUser() {
|
|
||||||
changed, err := suite.dbm.Queries.DeleteUser(context.Background(), testUserID)
|
|
||||||
suite.Nil(err, "should have nil err")
|
|
||||||
suite.Equal(int64(1), changed, "should have one changed row")
|
|
||||||
|
|
||||||
_, err = suite.dbm.Queries.GetUser(context.Background(), testUserID)
|
|
||||||
suite.ErrorIs(err, sql.ErrNoRows, "should have no rows error")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *UsersTestSuite) TestGetUsers() {
|
|
||||||
users, err := suite.dbm.Queries.GetUsers(context.Background())
|
|
||||||
suite.Nil(err, "should have nil err")
|
|
||||||
suite.Len(users, 1, "should have single user")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *UsersTestSuite) TestUpdateUser() {
|
|
||||||
newPassword := "newPass123"
|
|
||||||
user, err := suite.dbm.Queries.UpdateUser(context.Background(), UpdateUserParams{
|
|
||||||
UserID: testUserID,
|
|
||||||
Password: &newPassword,
|
|
||||||
})
|
|
||||||
suite.Nil(err, "should have nil err")
|
|
||||||
suite.Equal(newPassword, *user.Pass, "should have new password")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *UsersTestSuite) TestGetUserStatistics() {
|
|
||||||
err := suite.dbm.CacheTempTables(context.Background())
|
|
||||||
suite.NoError(err)
|
|
||||||
|
|
||||||
// Ensure Zero Items
|
|
||||||
userStats, err := suite.dbm.Queries.GetUserStatistics(context.Background())
|
|
||||||
suite.Nil(err, "should have nil err")
|
|
||||||
suite.Empty(userStats, "should be empty")
|
|
||||||
|
|
||||||
// Create Activity
|
|
||||||
end := time.Now()
|
|
||||||
start := end.AddDate(0, 0, -9)
|
|
||||||
var counter int64 = 0
|
|
||||||
|
|
||||||
for d := start; d.After(end) == false; d = d.AddDate(0, 0, 1) {
|
|
||||||
counter += 1
|
|
||||||
|
|
||||||
// Add Item
|
|
||||||
activity, err := suite.dbm.Queries.AddActivity(context.Background(), AddActivityParams{
|
|
||||||
DocumentID: documentID,
|
|
||||||
DeviceID: deviceID,
|
|
||||||
UserID: testUserID,
|
|
||||||
StartTime: d.UTC().Format(time.RFC3339),
|
|
||||||
Duration: 60,
|
|
||||||
StartPercentage: float64(counter) / 100.0,
|
|
||||||
EndPercentage: float64(counter+1) / 100.0,
|
|
||||||
})
|
|
||||||
|
|
||||||
suite.Nil(err, fmt.Sprintf("[%d] should have nil err for add activity", counter))
|
|
||||||
suite.Equal(counter, activity.ID, fmt.Sprintf("[%d] should have correct id for add activity", counter))
|
|
||||||
}
|
|
||||||
|
|
||||||
err = suite.dbm.CacheTempTables(context.Background())
|
|
||||||
suite.NoError(err)
|
|
||||||
|
|
||||||
// Ensure One Item
|
|
||||||
userStats, err = suite.dbm.Queries.GetUserStatistics(context.Background())
|
|
||||||
suite.Nil(err, "should have nil err")
|
|
||||||
suite.Len(userStats, 1, "should have length of one")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *UsersTestSuite) TestGetUsersStreaks() {
|
|
||||||
err := suite.dbm.CacheTempTables(context.Background())
|
|
||||||
suite.NoError(err)
|
|
||||||
|
|
||||||
// Ensure Zero Items
|
|
||||||
userStats, err := suite.dbm.Queries.GetUserStreaks(context.Background(), testUserID)
|
|
||||||
suite.Nil(err, "should have nil err")
|
|
||||||
suite.Empty(userStats, "should be empty")
|
|
||||||
|
|
||||||
// Create Activity
|
|
||||||
end := time.Now()
|
|
||||||
start := end.AddDate(0, 0, -9)
|
|
||||||
var counter int64 = 0
|
|
||||||
|
|
||||||
for d := start; d.After(end) == false; d = d.AddDate(0, 0, 1) {
|
|
||||||
counter += 1
|
|
||||||
|
|
||||||
// Add Item
|
|
||||||
activity, err := suite.dbm.Queries.AddActivity(context.Background(), AddActivityParams{
|
|
||||||
DocumentID: documentID,
|
|
||||||
DeviceID: deviceID,
|
|
||||||
UserID: testUserID,
|
|
||||||
StartTime: d.UTC().Format(time.RFC3339),
|
|
||||||
Duration: 60,
|
|
||||||
StartPercentage: float64(counter) / 100.0,
|
|
||||||
EndPercentage: float64(counter+1) / 100.0,
|
|
||||||
})
|
|
||||||
|
|
||||||
suite.Nil(err, fmt.Sprintf("[%d] should have nil err for add activity", counter))
|
|
||||||
suite.Equal(counter, activity.ID, fmt.Sprintf("[%d] should have correct id for add activity", counter))
|
|
||||||
}
|
|
||||||
|
|
||||||
err = suite.dbm.CacheTempTables(context.Background())
|
|
||||||
suite.NoError(err)
|
|
||||||
|
|
||||||
// Ensure Two Item
|
|
||||||
userStats, err = suite.dbm.Queries.GetUserStreaks(context.Background(), testUserID)
|
|
||||||
suite.Nil(err, "should have nil err")
|
|
||||||
suite.Len(userStats, 2, "should have length of two")
|
|
||||||
|
|
||||||
// Ensure Streak Stats
|
|
||||||
dayStats := userStats[0]
|
|
||||||
weekStats := userStats[1]
|
|
||||||
suite.Equal(int64(10), dayStats.CurrentStreak, "should be 10 days")
|
|
||||||
suite.Greater(weekStats.CurrentStreak, int64(1), "should be 2 or 3")
|
|
||||||
}
|
|
||||||
296
database/views.sql
Normal file
296
database/views.sql
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
---------------------------------------------------------------
|
||||||
|
---------------------------- Views ----------------------------
|
||||||
|
---------------------------------------------------------------
|
||||||
|
|
||||||
|
--------------------------------
|
||||||
|
--------- User Streaks ---------
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
CREATE VIEW view_user_streaks AS
|
||||||
|
|
||||||
|
WITH document_windows AS (
|
||||||
|
SELECT
|
||||||
|
activity.user_id,
|
||||||
|
users.timezone,
|
||||||
|
DATE(
|
||||||
|
LOCAL_TIME(activity.start_time, users.timezone),
|
||||||
|
'weekday 0', '-7 day'
|
||||||
|
) AS weekly_read,
|
||||||
|
DATE(LOCAL_TIME(activity.start_time, users.timezone)) AS daily_read
|
||||||
|
FROM activity
|
||||||
|
LEFT JOIN users ON users.id = activity.user_id
|
||||||
|
GROUP BY activity.user_id, weekly_read, daily_read
|
||||||
|
),
|
||||||
|
weekly_partitions AS (
|
||||||
|
SELECT
|
||||||
|
user_id,
|
||||||
|
timezone,
|
||||||
|
'WEEK' AS "window",
|
||||||
|
weekly_read AS read_window,
|
||||||
|
row_number() OVER (
|
||||||
|
PARTITION BY user_id ORDER BY weekly_read DESC
|
||||||
|
) AS seqnum
|
||||||
|
FROM document_windows
|
||||||
|
GROUP BY user_id, weekly_read
|
||||||
|
),
|
||||||
|
daily_partitions AS (
|
||||||
|
SELECT
|
||||||
|
user_id,
|
||||||
|
timezone,
|
||||||
|
'DAY' AS "window",
|
||||||
|
daily_read AS read_window,
|
||||||
|
row_number() OVER (
|
||||||
|
PARTITION BY user_id ORDER BY daily_read DESC
|
||||||
|
) AS seqnum
|
||||||
|
FROM document_windows
|
||||||
|
GROUP BY user_id, daily_read
|
||||||
|
),
|
||||||
|
streaks AS (
|
||||||
|
SELECT
|
||||||
|
COUNT(*) AS streak,
|
||||||
|
MIN(read_window) AS start_date,
|
||||||
|
MAX(read_window) AS end_date,
|
||||||
|
window,
|
||||||
|
user_id,
|
||||||
|
timezone
|
||||||
|
FROM daily_partitions
|
||||||
|
GROUP BY
|
||||||
|
timezone,
|
||||||
|
user_id,
|
||||||
|
DATE(read_window, '+' || seqnum || ' day')
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
COUNT(*) AS streak,
|
||||||
|
MIN(read_window) AS start_date,
|
||||||
|
MAX(read_window) AS end_date,
|
||||||
|
window,
|
||||||
|
user_id,
|
||||||
|
timezone
|
||||||
|
FROM weekly_partitions
|
||||||
|
GROUP BY
|
||||||
|
timezone,
|
||||||
|
user_id,
|
||||||
|
DATE(read_window, '+' || (seqnum * 7) || ' day')
|
||||||
|
),
|
||||||
|
max_streak AS (
|
||||||
|
SELECT
|
||||||
|
MAX(streak) AS max_streak,
|
||||||
|
start_date AS max_streak_start_date,
|
||||||
|
end_date AS max_streak_end_date,
|
||||||
|
window,
|
||||||
|
user_id
|
||||||
|
FROM streaks
|
||||||
|
GROUP BY user_id, window
|
||||||
|
),
|
||||||
|
current_streak AS (
|
||||||
|
SELECT
|
||||||
|
streak AS current_streak,
|
||||||
|
start_date AS current_streak_start_date,
|
||||||
|
end_date AS current_streak_end_date,
|
||||||
|
window,
|
||||||
|
user_id
|
||||||
|
FROM streaks
|
||||||
|
WHERE CASE
|
||||||
|
WHEN window = "WEEK" THEN
|
||||||
|
DATE(LOCAL_TIME(STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'), timezone), 'weekday 0', '-14 day') = current_streak_end_date
|
||||||
|
OR DATE(LOCAL_TIME(STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'), timezone), 'weekday 0', '-7 day') = current_streak_end_date
|
||||||
|
WHEN window = "DAY" THEN
|
||||||
|
DATE(LOCAL_TIME(STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'), timezone), '-1 day') = current_streak_end_date
|
||||||
|
OR DATE(LOCAL_TIME(STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'), timezone)) = current_streak_end_date
|
||||||
|
END
|
||||||
|
GROUP BY user_id, window
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
max_streak.user_id,
|
||||||
|
max_streak.window,
|
||||||
|
IFNULL(max_streak, 0) AS max_streak,
|
||||||
|
IFNULL(max_streak_start_date, "N/A") AS max_streak_start_date,
|
||||||
|
IFNULL(max_streak_end_date, "N/A") AS max_streak_end_date,
|
||||||
|
IFNULL(current_streak, 0) AS current_streak,
|
||||||
|
IFNULL(current_streak_start_date, "N/A") AS current_streak_start_date,
|
||||||
|
IFNULL(current_streak_end_date, "N/A") AS current_streak_end_date
|
||||||
|
FROM max_streak
|
||||||
|
LEFT JOIN current_streak ON
|
||||||
|
current_streak.user_id = max_streak.user_id
|
||||||
|
AND current_streak.window = max_streak.window;
|
||||||
|
|
||||||
|
--------------------------------
|
||||||
|
------- Document Stats ---------
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
CREATE VIEW view_document_user_statistics AS
|
||||||
|
|
||||||
|
WITH intermediate_ga AS (
|
||||||
|
SELECT
|
||||||
|
ga1.id AS row_id,
|
||||||
|
ga1.user_id,
|
||||||
|
ga1.document_id,
|
||||||
|
ga1.duration,
|
||||||
|
ga1.start_time,
|
||||||
|
ga1.start_percentage,
|
||||||
|
ga1.end_percentage,
|
||||||
|
|
||||||
|
-- Find Overlapping Events (Assign Unique ID)
|
||||||
|
(
|
||||||
|
SELECT MIN(id)
|
||||||
|
FROM activity AS ga2
|
||||||
|
WHERE
|
||||||
|
ga1.document_id = ga2.document_id
|
||||||
|
AND ga1.user_id = ga2.user_id
|
||||||
|
AND ga1.start_percentage <= ga2.end_percentage
|
||||||
|
AND ga1.end_percentage >= ga2.start_percentage
|
||||||
|
) AS group_leader
|
||||||
|
FROM activity AS ga1
|
||||||
|
),
|
||||||
|
|
||||||
|
grouped_activity AS (
|
||||||
|
SELECT
|
||||||
|
user_id,
|
||||||
|
document_id,
|
||||||
|
MAX(start_time) AS start_time,
|
||||||
|
MIN(start_percentage) AS start_percentage,
|
||||||
|
MAX(end_percentage) AS end_percentage,
|
||||||
|
MAX(end_percentage) - MIN(start_percentage) AS read_percentage,
|
||||||
|
SUM(duration) AS duration
|
||||||
|
FROM intermediate_ga
|
||||||
|
GROUP BY group_leader
|
||||||
|
),
|
||||||
|
|
||||||
|
current_progress AS (
|
||||||
|
SELECT
|
||||||
|
user_id,
|
||||||
|
document_id,
|
||||||
|
COALESCE((
|
||||||
|
SELECT percentage
|
||||||
|
FROM document_progress AS dp
|
||||||
|
WHERE
|
||||||
|
dp.user_id = iga.user_id
|
||||||
|
AND dp.document_id = iga.document_id
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
), end_percentage) AS percentage
|
||||||
|
FROM intermediate_ga AS iga
|
||||||
|
GROUP BY user_id, document_id
|
||||||
|
HAVING MAX(start_time)
|
||||||
|
)
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
ga.document_id,
|
||||||
|
ga.user_id,
|
||||||
|
cp.percentage,
|
||||||
|
MAX(start_time) AS last_read,
|
||||||
|
SUM(read_percentage) AS read_percentage,
|
||||||
|
|
||||||
|
-- All Time WPM
|
||||||
|
SUM(duration) AS total_time_seconds,
|
||||||
|
(CAST(COALESCE(d.words, 0.0) AS REAL) * SUM(read_percentage))
|
||||||
|
AS total_words_read,
|
||||||
|
(CAST(COALESCE(d.words, 0.0) AS REAL) * SUM(read_percentage))
|
||||||
|
/ (SUM(duration) / 60.0) AS total_wpm,
|
||||||
|
|
||||||
|
-- Yearly WPM
|
||||||
|
SUM(CASE WHEN start_time >= DATE('now', '-1 year') THEN duration ELSE 0 END)
|
||||||
|
AS yearly_time_seconds,
|
||||||
|
(
|
||||||
|
CAST(COALESCE(d.words, 0.0) AS REAL)
|
||||||
|
* SUM(
|
||||||
|
CASE
|
||||||
|
WHEN start_time >= DATE('now', '-1 year') THEN read_percentage
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
)
|
||||||
|
)
|
||||||
|
AS yearly_words_read,
|
||||||
|
COALESCE((
|
||||||
|
CAST(COALESCE(d.words, 0.0) AS REAL)
|
||||||
|
* SUM(
|
||||||
|
CASE
|
||||||
|
WHEN start_time >= DATE('now', '-1 year') THEN read_percentage
|
||||||
|
END
|
||||||
|
)
|
||||||
|
)
|
||||||
|
/ (
|
||||||
|
SUM(
|
||||||
|
CASE
|
||||||
|
WHEN start_time >= DATE('now', '-1 year') THEN duration
|
||||||
|
END
|
||||||
|
)
|
||||||
|
/ 60.0
|
||||||
|
), 0.0)
|
||||||
|
AS yearly_wpm,
|
||||||
|
|
||||||
|
-- Monthly WPM
|
||||||
|
SUM(
|
||||||
|
CASE WHEN start_time >= DATE('now', '-1 month') THEN duration ELSE 0 END
|
||||||
|
)
|
||||||
|
AS monthly_time_seconds,
|
||||||
|
(
|
||||||
|
CAST(COALESCE(d.words, 0.0) AS REAL)
|
||||||
|
* SUM(
|
||||||
|
CASE
|
||||||
|
WHEN start_time >= DATE('now', '-1 month') THEN read_percentage
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
)
|
||||||
|
)
|
||||||
|
AS monthly_words_read,
|
||||||
|
COALESCE((
|
||||||
|
CAST(COALESCE(d.words, 0.0) AS REAL)
|
||||||
|
* SUM(
|
||||||
|
CASE
|
||||||
|
WHEN start_time >= DATE('now', '-1 month') THEN read_percentage
|
||||||
|
END
|
||||||
|
)
|
||||||
|
)
|
||||||
|
/ (
|
||||||
|
SUM(
|
||||||
|
CASE
|
||||||
|
WHEN start_time >= DATE('now', '-1 month') THEN duration
|
||||||
|
END
|
||||||
|
)
|
||||||
|
/ 60.0
|
||||||
|
), 0.0)
|
||||||
|
AS monthly_wpm,
|
||||||
|
|
||||||
|
-- Weekly WPM
|
||||||
|
SUM(CASE WHEN start_time >= DATE('now', '-7 days') THEN duration ELSE 0 END)
|
||||||
|
AS weekly_time_seconds,
|
||||||
|
(
|
||||||
|
CAST(COALESCE(d.words, 0.0) AS REAL)
|
||||||
|
* SUM(
|
||||||
|
CASE
|
||||||
|
WHEN start_time >= DATE('now', '-7 days') THEN read_percentage
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
)
|
||||||
|
)
|
||||||
|
AS weekly_words_read,
|
||||||
|
COALESCE((
|
||||||
|
CAST(COALESCE(d.words, 0.0) AS REAL)
|
||||||
|
* SUM(
|
||||||
|
CASE
|
||||||
|
WHEN start_time >= DATE('now', '-7 days') THEN read_percentage
|
||||||
|
END
|
||||||
|
)
|
||||||
|
)
|
||||||
|
/ (
|
||||||
|
SUM(
|
||||||
|
CASE
|
||||||
|
WHEN start_time >= DATE('now', '-7 days') THEN duration
|
||||||
|
END
|
||||||
|
)
|
||||||
|
/ 60.0
|
||||||
|
), 0.0)
|
||||||
|
AS weekly_wpm
|
||||||
|
|
||||||
|
FROM grouped_activity AS ga
|
||||||
|
INNER JOIN
|
||||||
|
current_progress AS cp
|
||||||
|
ON ga.user_id = cp.user_id AND ga.document_id = cp.document_id
|
||||||
|
INNER JOIN
|
||||||
|
documents AS d
|
||||||
|
ON ga.document_id = d.id
|
||||||
|
GROUP BY ga.document_id, ga.user_id
|
||||||
|
ORDER BY total_wpm DESC;
|
||||||
61
flake.lock
generated
61
flake.lock
generated
@@ -1,61 +0,0 @@
|
|||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"flake-utils": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1731533236,
|
|
||||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1754292888,
|
|
||||||
"narHash": "sha256-1ziydHSiDuSnaiPzCQh1mRFBsM2d2yRX9I+5OPGEmIE=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "ce01daebf8489ba97bd1609d185ea276efdeb121",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixos-25.05",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-utils": "flake-utils",
|
|
||||||
"nixpkgs": "nixpkgs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"systems": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
||||||
29
flake.nix
29
flake.nix
@@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
description = "Development Environment";
|
|
||||||
|
|
||||||
inputs = {
|
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
|
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
|
||||||
};
|
|
||||||
|
|
||||||
outputs = { self, nixpkgs, flake-utils }:
|
|
||||||
flake-utils.lib.eachDefaultSystem (system:
|
|
||||||
let
|
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
devShells.default = pkgs.mkShell {
|
|
||||||
packages = with pkgs; [
|
|
||||||
go
|
|
||||||
golangci-lint
|
|
||||||
nodejs
|
|
||||||
tailwindcss
|
|
||||||
python311Packages.grip
|
|
||||||
];
|
|
||||||
shellHook = ''
|
|
||||||
export PATH=$PATH:~/go/bin
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
94
go.mod
94
go.mod
@@ -1,85 +1,81 @@
|
|||||||
module reichard.io/antholume
|
module reichard.io/antholume
|
||||||
|
|
||||||
go 1.24
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/PuerkitoBio/goquery v1.10.3
|
github.com/PuerkitoBio/goquery v1.8.1
|
||||||
github.com/alexedwards/argon2id v1.0.0
|
github.com/alexedwards/argon2id v1.0.0
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9
|
github.com/gabriel-vasile/mimetype v1.4.3
|
||||||
github.com/gin-contrib/multitemplate v1.1.1
|
github.com/gin-contrib/multitemplate v0.0.0-20231230012943-32b233489a81
|
||||||
github.com/gin-contrib/sessions v1.0.4
|
github.com/gin-contrib/sessions v0.0.5
|
||||||
github.com/gin-gonic/gin v1.10.1
|
github.com/gin-gonic/gin v1.9.1
|
||||||
github.com/itchyny/gojq v0.12.17
|
github.com/itchyny/gojq v0.12.14
|
||||||
github.com/jarcoal/httpmock v1.3.1
|
github.com/microcosm-cc/bluemonday v1.0.26
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27
|
github.com/pressly/goose/v3 v3.17.0
|
||||||
github.com/pkg/errors v0.9.1
|
|
||||||
github.com/pressly/goose/v3 v3.24.3
|
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/taylorskalyo/goreader v0.0.0-20230626212555-e7f5644f8115
|
||||||
github.com/taylorskalyo/goreader v1.0.1
|
github.com/urfave/cli/v2 v2.27.1
|
||||||
github.com/urfave/cli/v2 v2.27.7
|
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a
|
||||||
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792
|
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
modernc.org/sqlite v1.38.2
|
modernc.org/sqlite v1.28.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||||
github.com/aymerick/douceur v0.2.0 // indirect
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
github.com/bytedance/sonic v1.14.0 // indirect
|
github.com/bytedance/sonic v1.10.2 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||||
github.com/chenzhuoyu/iasm v0.9.1 // indirect
|
github.com/chenzhuoyu/iasm v0.9.1 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
github.com/go-playground/validator/v10 v10.17.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/context v1.1.2 // indirect
|
github.com/gorilla/context v1.1.2 // indirect
|
||||||
github.com/gorilla/css v1.0.1 // indirect
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||||
github.com/gorilla/sessions v1.4.0 // indirect
|
github.com/gorilla/sessions v1.2.2 // indirect
|
||||||
github.com/itchyny/timefmt-go v0.1.6 // indirect
|
github.com/itchyny/timefmt-go v0.1.5 // indirect
|
||||||
|
github.com/jarcoal/httpmock v1.3.1 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.2.4 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mfridman/interpolate v0.0.2 // indirect
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
github.com/sethvargo/go-retry v0.2.4 // indirect
|
||||||
|
github.com/stretchr/testify v1.8.4 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
|
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/arch v0.20.0 // indirect
|
golang.org/x/arch v0.7.0 // indirect
|
||||||
golang.org/x/crypto v0.41.0 // indirect
|
golang.org/x/crypto v0.18.0 // indirect
|
||||||
golang.org/x/mod v0.27.0 // indirect
|
golang.org/x/mod v0.14.0 // indirect
|
||||||
golang.org/x/net v0.43.0 // indirect
|
golang.org/x/net v0.20.0 // indirect
|
||||||
golang.org/x/sync v0.16.0 // indirect
|
golang.org/x/sync v0.6.0 // indirect
|
||||||
golang.org/x/sys v0.35.0 // indirect
|
golang.org/x/sys v0.16.0 // indirect
|
||||||
golang.org/x/text v0.28.0 // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
golang.org/x/tools v0.36.0 // indirect
|
golang.org/x/tools v0.17.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.7 // indirect
|
google.golang.org/protobuf v1.32.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
lukechampine.com/uint128 v1.3.0 // indirect
|
lukechampine.com/uint128 v1.3.0 // indirect
|
||||||
modernc.org/cc/v3 v3.41.0 // indirect
|
modernc.org/cc/v3 v3.41.0 // indirect
|
||||||
modernc.org/ccgo/v3 v3.17.0 // indirect
|
modernc.org/ccgo/v3 v3.16.15 // indirect
|
||||||
modernc.org/libc v1.66.6 // indirect
|
modernc.org/libc v1.40.7 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.7.2 // indirect
|
||||||
modernc.org/opt v0.1.4 // indirect
|
modernc.org/opt v0.1.3 // indirect
|
||||||
modernc.org/strutil v1.2.1 // indirect
|
modernc.org/strutil v1.2.0 // indirect
|
||||||
modernc.org/token v1.1.0 // indirect
|
modernc.org/token v1.1.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
153
go.sum
153
go.sum
@@ -2,38 +2,27 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25
|
|||||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
github.com/ClickHouse/ch-go v0.58.2 h1:jSm2szHbT9MCAB1rJ3WuCJqmGLi5UTjlNu+f530UTS0=
|
github.com/ClickHouse/ch-go v0.58.2 h1:jSm2szHbT9MCAB1rJ3WuCJqmGLi5UTjlNu+f530UTS0=
|
||||||
github.com/ClickHouse/ch-go v0.58.2/go.mod h1:Ap/0bEmiLa14gYjCiRkYGbXvbe8vwdrfTYWhsuQ99aw=
|
github.com/ClickHouse/ch-go v0.58.2/go.mod h1:Ap/0bEmiLa14gYjCiRkYGbXvbe8vwdrfTYWhsuQ99aw=
|
||||||
github.com/ClickHouse/ch-go v0.65.1 h1:SLuxmLl5Mjj44/XbINsK2HFvzqup0s6rwKLFH347ZhU=
|
|
||||||
github.com/ClickHouse/clickhouse-go/v2 v2.16.0 h1:rhMfnPewXPnY4Q4lQRGdYuTLRBRKJEIEYHtbUMrzmvI=
|
github.com/ClickHouse/clickhouse-go/v2 v2.16.0 h1:rhMfnPewXPnY4Q4lQRGdYuTLRBRKJEIEYHtbUMrzmvI=
|
||||||
github.com/ClickHouse/clickhouse-go/v2 v2.16.0/go.mod h1:J7SPfIxwR+x4mQ+o8MLSe0oY50NNntEqCIjFe/T1VPM=
|
github.com/ClickHouse/clickhouse-go/v2 v2.16.0/go.mod h1:J7SPfIxwR+x4mQ+o8MLSe0oY50NNntEqCIjFe/T1VPM=
|
||||||
github.com/ClickHouse/clickhouse-go/v2 v2.34.0 h1:Y4rqkdrRHgExvC4o/NTbLdY5LFQ3LHS77/RNFxFX3Co=
|
|
||||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
|
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
|
||||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
||||||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||||
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
|
||||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
|
||||||
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
|
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
|
||||||
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
|
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
|
||||||
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
|
||||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
|
||||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
|
||||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||||
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
||||||
github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
|
github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
|
||||||
github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
|
github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
|
||||||
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
|
||||||
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
|
||||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
|
||||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
|
||||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||||
@@ -43,15 +32,10 @@ github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpV
|
|||||||
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||||
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
|
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
|
||||||
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
|
||||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
|
||||||
github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8=
|
github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8=
|
||||||
github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
|
github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
|
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
@@ -68,36 +52,23 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
|||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/elastic/go-sysinfo v1.11.2 h1:mcm4OSYVMyws6+n2HIVMGkln5HOpo5Ie1ZmbbNn0jg4=
|
github.com/elastic/go-sysinfo v1.11.2 h1:mcm4OSYVMyws6+n2HIVMGkln5HOpo5Ie1ZmbbNn0jg4=
|
||||||
github.com/elastic/go-sysinfo v1.11.2/go.mod h1:GKqR8bbMK/1ITnez9NIsIfXQr25aLhRJa7AfT8HpBFQ=
|
github.com/elastic/go-sysinfo v1.11.2/go.mod h1:GKqR8bbMK/1ITnez9NIsIfXQr25aLhRJa7AfT8HpBFQ=
|
||||||
github.com/elastic/go-sysinfo v1.15.3 h1:W+RnmhKFkqPTCRoFq2VCTmsT4p/fwpo+3gKNQsn1XU0=
|
|
||||||
github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0=
|
github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0=
|
||||||
github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss=
|
github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss=
|
||||||
github.com/elastic/go-windows v1.0.2 h1:yoLLsAsV5cfg9FLhZ9EXZ2n2sQFKeDYrHenkcivY4vI=
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
|
||||||
github.com/gin-contrib/multitemplate v0.0.0-20231230012943-32b233489a81 h1:hQ/WeoPMTbN8NHk5i96dWy3D4uF7yCU+kORyWG+P4oU=
|
github.com/gin-contrib/multitemplate v0.0.0-20231230012943-32b233489a81 h1:hQ/WeoPMTbN8NHk5i96dWy3D4uF7yCU+kORyWG+P4oU=
|
||||||
github.com/gin-contrib/multitemplate v0.0.0-20231230012943-32b233489a81/go.mod h1:XLLtIXoP9+9zGcEDc7gAGV3AksGPO+vzv4kXHMJSdU0=
|
github.com/gin-contrib/multitemplate v0.0.0-20231230012943-32b233489a81/go.mod h1:XLLtIXoP9+9zGcEDc7gAGV3AksGPO+vzv4kXHMJSdU0=
|
||||||
github.com/gin-contrib/multitemplate v1.1.1 h1:uzhT/ZWS9nBd1h6P+AaxWaVSVAJRAcKH4yafrBU8sPc=
|
|
||||||
github.com/gin-contrib/multitemplate v1.1.1/go.mod h1:1Sa4984P8+x87U0cg5yWxK4jpbK1cXMYegUCZK6XT/M=
|
|
||||||
github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE=
|
github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE=
|
||||||
github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY=
|
github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY=
|
||||||
github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kbcqy8U=
|
|
||||||
github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs=
|
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
|
||||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
|
||||||
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
|
||||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
|
||||||
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
|
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
|
||||||
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
|
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
|
||||||
github.com/go-faster/errors v0.6.1 h1:nNIPOBkprlKzkThvS/0YaX8Zs9KewLCOSFQS5BU06FI=
|
github.com/go-faster/errors v0.6.1 h1:nNIPOBkprlKzkThvS/0YaX8Zs9KewLCOSFQS5BU06FI=
|
||||||
github.com/go-faster/errors v0.6.1/go.mod h1:5MGV2/2T9yvlrbhe9pD9LO5Z/2zCSq2T8j+Jpi2LAyY=
|
github.com/go-faster/errors v0.6.1/go.mod h1:5MGV2/2T9yvlrbhe9pD9LO5Z/2zCSq2T8j+Jpi2LAyY=
|
||||||
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
|
|
||||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
@@ -110,36 +81,26 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91
|
|||||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||||
github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74=
|
github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74=
|
||||||
github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
|
||||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
|
||||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
||||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||||
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
|
|
||||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
|
||||||
github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
|
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
@@ -152,48 +113,35 @@ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kX
|
|||||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||||
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
|
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
|
||||||
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
|
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
|
||||||
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
|
||||||
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
|
||||||
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
|
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
|
||||||
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||||
github.com/itchyny/gojq v0.12.14 h1:6k8vVtsrhQSYgSGg827AD+PVVaB1NLXEdX+dda2oZCc=
|
github.com/itchyny/gojq v0.12.14 h1:6k8vVtsrhQSYgSGg827AD+PVVaB1NLXEdX+dda2oZCc=
|
||||||
github.com/itchyny/gojq v0.12.14/go.mod h1:y1G7oO7XkcR1LPZO59KyoCRy08T3j9vDYRV0GgYSS+s=
|
github.com/itchyny/gojq v0.12.14/go.mod h1:y1G7oO7XkcR1LPZO59KyoCRy08T3j9vDYRV0GgYSS+s=
|
||||||
github.com/itchyny/gojq v0.12.17 h1:8av8eGduDb5+rvEdaOO+zQUjA04MS0m3Ps8HiD+fceg=
|
|
||||||
github.com/itchyny/gojq v0.12.17/go.mod h1:WBrEMkgAfAGO1LUcGOckBl5O726KPp+OlkKug0I/FEY=
|
|
||||||
github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
|
github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
|
||||||
github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
|
github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
|
||||||
github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q=
|
|
||||||
github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg=
|
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
|
||||||
github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI=
|
github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI=
|
||||||
github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
|
github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
|
||||||
github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
|
|
||||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
|
||||||
github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww=
|
github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww=
|
||||||
github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
|
github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
|
||||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4=
|
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4=
|
||||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak=
|
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak=
|
||||||
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
|
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
|
||||||
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
|
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
|
||||||
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||||
github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4=
|
github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4=
|
||||||
github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
|
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
|
||||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
@@ -206,22 +154,13 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|||||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
|
||||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g=
|
|
||||||
github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
|
|
||||||
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
|
|
||||||
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
|
|
||||||
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
|
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||||
@@ -231,8 +170,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
|
||||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI=
|
github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI=
|
||||||
@@ -243,15 +180,11 @@ github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4a
|
|||||||
github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg=
|
github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg=
|
||||||
github.com/paulmach/orb v0.10.0 h1:guVYVqzxHE/CQ1KpfGO077TR0ATHSNjp4s6XGLn3W9s=
|
github.com/paulmach/orb v0.10.0 h1:guVYVqzxHE/CQ1KpfGO077TR0ATHSNjp4s6XGLn3W9s=
|
||||||
github.com/paulmach/orb v0.10.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
|
github.com/paulmach/orb v0.10.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
|
||||||
github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||||
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
|
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
|
||||||
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
|
||||||
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
|
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
|
||||||
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
|
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
@@ -259,28 +192,21 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pressly/goose/v3 v3.17.0 h1:fT4CL3LRm4kfyLuPWzDFAoxjR5ZHjeJ6uQhibQtBaIs=
|
github.com/pressly/goose/v3 v3.17.0 h1:fT4CL3LRm4kfyLuPWzDFAoxjR5ZHjeJ6uQhibQtBaIs=
|
||||||
github.com/pressly/goose/v3 v3.17.0/go.mod h1:22aw7NpnCPlS86oqkO/+3+o9FuCaJg4ZVWRUO3oGzHQ=
|
github.com/pressly/goose/v3 v3.17.0/go.mod h1:22aw7NpnCPlS86oqkO/+3+o9FuCaJg4ZVWRUO3oGzHQ=
|
||||||
github.com/pressly/goose/v3 v3.24.3 h1:DSWWNwwggVUsYZ0X2VitiAa9sKuqtBfe+Jr9zFGwWlM=
|
|
||||||
github.com/pressly/goose/v3 v3.24.3/go.mod h1:v9zYL4xdViLHCUUJh/mhjnm6JrK7Eul8AS93IxiZM4E=
|
|
||||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||||
github.com/sethvargo/go-retry v0.2.4 h1:T+jHEQy/zKJf5s95UkguisicE0zuF9y7+/vgz08Ocec=
|
github.com/sethvargo/go-retry v0.2.4 h1:T+jHEQy/zKJf5s95UkguisicE0zuF9y7+/vgz08Ocec=
|
||||||
github.com/sethvargo/go-retry v0.2.4/go.mod h1:1afjQuvh7s4gflMObvjLPaWgluLLyhA1wmVZ6KLpICw=
|
github.com/sethvargo/go-retry v0.2.4/go.mod h1:1afjQuvh7s4gflMObvjLPaWgluLLyhA1wmVZ6KLpICw=
|
||||||
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
|
||||||
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
|
|
||||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
@@ -295,24 +221,16 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
|||||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
github.com/taylorskalyo/goreader v0.0.0-20230626212555-e7f5644f8115 h1:OEAIMYp5l9kJ2kT9UPL5QSUriKIIDhnLmpJTy69sltA=
|
github.com/taylorskalyo/goreader v0.0.0-20230626212555-e7f5644f8115 h1:OEAIMYp5l9kJ2kT9UPL5QSUriKIIDhnLmpJTy69sltA=
|
||||||
github.com/taylorskalyo/goreader v0.0.0-20230626212555-e7f5644f8115/go.mod h1:AIVbkIe1G7fpFHiKOdxZnU5p9tFPYNTQyH3H5IrRkGw=
|
github.com/taylorskalyo/goreader v0.0.0-20230626212555-e7f5644f8115/go.mod h1:AIVbkIe1G7fpFHiKOdxZnU5p9tFPYNTQyH3H5IrRkGw=
|
||||||
github.com/taylorskalyo/goreader v1.0.1 h1:eS9SYiHai2aAHhm+YMGRTqrvNt2aoRMTd7p6ftm0crY=
|
|
||||||
github.com/taylorskalyo/goreader v1.0.1/go.mod h1:JrUsWCgnk4C3P5Jsr7Pf2mFrMpsR0ls/0bjR5aorYTI=
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
|
||||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
|
||||||
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
|
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
|
||||||
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||||
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
|
|
||||||
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
|
|
||||||
github.com/vertica/vertica-sql-go v1.3.3 h1:fL+FKEAEy5ONmsvya2WH5T8bhkvY27y/Ik3ReR2T+Qw=
|
github.com/vertica/vertica-sql-go v1.3.3 h1:fL+FKEAEy5ONmsvya2WH5T8bhkvY27y/Ik3ReR2T+Qw=
|
||||||
github.com/vertica/vertica-sql-go v1.3.3/go.mod h1:jnn2GFuv+O2Jcjktb7zyc4Utlbu9YVqpHH/lx63+1M4=
|
github.com/vertica/vertica-sql-go v1.3.3/go.mod h1:jnn2GFuv+O2Jcjktb7zyc4Utlbu9YVqpHH/lx63+1M4=
|
||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||||
@@ -323,53 +241,32 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17
|
|||||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||||
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI=
|
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI=
|
||||||
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
|
|
||||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
|
||||||
github.com/ydb-platform/ydb-go-genproto v0.0.0-20231012155159-f85a672542fd h1:dzWP1Lu+A40W883dK/Mr3xyDSM/2MggS8GtHT0qgAnE=
|
github.com/ydb-platform/ydb-go-genproto v0.0.0-20231012155159-f85a672542fd h1:dzWP1Lu+A40W883dK/Mr3xyDSM/2MggS8GtHT0qgAnE=
|
||||||
github.com/ydb-platform/ydb-go-genproto v0.0.0-20231012155159-f85a672542fd/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I=
|
github.com/ydb-platform/ydb-go-genproto v0.0.0-20231012155159-f85a672542fd/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I=
|
||||||
github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 h1:LY6cI8cP4B9rrpTleZk95+08kl2gF4rixG7+V/dwL6Q=
|
|
||||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.54.2 h1:E0yUuuX7UmPxXm92+yQCjMveLFO3zfvYFIJVuAqsVRA=
|
github.com/ydb-platform/ydb-go-sdk/v3 v3.54.2 h1:E0yUuuX7UmPxXm92+yQCjMveLFO3zfvYFIJVuAqsVRA=
|
||||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.54.2/go.mod h1:fjBLQ2TdQNl4bMjuWl9adoTGBypwUTPoGC+EqYqiIcU=
|
github.com/ydb-platform/ydb-go-sdk/v3 v3.54.2/go.mod h1:fjBLQ2TdQNl4bMjuWl9adoTGBypwUTPoGC+EqYqiIcU=
|
||||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.108.1 h1:ixAiqjj2S/dNuJqrz4AxSqgw2P5OBMXp68hB5nNriUk=
|
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.opentelemetry.io/otel v1.20.0 h1:vsb/ggIY+hUjD/zCAQHpzTmndPqv/ml2ArbsbfBYTAc=
|
go.opentelemetry.io/otel v1.20.0 h1:vsb/ggIY+hUjD/zCAQHpzTmndPqv/ml2ArbsbfBYTAc=
|
||||||
go.opentelemetry.io/otel v1.20.0/go.mod h1:oUIGj3D77RwJdM6PPZImDpSZGDvkD9fhesHny69JFrs=
|
go.opentelemetry.io/otel v1.20.0/go.mod h1:oUIGj3D77RwJdM6PPZImDpSZGDvkD9fhesHny69JFrs=
|
||||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
|
||||||
go.opentelemetry.io/otel/trace v1.20.0 h1:+yxVAPZPbQhbC3OfAkeIVTky6iTFpcr4SiY9om7mXSQ=
|
go.opentelemetry.io/otel/trace v1.20.0 h1:+yxVAPZPbQhbC3OfAkeIVTky6iTFpcr4SiY9om7mXSQ=
|
||||||
go.opentelemetry.io/otel/trace v1.20.0/go.mod h1:HJSK7F/hA5RlzpZ0zKDCHCDHm556LCDtKaAo6JmBFUU=
|
go.opentelemetry.io/otel/trace v1.20.0/go.mod h1:HJSK7F/hA5RlzpZ0zKDCHCDHm556LCDtKaAo6JmBFUU=
|
||||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
|
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
|
||||||
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
|
||||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
|
||||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
|
||||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
|
||||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
|
||||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
|
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
|
||||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
||||||
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4=
|
|
||||||
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
|
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|
||||||
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
||||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
|
||||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
|
||||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
@@ -378,24 +275,13 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
|||||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
|
||||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
|
||||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
|
||||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
|
||||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
|
||||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
|
||||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
|
||||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -409,26 +295,15 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
|
||||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
|
||||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
|
||||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
|
||||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
@@ -438,35 +313,22 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
|||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
|
||||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
|
||||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
|
||||||
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
|
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
|
||||||
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
|
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
|
||||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
|
||||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
|
|
||||||
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
|
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
|
||||||
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
|
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
|
||||||
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
|
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
|
|
||||||
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
@@ -482,43 +344,28 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||||
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
|
|
||||||
lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
|
lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
|
||||||
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||||
modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q=
|
modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q=
|
||||||
modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
|
modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
|
||||||
modernc.org/ccgo/v3 v3.16.15 h1:KbDR3ZAVU+wiLyMESPtbtE/Add4elztFyfsWoNTgxS0=
|
modernc.org/ccgo/v3 v3.16.15 h1:KbDR3ZAVU+wiLyMESPtbtE/Add4elztFyfsWoNTgxS0=
|
||||||
modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI=
|
modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI=
|
||||||
modernc.org/ccgo/v3 v3.17.0 h1:o3OmOqx4/OFnl4Vm3G8Bgmqxnvxnh0nbxeT5p/dWChA=
|
|
||||||
modernc.org/ccgo/v3 v3.17.0/go.mod h1:Sg3fwVpmLvCUTaqEUjiBDAvshIaKDB0RXaf+zgqFu8I=
|
|
||||||
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
|
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
|
||||||
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||||
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
|
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
|
||||||
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
||||||
modernc.org/libc v1.40.7 h1:oeLS0G067ZqUu+v143Dqad0btMfKmNS7SuOsnkq0Ysg=
|
modernc.org/libc v1.40.7 h1:oeLS0G067ZqUu+v143Dqad0btMfKmNS7SuOsnkq0Ysg=
|
||||||
modernc.org/libc v1.40.7/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
|
modernc.org/libc v1.40.7/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
|
||||||
modernc.org/libc v1.66.6 h1:RyQpwAhM/19nXD8y3iejM/AjmKwY2TjxZTlUWTsWw2U=
|
|
||||||
modernc.org/libc v1.66.6/go.mod h1:j8z0EYAuumoMQ3+cWXtmw6m+LYn3qm8dcZDFtFTSq+M=
|
|
||||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
|
||||||
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
||||||
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
|
||||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
|
||||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
|
||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
|
||||||
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
|
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
|
||||||
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
|
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
|
||||||
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
|
|
||||||
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
|
||||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
|
||||||
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
|
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
|
||||||
modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c=
|
modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ func getSVGBezierControlPoint(currentPoint *SVGGraphPoint, prevPoint *SVGGraphPo
|
|||||||
// Modifiers
|
// Modifiers
|
||||||
var smoothingRatio float64 = 0.2
|
var smoothingRatio float64 = 0.2
|
||||||
var directionModifier float64 = 0
|
var directionModifier float64 = 0
|
||||||
if isReverse {
|
if isReverse == true {
|
||||||
directionModifier = math.Pi
|
directionModifier = math.Pi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,12 +53,10 @@ func countEPUBWords(filepath string) (int64, error) {
|
|||||||
rf := rc.Rootfiles[0]
|
rf := rc.Rootfiles[0]
|
||||||
|
|
||||||
var completeCount int64
|
var completeCount int64
|
||||||
for _, item := range rf.Itemrefs {
|
for _, item := range rf.Spine.Itemrefs {
|
||||||
f, _ := item.Open()
|
f, _ := item.Open()
|
||||||
doc, _ := goquery.NewDocumentFromReader(f)
|
doc, _ := goquery.NewDocumentFromReader(f)
|
||||||
doc.Find("script, style, noscript, iframe").Remove()
|
completeCount = completeCount + int64(len(strings.Fields(doc.Text())))
|
||||||
words := len(strings.Fields(doc.Text()))
|
|
||||||
completeCount = completeCount + int64(words)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return completeCount, nil
|
return completeCount, nil
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ func getGBooksMetadata(metadataSearch MetadataInfo) ([]MetadataInfo, error) {
|
|||||||
func saveGBooksCover(gbid string, coverFilePath string, overwrite bool) error {
|
func saveGBooksCover(gbid string, coverFilePath string, overwrite bool) error {
|
||||||
// Validate File Doesn't Exists
|
// Validate File Doesn't Exists
|
||||||
_, err := os.Stat(coverFilePath)
|
_, err := os.Stat(coverFilePath)
|
||||||
if err == nil && !overwrite {
|
if err == nil && overwrite == false {
|
||||||
log.Warn("File Alreads Exists")
|
log.Warn("File Alreads Exists")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ func hookAPI() *details {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert to JSON Response
|
// Convert to JSON Response
|
||||||
var responseData map[string]any
|
var responseData map[string]interface{}
|
||||||
_ = json.Unmarshal([]byte(rawResp), &responseData)
|
json.Unmarshal([]byte(rawResp), &responseData)
|
||||||
|
|
||||||
// Return Response
|
// Return Response
|
||||||
return httpmock.NewJsonResponse(200, responseData)
|
return httpmock.NewJsonResponse(200, responseData)
|
||||||
|
|||||||
@@ -65,9 +65,9 @@ func SearchMetadata(s Source, metadataSearch MetadataInfo) ([]MetadataInfo, erro
|
|||||||
case SOURCE_GBOOK:
|
case SOURCE_GBOOK:
|
||||||
return getGBooksMetadata(metadataSearch)
|
return getGBooksMetadata(metadataSearch)
|
||||||
case SOURCE_OLIB:
|
case SOURCE_OLIB:
|
||||||
return nil, errors.New("not implemented")
|
return nil, errors.New("Not implemented")
|
||||||
default:
|
default:
|
||||||
return nil, errors.New("not implemented")
|
return nil, errors.New("Not implemented")
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,7 +87,7 @@ func GetWordCount(filepath string) (*int64, error) {
|
|||||||
}
|
}
|
||||||
return &totalWords, nil
|
return &totalWords, nil
|
||||||
} else {
|
} else {
|
||||||
return nil, fmt.Errorf("invalid extension: %s", fileExtension)
|
return nil, fmt.Errorf("Invalid extension")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,9 +109,6 @@ func GetMetadata(filepath string) (*MetadataInfo, error) {
|
|||||||
|
|
||||||
// Acquire Metadata
|
// Acquire Metadata
|
||||||
metadataInfo, err := handler(filepath)
|
metadataInfo, err := handler(filepath)
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to acquire metadata")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate MD5 & Partial MD5
|
// Calculate MD5 & Partial MD5
|
||||||
partialMD5, err := utils.CalculatePartialMD5(filepath)
|
partialMD5, err := utils.CalculatePartialMD5(filepath)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestGetWordCount(t *testing.T) {
|
func TestGetWordCount(t *testing.T) {
|
||||||
var desiredCount int64 = 30070
|
var desiredCount int64 = 30080
|
||||||
actualCount, err := countEPUBWords("../_test_files/alice.epub")
|
actualCount, err := countEPUBWords("../_test_files/alice.epub")
|
||||||
|
|
||||||
assert.Nil(t, err, "should have no error")
|
assert.Nil(t, err, "should have no error")
|
||||||
|
|||||||
55
package-lock.json
generated
55
package-lock.json
generated
@@ -1,55 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "antholume",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"name": "antholume",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"devDependencies": {
|
|
||||||
"prettier-plugin-go-template": "^0.0.15"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prettier": {
|
|
||||||
"version": "3.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz",
|
|
||||||
"integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==",
|
|
||||||
"dev": true,
|
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
|
||||||
"prettier": "bin/prettier.cjs"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prettier-plugin-go-template": {
|
|
||||||
"version": "0.0.15",
|
|
||||||
"resolved": "https://registry.npmjs.org/prettier-plugin-go-template/-/prettier-plugin-go-template-0.0.15.tgz",
|
|
||||||
"integrity": "sha512-WqU92E1NokWYNZ9mLE6ijoRg6LtIGdLMePt2C7UBDjXeDH9okcRI3zRqtnWR4s5AloiqyvZ66jNBAa9tmRY5EQ==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"ulid": "^2.3.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"prettier": "^3.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ulid": {
|
|
||||||
"version": "2.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ulid/-/ulid-2.3.0.tgz",
|
|
||||||
"integrity": "sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==",
|
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
|
||||||
"ulid": "bin/cli.js"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "antholume",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"devDependencies": {
|
|
||||||
"prettier-plugin-go-template": "^0.0.15"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package formatters
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FormatDuration takes a duration and returns a human-readable duration string.
|
|
||||||
// For example: 1928371 seconds -> "22d 7h 39m 31s"
|
|
||||||
func FormatDuration(d time.Duration) string {
|
|
||||||
if d == 0 {
|
|
||||||
return "N/A"
|
|
||||||
}
|
|
||||||
|
|
||||||
var parts []string
|
|
||||||
|
|
||||||
days := int(d.Hours()) / 24
|
|
||||||
hours := int(d.Hours()) % 24
|
|
||||||
minutes := int(d.Minutes()) % 60
|
|
||||||
seconds := int(d.Seconds()) % 60
|
|
||||||
|
|
||||||
if days > 0 {
|
|
||||||
parts = append(parts, fmt.Sprintf("%dd", days))
|
|
||||||
}
|
|
||||||
if hours > 0 {
|
|
||||||
parts = append(parts, fmt.Sprintf("%dh", hours))
|
|
||||||
}
|
|
||||||
if minutes > 0 {
|
|
||||||
parts = append(parts, fmt.Sprintf("%dm", minutes))
|
|
||||||
}
|
|
||||||
if seconds > 0 {
|
|
||||||
parts = append(parts, fmt.Sprintf("%ds", seconds))
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(parts, " ")
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package formatters
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FormatNumber takes an int64 and returns a human-readable string.
|
|
||||||
// For example: 19823 -> "19.8k", 1500000 -> "1.5M"
|
|
||||||
func FormatNumber(input int64) string {
|
|
||||||
if input == 0 {
|
|
||||||
return "0"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle Negative
|
|
||||||
negative := input < 0
|
|
||||||
if negative {
|
|
||||||
input = -input
|
|
||||||
}
|
|
||||||
|
|
||||||
abbreviations := []string{"", "k", "M", "B", "T"}
|
|
||||||
abbrevIndex := int(math.Log10(float64(input)) / 3)
|
|
||||||
|
|
||||||
// Bounds Check
|
|
||||||
if abbrevIndex >= len(abbreviations) {
|
|
||||||
abbrevIndex = len(abbreviations) - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
scaledNumber := float64(input) / math.Pow(10, float64(abbrevIndex*3))
|
|
||||||
|
|
||||||
var result string
|
|
||||||
if scaledNumber >= 100 {
|
|
||||||
result = fmt.Sprintf("%.0f%s", scaledNumber, abbreviations[abbrevIndex])
|
|
||||||
} else if scaledNumber >= 10 {
|
|
||||||
result = fmt.Sprintf("%.1f%s", scaledNumber, abbreviations[abbrevIndex])
|
|
||||||
} else {
|
|
||||||
result = fmt.Sprintf("%.2f%s", scaledNumber, abbreviations[abbrevIndex])
|
|
||||||
}
|
|
||||||
|
|
||||||
if negative {
|
|
||||||
result = "-" + result
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package ptr
|
|
||||||
|
|
||||||
func Of[T any](v T) *T {
|
|
||||||
return &v
|
|
||||||
}
|
|
||||||
|
|
||||||
func Deref[T any](v *T) T {
|
|
||||||
var zeroT T
|
|
||||||
if v == nil {
|
|
||||||
return zeroT
|
|
||||||
}
|
|
||||||
return *v
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package sliceutils
|
|
||||||
|
|
||||||
func First[T any](s []T) (T, bool) {
|
|
||||||
if len(s) == 0 {
|
|
||||||
var zeroT T
|
|
||||||
return zeroT, false
|
|
||||||
}
|
|
||||||
return s[0], true
|
|
||||||
}
|
|
||||||
|
|
||||||
func Map[R, I any](s []I, f func(I) R) []R {
|
|
||||||
r := make([]R, 0, len(s))
|
|
||||||
for _, v := range s {
|
|
||||||
r = append(r, f(v))
|
|
||||||
}
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
func Ternary[T any](cond bool, tVal, fVal T) T {
|
|
||||||
if cond {
|
|
||||||
return tVal
|
|
||||||
}
|
|
||||||
return fVal
|
|
||||||
}
|
|
||||||
|
|
||||||
func FirstNonZero[T comparable](v ...T) T {
|
|
||||||
var zero T
|
|
||||||
for _, val := range v {
|
|
||||||
if val != zero {
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return zero
|
|
||||||
}
|
|
||||||
@@ -3,20 +3,26 @@ package search
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/PuerkitoBio/goquery"
|
"github.com/PuerkitoBio/goquery"
|
||||||
)
|
)
|
||||||
|
|
||||||
func searchAnnasArchive(query string) ([]SearchItem, error) {
|
func parseAnnasArchiveDownloadURL(body io.ReadCloser) (string, error) {
|
||||||
searchURL := "https://annas-archive.org/search?index=&q=%s&ext=epub&sort=&lang=en"
|
// Parse
|
||||||
url := fmt.Sprintf(searchURL, url.QueryEscape(query))
|
defer body.Close()
|
||||||
body, err := getPage(url)
|
doc, _ := goquery.NewDocumentFromReader(body)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
// Return Download URL
|
||||||
|
downloadURL, exists := doc.Find("body > table > tbody > tr > td > a").Attr("href")
|
||||||
|
if exists == false {
|
||||||
|
return "", fmt.Errorf("Download URL not found")
|
||||||
}
|
}
|
||||||
return parseAnnasArchive(body)
|
|
||||||
|
// Possible Funky URL
|
||||||
|
downloadURL = strings.ReplaceAll(downloadURL, "\\", "/")
|
||||||
|
|
||||||
|
return downloadURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseAnnasArchive(body io.ReadCloser) ([]SearchItem, error) {
|
func parseAnnasArchive(body io.ReadCloser) ([]SearchItem, error) {
|
||||||
@@ -29,32 +35,39 @@ func parseAnnasArchive(body io.ReadCloser) ([]SearchItem, error) {
|
|||||||
|
|
||||||
// Normalize Results
|
// Normalize Results
|
||||||
var allEntries []SearchItem
|
var allEntries []SearchItem
|
||||||
doc.Find(".js-aarecord-list-outer > div > div").Each(func(ix int, rawBook *goquery.Selection) {
|
doc.Find("form > div.w-full > div.w-full > div > div.justify-center").Each(func(ix int, rawBook *goquery.Selection) {
|
||||||
|
|
||||||
// Parse Details
|
// Parse Details
|
||||||
details := rawBook.Find("div:nth-child(3)").Text()
|
details := rawBook.Find("div:nth-child(2) > div:nth-child(1)").Text()
|
||||||
detailsSplit := strings.Split(details, " · ")
|
detailsSplit := strings.Split(details, ", ")
|
||||||
|
|
||||||
// Invalid Details
|
// Invalid Details
|
||||||
if len(detailsSplit) < 3 {
|
if len(detailsSplit) < 3 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
language := detailsSplit[0]
|
||||||
|
fileType := detailsSplit[1]
|
||||||
|
fileSize := detailsSplit[2]
|
||||||
|
|
||||||
|
// Get Title & Author
|
||||||
|
title := rawBook.Find("h3").Text()
|
||||||
|
author := rawBook.Find("div:nth-child(2) > div:nth-child(4)").Text()
|
||||||
|
|
||||||
// Parse MD5
|
// Parse MD5
|
||||||
titleAuthorDetails := rawBook.Find("div a")
|
itemHref, _ := rawBook.Find("a").Attr("href")
|
||||||
titleEl := titleAuthorDetails.Eq(0)
|
|
||||||
itemHref, _ := titleEl.Attr("href")
|
|
||||||
hrefArray := strings.Split(itemHref, "/")
|
hrefArray := strings.Split(itemHref, "/")
|
||||||
id := hrefArray[len(hrefArray)-1]
|
id := hrefArray[len(hrefArray)-1]
|
||||||
|
|
||||||
allEntries = append(allEntries, SearchItem{
|
item := SearchItem{
|
||||||
ID: id,
|
ID: id,
|
||||||
Title: titleEl.Text(),
|
Title: title,
|
||||||
Author: titleAuthorDetails.Eq(1).Text(),
|
Author: author,
|
||||||
Language: detailsSplit[0],
|
Language: language,
|
||||||
FileType: detailsSplit[1],
|
FileType: fileType,
|
||||||
FileSize: detailsSplit[2],
|
FileSize: fileSize,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
allEntries = append(allEntries, item)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Return Results
|
// Return Results
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
package search
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/PuerkitoBio/goquery"
|
|
||||||
)
|
|
||||||
|
|
||||||
func getLibGenDownloadURL(md5 string, _ Source) ([]string, error) {
|
|
||||||
// Get Page
|
|
||||||
body, err := getPage("http://libgen.li/ads.php?md5=" + md5)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer body.Close()
|
|
||||||
|
|
||||||
// Parse
|
|
||||||
doc, err := goquery.NewDocumentFromReader(body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return Download URL
|
|
||||||
downloadPath, exists := doc.Find("body > table > tbody > tr > td > a").Attr("href")
|
|
||||||
if !exists {
|
|
||||||
return nil, fmt.Errorf("download URL not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Possible Funky URL
|
|
||||||
downloadPath = strings.ReplaceAll(downloadPath, "\\", "/")
|
|
||||||
return []string{fmt.Sprintf("http://libgen.li/%s", downloadPath)}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getLibraryDownloadURL(md5 string, source Source) ([]string, error) {
|
|
||||||
// Derive Info URL
|
|
||||||
var infoURL string
|
|
||||||
switch source {
|
|
||||||
case SOURCE_LIBGEN, SOURCE_ANNAS_ARCHIVE:
|
|
||||||
infoURL = "http://library.lol/fiction/" + md5
|
|
||||||
// case SOURCE_LIBGEN_NON_FICTION:
|
|
||||||
// infoURL = "http://library.lol/main/" + md5
|
|
||||||
default:
|
|
||||||
return nil, errors.New("invalid source")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get Page
|
|
||||||
body, err := getPage(infoURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer body.Close()
|
|
||||||
|
|
||||||
// Parse
|
|
||||||
doc, err := goquery.NewDocumentFromReader(body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return Download URL
|
|
||||||
// downloadURL, _ := doc.Find("#download [href*=cloudflare]").Attr("href")
|
|
||||||
downloadURL, exists := doc.Find("#download h2 a").Attr("href")
|
|
||||||
if !exists {
|
|
||||||
return nil, errors.New("download URL not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
return []string{downloadURL}, nil
|
|
||||||
}
|
|
||||||
147
search/libgen.go
147
search/libgen.go
@@ -1,44 +1,15 @@
|
|||||||
package search
|
package search
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/PuerkitoBio/goquery"
|
"github.com/PuerkitoBio/goquery"
|
||||||
)
|
)
|
||||||
|
|
||||||
const LIBGEN_SEARCH_URL = "https://%s/index.php?req=ext:epub+%s&gmode=on"
|
func parseLibGenFiction(body io.ReadCloser) ([]SearchItem, error) {
|
||||||
|
|
||||||
var libgenDomains []string = []string{
|
|
||||||
"libgen.vg",
|
|
||||||
"libgen.is",
|
|
||||||
}
|
|
||||||
|
|
||||||
func searchLibGen(query string) ([]SearchItem, error) {
|
|
||||||
var allErrors []error
|
|
||||||
|
|
||||||
for _, domain := range libgenDomains {
|
|
||||||
url := fmt.Sprintf(LIBGEN_SEARCH_URL, domain, url.QueryEscape(query))
|
|
||||||
body, err := getPage(url)
|
|
||||||
if err != nil {
|
|
||||||
allErrors = append(allErrors, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
results, err := parseLibGen(body)
|
|
||||||
if err != nil {
|
|
||||||
allErrors = append(allErrors, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return results, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("could not query libgen: %w", errors.Join(allErrors...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseLibGen(body io.ReadCloser) ([]SearchItem, error) {
|
|
||||||
// Parse
|
// Parse
|
||||||
defer body.Close()
|
defer body.Close()
|
||||||
doc, err := goquery.NewDocumentFromReader(body)
|
doc, err := goquery.NewDocumentFromReader(body)
|
||||||
@@ -48,27 +19,105 @@ func parseLibGen(body io.ReadCloser) ([]SearchItem, error) {
|
|||||||
|
|
||||||
// Normalize Results
|
// Normalize Results
|
||||||
var allEntries []SearchItem
|
var allEntries []SearchItem
|
||||||
doc.Find("#tablelibgen tbody > tr").Each(func(ix int, rawBook *goquery.Selection) {
|
doc.Find("table.catalog tbody > tr").Each(func(ix int, rawBook *goquery.Selection) {
|
||||||
// Parse MD5
|
|
||||||
linksRaw := rawBook.Find("td:nth-child(9) a")
|
|
||||||
linksHref, _ := linksRaw.Attr("href")
|
|
||||||
hrefArray := strings.Split(linksHref, "?md5=")
|
|
||||||
if len(hrefArray) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
id := hrefArray[1]
|
|
||||||
|
|
||||||
allEntries = append(allEntries, SearchItem{
|
// Parse File Details
|
||||||
ID: id,
|
fileItem := rawBook.Find("td:nth-child(5)")
|
||||||
Title: rawBook.Find("td:nth-child(1) > a").First().Text(),
|
fileDesc := fileItem.Text()
|
||||||
Author: rawBook.Find("td:nth-child(2)").Text(),
|
fileDescSplit := strings.Split(fileDesc, "/")
|
||||||
Series: rawBook.Find("td:nth-child(1) > b").Text(),
|
fileType := strings.ToLower(strings.TrimSpace(fileDescSplit[0]))
|
||||||
Language: rawBook.Find("td:nth-child(5)").Text(),
|
fileSize := strings.TrimSpace(fileDescSplit[1])
|
||||||
FileType: strings.ToLower(strings.TrimSpace(rawBook.Find("td:nth-child(8)").Text())),
|
|
||||||
FileSize: strings.ToLower(strings.TrimSpace(rawBook.Find("td:nth-child(7)").Text())),
|
// Parse Upload Date
|
||||||
})
|
uploadedRaw, _ := fileItem.Attr("title")
|
||||||
|
uploadedDateRaw := strings.Split(uploadedRaw, "Uploaded at ")[1]
|
||||||
|
uploadDate, _ := time.Parse("2006-01-02 15:04:05", uploadedDateRaw)
|
||||||
|
|
||||||
|
// Parse MD5
|
||||||
|
editHref, _ := rawBook.Find("td:nth-child(7) a").Attr("href")
|
||||||
|
hrefArray := strings.Split(editHref, "/")
|
||||||
|
id := hrefArray[len(hrefArray)-1]
|
||||||
|
|
||||||
|
// Parse Other Details
|
||||||
|
title := rawBook.Find("td:nth-child(3) p a").Text()
|
||||||
|
author := rawBook.Find(".catalog_authors li a").Text()
|
||||||
|
language := rawBook.Find("td:nth-child(4)").Text()
|
||||||
|
series := rawBook.Find("td:nth-child(2)").Text()
|
||||||
|
|
||||||
|
item := SearchItem{
|
||||||
|
ID: id,
|
||||||
|
Title: title,
|
||||||
|
Author: author,
|
||||||
|
Series: series,
|
||||||
|
Language: language,
|
||||||
|
FileType: fileType,
|
||||||
|
FileSize: fileSize,
|
||||||
|
UploadDate: uploadDate.Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
|
||||||
|
allEntries = append(allEntries, item)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Return Results
|
// Return Results
|
||||||
return allEntries, nil
|
return allEntries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseLibGenNonFiction(body io.ReadCloser) ([]SearchItem, error) {
|
||||||
|
// Parse
|
||||||
|
defer body.Close()
|
||||||
|
doc, err := goquery.NewDocumentFromReader(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize Results
|
||||||
|
var allEntries []SearchItem
|
||||||
|
doc.Find("table.c tbody > tr:nth-child(n + 2)").Each(func(ix int, rawBook *goquery.Selection) {
|
||||||
|
|
||||||
|
// Parse Type & Size
|
||||||
|
fileSize := strings.ToLower(strings.TrimSpace(rawBook.Find("td:nth-child(8)").Text()))
|
||||||
|
fileType := strings.ToLower(strings.TrimSpace(rawBook.Find("td:nth-child(9)").Text()))
|
||||||
|
|
||||||
|
// Parse MD5
|
||||||
|
titleRaw := rawBook.Find("td:nth-child(3) [id]")
|
||||||
|
editHref, _ := titleRaw.Attr("href")
|
||||||
|
hrefArray := strings.Split(editHref, "?md5=")
|
||||||
|
id := hrefArray[1]
|
||||||
|
|
||||||
|
// Parse Other Details
|
||||||
|
title := titleRaw.Text()
|
||||||
|
author := rawBook.Find("td:nth-child(2)").Text()
|
||||||
|
language := rawBook.Find("td:nth-child(7)").Text()
|
||||||
|
series := rawBook.Find("td:nth-child(3) [href*='column=series']").Text()
|
||||||
|
|
||||||
|
item := SearchItem{
|
||||||
|
ID: id,
|
||||||
|
Title: title,
|
||||||
|
Author: author,
|
||||||
|
Series: series,
|
||||||
|
Language: language,
|
||||||
|
FileType: fileType,
|
||||||
|
FileSize: fileSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
allEntries = append(allEntries, item)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Return Results
|
||||||
|
return allEntries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLibGenDownloadURL(body io.ReadCloser) (string, error) {
|
||||||
|
// Parse
|
||||||
|
defer body.Close()
|
||||||
|
doc, _ := goquery.NewDocumentFromReader(body)
|
||||||
|
|
||||||
|
// Return Download URL
|
||||||
|
// downloadURL, _ := doc.Find("#download [href*=cloudflare]").Attr("href")
|
||||||
|
downloadURL, exists := doc.Find("#download h2 a").Attr("href")
|
||||||
|
if exists == false {
|
||||||
|
return "", fmt.Errorf("Download URL not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return downloadURL, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
package search
|
|
||||||
|
|
||||||
type writeCounter struct {
|
|
||||||
Total int64
|
|
||||||
Current int64
|
|
||||||
ProgressFunction func(float32)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wc *writeCounter) Write(p []byte) (int, error) {
|
|
||||||
n := len(p)
|
|
||||||
wc.Current += int64(n)
|
|
||||||
wc.flushProgress()
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wc *writeCounter) flushProgress() {
|
|
||||||
if wc.ProgressFunction == nil || wc.Total < 100000 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
percentage := float32(wc.Current) * 100 / float32(wc.Total)
|
|
||||||
wc.ProgressFunction(percentage)
|
|
||||||
}
|
|
||||||
203
search/search.go
203
search/search.go
@@ -2,18 +2,17 @@ package search
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"reichard.io/antholume/metadata"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const userAgent string = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
const userAgent string = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0"
|
||||||
|
|
||||||
type Cadence string
|
type Cadence string
|
||||||
|
|
||||||
@@ -22,11 +21,19 @@ const (
|
|||||||
CADENCE_TOP_MONTH Cadence = "m"
|
CADENCE_TOP_MONTH Cadence = "m"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type BookType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
BOOK_FICTION BookType = iota
|
||||||
|
BOOK_NON_FICTION
|
||||||
|
)
|
||||||
|
|
||||||
type Source string
|
type Source string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SOURCE_ANNAS_ARCHIVE Source = "Annas Archive"
|
SOURCE_ANNAS_ARCHIVE Source = "Annas Archive"
|
||||||
SOURCE_LIBGEN Source = "LibGen"
|
SOURCE_LIBGEN_FICTION Source = "LibGen Fiction"
|
||||||
|
SOURCE_LIBGEN_NON_FICTION Source = "LibGen Non-fiction"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SearchItem struct {
|
type SearchItem struct {
|
||||||
@@ -40,74 +47,120 @@ type SearchItem struct {
|
|||||||
UploadDate string
|
UploadDate string
|
||||||
}
|
}
|
||||||
|
|
||||||
type searchFunc func(query string) (searchResults []SearchItem, err error)
|
type sourceDef struct {
|
||||||
type downloadFunc func(md5 string, source Source) (downloadURL []string, err error)
|
searchURL string
|
||||||
|
downloadURL string
|
||||||
var searchDefs = map[Source]searchFunc{
|
parseSearchFunc func(io.ReadCloser) ([]SearchItem, error)
|
||||||
SOURCE_ANNAS_ARCHIVE: searchAnnasArchive,
|
parseDownloadFunc func(io.ReadCloser) (string, error)
|
||||||
SOURCE_LIBGEN: searchLibGen,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var downloadFuncs = []downloadFunc{
|
var sourceDefs = map[Source]sourceDef{
|
||||||
getLibGenDownloadURL,
|
SOURCE_ANNAS_ARCHIVE: {
|
||||||
getLibraryDownloadURL,
|
searchURL: "https://annas-archive.org/search?index=&q=%s&ext=epub&sort=&lang=en",
|
||||||
|
downloadURL: "http://libgen.li/ads.php?md5=%s",
|
||||||
|
parseSearchFunc: parseAnnasArchive,
|
||||||
|
parseDownloadFunc: parseAnnasArchiveDownloadURL,
|
||||||
|
},
|
||||||
|
SOURCE_LIBGEN_FICTION: {
|
||||||
|
searchURL: "https://libgen.is/fiction/?q=%s&language=English&format=epub",
|
||||||
|
downloadURL: "http://library.lol/fiction/%s",
|
||||||
|
parseSearchFunc: parseLibGenFiction,
|
||||||
|
parseDownloadFunc: parseLibGenDownloadURL,
|
||||||
|
},
|
||||||
|
SOURCE_LIBGEN_NON_FICTION: {
|
||||||
|
searchURL: "https://libgen.is/search.php?req=%s",
|
||||||
|
downloadURL: "http://library.lol/main/%s",
|
||||||
|
parseSearchFunc: parseLibGenNonFiction,
|
||||||
|
parseDownloadFunc: parseLibGenDownloadURL,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func SearchBook(query string, source Source) ([]SearchItem, error) {
|
func SearchBook(query string, source Source) ([]SearchItem, error) {
|
||||||
searchFunc, found := searchDefs[source]
|
def := sourceDefs[source]
|
||||||
if !found {
|
log.Debug("Source: ", def)
|
||||||
return nil, fmt.Errorf("invalid source: %s", source)
|
url := fmt.Sprintf(def.searchURL, url.QueryEscape(query))
|
||||||
|
body, err := getPage(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
log.Debug("Source: ", source)
|
return def.parseSearchFunc(body)
|
||||||
return searchFunc(query)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func SaveBook(md5 string, source Source, progressFunc func(float32)) (string, *metadata.MetadataInfo, error) {
|
func SaveBook(id string, source Source) (string, error) {
|
||||||
for _, f := range downloadFuncs {
|
def := sourceDefs[source]
|
||||||
downloadURLs, err := f(md5, source)
|
log.Debug("Source: ", def)
|
||||||
if err != nil {
|
url := fmt.Sprintf(def.downloadURL, id)
|
||||||
log.Error("failed to acquire download urls")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, bookURL := range downloadURLs {
|
body, err := getPage(url)
|
||||||
// Download File
|
if err != nil {
|
||||||
log.Info("Downloading Book: ", bookURL)
|
return "", err
|
||||||
fileName, err := downloadBook(bookURL, progressFunc)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Book URL API Failure: ", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get Metadata
|
|
||||||
metadata, err := metadata.GetMetadata(fileName)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Book Metadata Failure: ", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileName, metadata, nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", nil, errors.New("failed to download book")
|
bookURL, err := def.parseDownloadFunc(body)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Parse Download URL Error: ", err)
|
||||||
|
return "", fmt.Errorf("Download Failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create File
|
||||||
|
tempFile, err := os.CreateTemp("", "book")
|
||||||
|
if err != nil {
|
||||||
|
log.Error("File Create Error: ", err)
|
||||||
|
return "", fmt.Errorf("File Failure")
|
||||||
|
}
|
||||||
|
defer tempFile.Close()
|
||||||
|
|
||||||
|
// Download File
|
||||||
|
log.Info("Downloading Book: ", bookURL)
|
||||||
|
resp, err := downloadBook(bookURL)
|
||||||
|
if err != nil {
|
||||||
|
os.Remove(tempFile.Name())
|
||||||
|
log.Error("Book URL API Failure: ", err)
|
||||||
|
return "", fmt.Errorf("API Failure")
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Copy File to Disk
|
||||||
|
log.Info("Saving Book")
|
||||||
|
_, err = io.Copy(tempFile, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
os.Remove(tempFile.Name())
|
||||||
|
log.Error("File Copy Error: ", err)
|
||||||
|
return "", fmt.Errorf("File Failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
return tempFile.Name(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetBookURL(id string, bookType BookType) (string, error) {
|
||||||
|
// Derive Info URL
|
||||||
|
var infoURL string
|
||||||
|
if bookType == BOOK_FICTION {
|
||||||
|
infoURL = "http://library.lol/fiction/" + id
|
||||||
|
} else if bookType == BOOK_NON_FICTION {
|
||||||
|
infoURL = "http://library.lol/main/" + id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse & Derive Download URL
|
||||||
|
body, err := getPage(infoURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// downloadURL := parseLibGenDownloadURL(body)
|
||||||
|
return parseLibGenDownloadURL(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPage(page string) (io.ReadCloser, error) {
|
func getPage(page string) (io.ReadCloser, error) {
|
||||||
log.Debug("URL: ", page)
|
log.Debug("URL: ", page)
|
||||||
|
|
||||||
// Set 10s Timeout
|
// Set 10s Timeout
|
||||||
client := http.Client{Timeout: 10 * time.Second}
|
client := http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
// Start Request
|
|
||||||
req, err := http.NewRequest("GET", page, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
req.Header.Set("User-Agent", userAgent)
|
|
||||||
|
|
||||||
// Do Request
|
// Get Page
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Get(page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -116,46 +169,20 @@ func getPage(page string) (io.ReadCloser, error) {
|
|||||||
return resp.Body, err
|
return resp.Body, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func downloadBook(bookURL string, progressFunc func(float32)) (string, error) {
|
func downloadBook(bookURL string) (*http.Response, error) {
|
||||||
log.Debug("URL: ", bookURL)
|
|
||||||
|
|
||||||
// Allow Insecure
|
// Allow Insecure
|
||||||
client := &http.Client{
|
client := &http.Client{Transport: &http.Transport{
|
||||||
Transport: &http.Transport{
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
}}
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start Request
|
// Start Request
|
||||||
req, err := http.NewRequest("GET", bookURL, nil)
|
req, err := http.NewRequest("GET", bookURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set UserAgent
|
||||||
req.Header.Set("User-Agent", userAgent)
|
req.Header.Set("User-Agent", userAgent)
|
||||||
|
|
||||||
// Perform API Request
|
return client.Do(req)
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create File
|
|
||||||
tempFile, err := os.CreateTemp("", "book")
|
|
||||||
if err != nil {
|
|
||||||
log.Error("File Create Error: ", err)
|
|
||||||
return "", fmt.Errorf("failed to create temp file: %w", err)
|
|
||||||
}
|
|
||||||
defer tempFile.Close()
|
|
||||||
|
|
||||||
// Copy File to Disk
|
|
||||||
log.Info("Saving Book")
|
|
||||||
counter := &writeCounter{Total: resp.ContentLength, ProgressFunction: progressFunc}
|
|
||||||
_, err = io.Copy(tempFile, io.TeeReader(resp.Body, counter))
|
|
||||||
if err != nil {
|
|
||||||
os.Remove(tempFile.Name())
|
|
||||||
log.Error("File Copy Error: ", err)
|
|
||||||
return "", fmt.Errorf("failed to copy response to temp file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return tempFile.Name(), nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -53,14 +52,12 @@ func (s *server) Start() {
|
|||||||
ticker := time.NewTicker(15 * time.Minute)
|
ticker := time.NewTicker(15 * time.Minute)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Minute))
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
s.runScheduledTasks(ctx)
|
s.runScheduledTasks()
|
||||||
case <-s.done:
|
case <-s.done:
|
||||||
log.Info("Stopping task runner...")
|
log.Info("Stopping task runner...")
|
||||||
cancel()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,9 +81,9 @@ func (s *server) Stop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run normal scheduled tasks
|
// Run normal scheduled tasks
|
||||||
func (s *server) runScheduledTasks(ctx context.Context) {
|
func (s *server) runScheduledTasks() {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
if err := s.db.CacheTempTables(ctx); err != nil {
|
if err := s.db.CacheTempTables(); err != nil {
|
||||||
log.Warn("Refreshing temp table cache failed: ", err)
|
log.Warn("Refreshing temp table cache failed: ", err)
|
||||||
}
|
}
|
||||||
log.Debug("Completed in: ", time.Since(start))
|
log.Debug("Completed in: ", time.Since(start))
|
||||||
|
|||||||
12
shell.nix
Normal file
12
shell.nix
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{ pkgs ? import <nixpkgs> { } }:
|
||||||
|
|
||||||
|
pkgs.mkShell {
|
||||||
|
packages = with pkgs; [
|
||||||
|
go
|
||||||
|
nodePackages.tailwindcss
|
||||||
|
python311Packages.grip
|
||||||
|
];
|
||||||
|
shellHook = ''
|
||||||
|
export PATH=$PATH:~/go/bin
|
||||||
|
'';
|
||||||
|
}
|
||||||
@@ -1,26 +1,18 @@
|
|||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta
|
<meta name="viewport"
|
||||||
name="viewport"
|
content="width=device-width, initial-scale=0.90, user-scalable=no, viewport-fit=cover" />
|
||||||
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-capable" content="yes" />
|
||||||
<meta
|
<meta name="apple-mobile-web-app-status-bar-style"
|
||||||
name="apple-mobile-web-app-status-bar-style"
|
content="black-translucent" />
|
||||||
content="black-translucent"
|
<meta name="theme-color"
|
||||||
/>
|
content="#F3F4F6"
|
||||||
<meta
|
media="(prefers-color-scheme: light)" />
|
||||||
name="theme-color"
|
<meta name="theme-color"
|
||||||
content="#F3F4F6"
|
content="#1F2937"
|
||||||
media="(prefers-color-scheme: light)"
|
media="(prefers-color-scheme: dark)" />
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
name="theme-color"
|
|
||||||
content="#1F2937"
|
|
||||||
media="(prefers-color-scheme: dark)"
|
|
||||||
/>
|
|
||||||
<title>AnthoLume - {{ block "title" . }}{{ end }}</title>
|
<title>AnthoLume - {{ block "title" . }}{{ end }}</title>
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<link rel="stylesheet" href="/assets/style.css" />
|
<link rel="stylesheet" href="/assets/style.css" />
|
||||||
@@ -40,8 +32,7 @@
|
|||||||
|
|
||||||
html {
|
html {
|
||||||
height: calc(100% + env(safe-area-inset-bottom));
|
height: calc(100% + env(safe-area-inset-bottom));
|
||||||
padding: env(safe-area-inset-top) env(safe-area-inset-right) 0
|
padding: env(safe-area-inset-top) env(safe-area-inset-right) 0 env(safe-area-inset-left);
|
||||||
env(safe-area-inset-left);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
@@ -66,12 +57,12 @@
|
|||||||
/* ----------------------------- */
|
/* ----------------------------- */
|
||||||
/* -------- CSS Button -------- */
|
/* -------- CSS Button -------- */
|
||||||
/* ----------------------------- */
|
/* ----------------------------- */
|
||||||
.css-button:checked + div {
|
.css-button:checked+div {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.css-button + div {
|
.css-button+div {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
@@ -79,7 +70,7 @@
|
|||||||
/* ----------------------------- */
|
/* ----------------------------- */
|
||||||
/* ------- User Dropdown ------- */
|
/* ------- User Dropdown ------- */
|
||||||
/* ----------------------------- */
|
/* ----------------------------- */
|
||||||
#user-dropdown-button:checked + #user-dropdown {
|
#user-dropdown-button:checked+#user-dropdown {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
@@ -94,9 +85,8 @@
|
|||||||
/* ----------------------------- */
|
/* ----------------------------- */
|
||||||
#mobile-nav-button span {
|
#mobile-nav-button span {
|
||||||
transform-origin: 5px 0px;
|
transform-origin: 5px 0px;
|
||||||
transition:
|
transition: transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1.0),
|
||||||
transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1),
|
background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1.0),
|
||||||
background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1),
|
|
||||||
opacity 0.55s ease;
|
opacity 0.55s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,26 +98,26 @@
|
|||||||
transform-origin: 0% 100%;
|
transform-origin: 0% 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#mobile-nav-button input:checked ~ span {
|
#mobile-nav-button input:checked~span {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: rotate(45deg) translate(2px, -2px);
|
transform: rotate(45deg) translate(2px, -2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#mobile-nav-button input:checked ~ span:nth-last-child(3) {
|
#mobile-nav-button input:checked~span:nth-last-child(3) {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: rotate(0deg) scale(0.2, 0.2);
|
transform: rotate(0deg) scale(0.2, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#mobile-nav-button input:checked ~ span:nth-last-child(2) {
|
#mobile-nav-button input:checked~span:nth-last-child(2) {
|
||||||
transform: rotate(-45deg) translate(0, 6px);
|
transform: rotate(-45deg) translate(0, 6px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#mobile-nav-button input:checked ~ div {
|
#mobile-nav-button input:checked~div {
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
#mobile-nav-button input ~ div {
|
#mobile-nav-button input~div {
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,15 +127,12 @@
|
|||||||
padding-top: env(safe-area-inset-top);
|
padding-top: env(safe-area-inset-top);
|
||||||
transform-origin: 0% 0%;
|
transform-origin: 0% 0%;
|
||||||
transform: translate(-100%, 0);
|
transform: translate(-100%, 0);
|
||||||
transition: transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1);
|
transition: transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (orientation: landscape) {
|
@media (orientation: landscape) {
|
||||||
#menu {
|
#menu {
|
||||||
transform: translate(
|
transform: translate(calc(-1 * (env(safe-area-inset-left) + 100%)), 0);
|
||||||
calc(-1 * (env(safe-area-inset-left) + 100%)),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -153,243 +140,141 @@
|
|||||||
<body class="bg-gray-100 dark:bg-gray-800">
|
<body class="bg-gray-100 dark:bg-gray-800">
|
||||||
<div class="flex items-center justify-between w-full h-16">
|
<div class="flex items-center justify-between w-full h-16">
|
||||||
<div id="mobile-nav-button" class="flex flex-col z-40 relative ml-6">
|
<div id="mobile-nav-button" class="flex flex-col z-40 relative ml-6">
|
||||||
<input
|
<input type="checkbox"
|
||||||
type="checkbox"
|
class="absolute lg:hidden z-50 -top-2 w-7 h-7 flex cursor-pointer opacity-0" />
|
||||||
class="absolute lg:hidden z-50 -top-2 w-7 h-7 flex cursor-pointer opacity-0"
|
<span class="lg:hidden bg-black w-7 h-0.5 z-40 mt-0.5 dark:bg-white"></span>
|
||||||
/>
|
<span class="lg:hidden bg-black w-7 h-0.5 z-40 mt-1 dark:bg-white"></span>
|
||||||
<span
|
<span class="lg:hidden bg-black w-7 h-0.5 z-40 mt-1 dark:bg-white"></span>
|
||||||
class="lg:hidden bg-black w-7 h-0.5 z-40 mt-0.5 dark:bg-white"
|
<div id="menu"
|
||||||
></span>
|
class="fixed -ml-6 h-full w-56 lg:w-48 bg-white dark:bg-gray-700 shadow-lg">
|
||||||
<span
|
|
||||||
class="lg:hidden bg-black w-7 h-0.5 z-40 mt-1 dark:bg-white"
|
|
||||||
></span>
|
|
||||||
<span
|
|
||||||
class="lg:hidden bg-black w-7 h-0.5 z-40 mt-1 dark:bg-white"
|
|
||||||
></span>
|
|
||||||
<div
|
|
||||||
id="menu"
|
|
||||||
class="fixed -ml-6 h-full w-56 lg:w-48 bg-white dark:bg-gray-700 shadow-lg"
|
|
||||||
>
|
|
||||||
<div class="h-16 flex justify-end lg:justify-around">
|
<div class="h-16 flex justify-end lg:justify-around">
|
||||||
<p
|
<p class="text-xl font-bold dark:text-white text-right my-auto pr-8 lg:pr-0">AnthoLume</p>
|
||||||
class="text-xl font-bold dark:text-white text-right my-auto pr-8 lg:pr-0"
|
|
||||||
>
|
|
||||||
AnthoLume
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{{ $default := "flex items-center justify-start w-full p-2 pl-6 my-2 transition-colors duration-200 border-l-4" }}
|
{{ $default := "flex items-center justify-start w-full p-2 pl-6 my-2 transition-colors duration-200 border-l-4" }}
|
||||||
{{ $inactive := "border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-100" }}
|
{{ $inactive := "border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-100"}}
|
||||||
{{ $active := "border-purple-500 dark:text-white" }}
|
{{ $active := "border-purple-500 dark:text-white"}}
|
||||||
<a
|
<a class="{{ $default }} {{ if eq .RouteName "home" }}{{ $active }}{{ else if true }}{{ $inactive }}{{ end }}"
|
||||||
class="{{ $default }} {{ if eq .RouteName "home" }}
|
href="/">
|
||||||
{{ $active }}
|
|
||||||
{{ else if true }}
|
|
||||||
{{ $inactive }}
|
|
||||||
{{ end }}"
|
|
||||||
href="/"
|
|
||||||
>
|
|
||||||
{{ template "svg/home" (dict "Size" 20) }}
|
{{ template "svg/home" (dict "Size" 20) }}
|
||||||
<span class="mx-4 text-sm font-normal">Home</span>
|
<span class="mx-4 text-sm font-normal">Home</span>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a class="{{ $default }} {{ if eq .RouteName "documents" }}{{ $active }}{{ else if true }}{{ $inactive }}{{ end }}"
|
||||||
class="{{ $default }} {{ if eq .RouteName "documents" }}
|
href="/documents">
|
||||||
{{ $active }}
|
|
||||||
{{ else if true }}
|
|
||||||
{{ $inactive }}
|
|
||||||
{{ end }}"
|
|
||||||
href="/documents"
|
|
||||||
>
|
|
||||||
{{ template "svg/documents" (dict "Size" 20) }}
|
{{ template "svg/documents" (dict "Size" 20) }}
|
||||||
<span class="mx-4 text-sm font-normal">Documents</span>
|
<span class="mx-4 text-sm font-normal">Documents</span>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a class="{{ $default }} {{ if eq .RouteName "progress" }}{{ $active }}{{ else if true }}{{ $inactive }}{{ end }}"
|
||||||
class="{{ $default }} {{ if eq .RouteName "progress" }}
|
href="/progress">
|
||||||
{{ $active }}
|
|
||||||
{{ else if true }}
|
|
||||||
{{ $inactive }}
|
|
||||||
{{ end }}"
|
|
||||||
href="/progress"
|
|
||||||
>
|
|
||||||
{{ template "svg/activity" (dict "Size" 20) }}
|
{{ template "svg/activity" (dict "Size" 20) }}
|
||||||
<span class="mx-4 text-sm font-normal">Progress</span>
|
<span class="mx-4 text-sm font-normal">Progress</span>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a class="{{ $default }} {{ if eq .RouteName "activity" }}{{ $active }}{{ else if true }}{{ $inactive }}{{ end }}"
|
||||||
class="{{ $default }} {{ if eq .RouteName "activity" }}
|
href="/activity">
|
||||||
{{ $active }}
|
|
||||||
{{ else if true }}
|
|
||||||
{{ $inactive }}
|
|
||||||
{{ end }}"
|
|
||||||
href="/activity"
|
|
||||||
>
|
|
||||||
{{ template "svg/activity" (dict "Size" 20) }}
|
{{ template "svg/activity" (dict "Size" 20) }}
|
||||||
<span class="mx-4 text-sm font-normal">Activity</span>
|
<span class="mx-4 text-sm font-normal">Activity</span>
|
||||||
</a>
|
</a>
|
||||||
{{ if .Config.SearchEnabled }}
|
{{ if .Config.SearchEnabled }}
|
||||||
<a
|
<a class="{{ $default }} {{ if eq .RouteName "search" }}{{ $active }}{{ else if true }}{{ $inactive }}{{ end }}"
|
||||||
class="{{ $default }} {{ if eq .RouteName "search" }}
|
href="/search">
|
||||||
{{ $active }}
|
{{ template "svg/search" (dict "Size" 20) }}
|
||||||
{{ else if true }}
|
<span class="mx-4 text-sm font-normal">Search</span>
|
||||||
{{ $inactive }}
|
</a>
|
||||||
{{ end }}"
|
|
||||||
href="/search"
|
|
||||||
>
|
|
||||||
{{ template "svg/search" (dict "Size" 20) }}
|
|
||||||
<span class="mx-4 text-sm font-normal">Search</span>
|
|
||||||
</a>
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ if .Authorization.IsAdmin }}
|
{{ if .Authorization.IsAdmin }}
|
||||||
<div
|
<div class="flex flex-col gap-4 p-2 pl-6 my-2 transition-colors duration-200 border-l-4 {{ if hasPrefix .RouteName "admin" }}dark:text-white border-purple-500{{ else if true }}border-transparent text-gray-400{{ end }}">
|
||||||
class="flex flex-col gap-4 p-2 pl-6 my-2 transition-colors duration-200 border-l-4 {{ if hasPrefix .RouteName "admin" }}
|
<a href="/admin"
|
||||||
dark:text-white border-purple-500
|
class="flex justify-start w-full {{ if not (hasPrefix .RouteName "admin") }}text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{ end }}">
|
||||||
{{ else if true }}
|
{{ template "svg/settings" (dict "Size" 20) }}
|
||||||
border-transparent text-gray-400
|
<span class="mx-4 text-sm font-normal">Admin</span>
|
||||||
{{ end }}"
|
</a>
|
||||||
>
|
{{ if hasPrefix .RouteName "admin" }}
|
||||||
<a
|
<a href="/admin"
|
||||||
href="/admin"
|
style="padding-left: 1.75em"
|
||||||
class="flex justify-start w-full {{ if not (hasPrefix .RouteName "admin") }}
|
class="flex justify-start w-full {{ if not (eq .RouteName "admin") }}text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{ end }}">
|
||||||
text-gray-400 hover:text-gray-800 dark:hover:text-gray-100
|
<span class="mx-4 text-sm font-normal">General</span>
|
||||||
{{ end }}"
|
</a>
|
||||||
>
|
<a href="/admin/import"
|
||||||
{{ template "svg/settings" (dict "Size" 20) }}
|
style="padding-left: 1.75em"
|
||||||
<span class="mx-4 text-sm font-normal">Admin</span>
|
class="flex justify-start w-full {{ if not (eq .RouteName "admin-import") }}text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{ end }}">
|
||||||
</a>
|
<span class="mx-4 text-sm font-normal">Import</span>
|
||||||
{{ if hasPrefix .RouteName "admin" }}
|
</a>
|
||||||
<a
|
<a href="/admin/users"
|
||||||
href="/admin"
|
style="padding-left: 1.75em"
|
||||||
style="padding-left: 1.75em"
|
class="flex justify-start w-full {{ if not (eq .RouteName "admin-users") }}text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{ end }}">
|
||||||
class="flex justify-start w-full {{ if not (eq .RouteName "admin") }}
|
<span class="mx-4 text-sm font-normal">Users</span>
|
||||||
text-gray-400 hover:text-gray-800 dark:hover:text-gray-100
|
</a>
|
||||||
{{ end }}"
|
<a href="/admin/logs"
|
||||||
>
|
style="padding-left: 1.75em"
|
||||||
<span class="mx-4 text-sm font-normal">General</span>
|
class="flex justify-start w-full {{ if not (eq .RouteName "admin-logs") }}text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{ end }}">
|
||||||
</a>
|
<span class="mx-4 text-sm font-normal">Logs</span>
|
||||||
<a
|
</a>
|
||||||
href="/admin/import"
|
{{ end }}
|
||||||
style="padding-left: 1.75em"
|
</div>
|
||||||
class="flex justify-start w-full {{ if not (eq .RouteName "admin-import") }}
|
|
||||||
text-gray-400 hover:text-gray-800 dark:hover:text-gray-100
|
|
||||||
{{ end }}"
|
|
||||||
>
|
|
||||||
<span class="mx-4 text-sm font-normal">Import</span>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="/admin/users"
|
|
||||||
style="padding-left: 1.75em"
|
|
||||||
class="flex justify-start w-full {{ if not (eq .RouteName "admin-users") }}
|
|
||||||
text-gray-400 hover:text-gray-800 dark:hover:text-gray-100
|
|
||||||
{{ end }}"
|
|
||||||
>
|
|
||||||
<span class="mx-4 text-sm font-normal">Users</span>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="/admin/logs"
|
|
||||||
style="padding-left: 1.75em"
|
|
||||||
class="flex justify-start w-full {{ if not (eq .RouteName "admin-logs") }}
|
|
||||||
text-gray-400 hover:text-gray-800 dark:hover:text-gray-100
|
|
||||||
{{ end }}"
|
|
||||||
>
|
|
||||||
<span class="mx-4 text-sm font-normal">Logs</span>
|
|
||||||
</a>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a class="flex flex-col gap-2 justify-center items-center p-6 w-full absolute bottom-0 text-black dark:text-white"
|
||||||
class="flex flex-col gap-2 justify-center items-center p-6 w-full absolute bottom-0 text-black dark:text-white"
|
target="_blank"
|
||||||
target="_blank"
|
href="https://gitea.va.reichard.io/evan/AnthoLume">
|
||||||
href="https://gitea.va.reichard.io/evan/AnthoLume"
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
class="text-black dark:text-white"
|
||||||
<svg
|
height="20"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
viewBox="0 0 219 92"
|
||||||
class="text-black dark:text-white"
|
fill="currentColor">
|
||||||
height="20"
|
|
||||||
viewBox="0 0 219 92"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<defs>
|
<defs>
|
||||||
<clipPath id="a">
|
<clipPath id="a">
|
||||||
<path d="M159 .79h25V69h-25Zm0 0" />
|
<path d="M159 .79h25V69h-25Zm0 0" />
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="b">
|
<clipPath id="b">
|
||||||
<path d="M183 9h35.371v60H183Zm0 0" />
|
<path d="M183 9h35.371v60H183Zm0 0" />
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<clipPath id="c">
|
<clipPath id="c">
|
||||||
<path d="M0 .79h92V92H0Zm0 0" />
|
<path d="M0 .79h92V92H0Zm0 0" />
|
||||||
</clipPath>
|
</clipPath>
|
||||||
</defs>
|
</defs>
|
||||||
<path
|
<path style="stroke: none; fill-rule: nonzero; fill-opacity: 1" d="M130.871 31.836c-4.785 0-8.351 2.352-8.351 8.008 0 4.261 2.347 7.222 8.093 7.222 4.871 0 8.18-2.867 8.18-7.398 0-5.133-2.961-7.832-7.922-7.832Zm-9.57 39.95c-1.133 1.39-2.262 2.87-2.262 4.612 0 3.48 4.434 4.524 10.527 4.524 5.051 0 11.926-.352 11.926-5.043 0-2.793-3.308-2.965-7.488-3.227Zm25.761-39.688c1.563 2.004 3.22 4.789 3.22 8.793 0 9.656-7.571 15.316-18.536 15.316-2.789 0-5.312-.348-6.879-.785l-2.87 4.613 8.526.52c15.059.96 23.934 1.398 23.934 12.968 0 10.008-8.789 15.665-23.934 15.665-15.75 0-21.757-4.004-21.757-10.88 0-3.917 1.742-6 4.789-8.878-2.875-1.211-3.828-3.387-3.828-5.739 0-1.914.953-3.656 2.523-5.312 1.566-1.652 3.305-3.305 5.395-5.219-4.262-2.09-7.485-6.617-7.485-13.058 0-10.008 6.613-16.88 19.93-16.88 3.742 0 6.004.344 8.008.872h16.972v7.394l-8.007.61" />
|
||||||
style="stroke: none; fill-rule: nonzero; fill-opacity: 1"
|
|
||||||
d="M130.871 31.836c-4.785 0-8.351 2.352-8.351 8.008 0 4.261 2.347 7.222 8.093 7.222 4.871 0 8.18-2.867 8.18-7.398 0-5.133-2.961-7.832-7.922-7.832Zm-9.57 39.95c-1.133 1.39-2.262 2.87-2.262 4.612 0 3.48 4.434 4.524 10.527 4.524 5.051 0 11.926-.352 11.926-5.043 0-2.793-3.308-2.965-7.488-3.227Zm25.761-39.688c1.563 2.004 3.22 4.789 3.22 8.793 0 9.656-7.571 15.316-18.536 15.316-2.789 0-5.312-.348-6.879-.785l-2.87 4.613 8.526.52c15.059.96 23.934 1.398 23.934 12.968 0 10.008-8.789 15.665-23.934 15.665-15.75 0-21.757-4.004-21.757-10.88 0-3.917 1.742-6 4.789-8.878-2.875-1.211-3.828-3.387-3.828-5.739 0-1.914.953-3.656 2.523-5.312 1.566-1.652 3.305-3.305 5.395-5.219-4.262-2.09-7.485-6.617-7.485-13.058 0-10.008 6.613-16.88 19.93-16.88 3.742 0 6.004.344 8.008.872h16.972v7.394l-8.007.61"
|
|
||||||
/>
|
|
||||||
<g clip-path="url(#a)">
|
<g clip-path="url(#a)">
|
||||||
<path
|
<path style="stroke: none; fill-rule: nonzero; fill-opacity: 1" d="M170.379 16.281c-4.961 0-7.832-2.87-7.832-7.836 0-4.957 2.871-7.656 7.832-7.656 5.05 0 7.922 2.7 7.922 7.656 0 4.965-2.871 7.836-7.922 7.836Zm-11.227 52.305V61.71l4.438-.606c1.219-.175 1.394-.437 1.394-1.746V33.773c0-.953-.261-1.566-1.132-1.824l-4.7-1.656.957-7.047h18.016V59.36c0 1.399.086 1.57 1.395 1.746l4.437.606v6.875h-24.805" />
|
||||||
style="stroke: none; fill-rule: nonzero; fill-opacity: 1"
|
|
||||||
d="M170.379 16.281c-4.961 0-7.832-2.87-7.832-7.836 0-4.957 2.871-7.656 7.832-7.656 5.05 0 7.922 2.7 7.922 7.656 0 4.965-2.871 7.836-7.922 7.836Zm-11.227 52.305V61.71l4.438-.606c1.219-.175 1.394-.437 1.394-1.746V33.773c0-.953-.261-1.566-1.132-1.824l-4.7-1.656.957-7.047h18.016V59.36c0 1.399.086 1.57 1.395 1.746l4.437.606v6.875h-24.805"
|
|
||||||
/>
|
|
||||||
</g>
|
</g>
|
||||||
<g clip-path="url(#b)">
|
<g clip-path="url(#b)">
|
||||||
<path
|
<path style="stroke: none; fill-rule: nonzero; fill-opacity: 1" d="M218.371 65.21c-3.742 1.825-9.223 3.481-14.187 3.481-10.356 0-14.27-4.175-14.27-14.015V31.879c0-.524 0-.871-.7-.871h-6.093v-7.746c7.664-.871 10.707-4.703 11.664-14.188h8.27v12.36c0 .609 0 .87.695.87h12.27v8.704h-12.965v20.797c0 5.136 1.218 7.136 5.918 7.136 2.437 0 4.96-.609 7.047-1.39l2.351 7.66" />
|
||||||
style="stroke: none; fill-rule: nonzero; fill-opacity: 1"
|
|
||||||
d="M218.371 65.21c-3.742 1.825-9.223 3.481-14.187 3.481-10.356 0-14.27-4.175-14.27-14.015V31.879c0-.524 0-.871-.7-.871h-6.093v-7.746c7.664-.871 10.707-4.703 11.664-14.188h8.27v12.36c0 .609 0 .87.695.87h12.27v8.704h-12.965v20.797c0 5.136 1.218 7.136 5.918 7.136 2.437 0 4.96-.609 7.047-1.39l2.351 7.66"
|
|
||||||
/>
|
|
||||||
</g>
|
</g>
|
||||||
<g clip-path="url(#c)">
|
<g clip-path="url(#c)">
|
||||||
<path
|
<path style="stroke: none; fill-rule: nonzero; fill-opacity: 1" d="M89.422 42.371 49.629 2.582a5.868 5.868 0 0 0-8.3 0l-8.263 8.262 10.48 10.484a6.965 6.965 0 0 1 7.173 1.668 6.98 6.98 0 0 1 1.656 7.215l10.102 10.105a6.963 6.963 0 0 1 7.214 1.657 6.976 6.976 0 0 1 0 9.875 6.98 6.98 0 0 1-9.879 0 6.987 6.987 0 0 1-1.519-7.594l-9.422-9.422v24.793a6.979 6.979 0 0 1 1.848 1.32 6.988 6.988 0 0 1 0 9.88c-2.73 2.726-7.153 2.726-9.875 0a6.98 6.98 0 0 1 0-9.88 6.893 6.893 0 0 1 2.285-1.523V34.398a6.893 6.893 0 0 1-2.285-1.523 6.988 6.988 0 0 1-1.508-7.637L29.004 14.902 1.719 42.187a5.868 5.868 0 0 0 0 8.301l39.793 39.793a5.868 5.868 0 0 0 8.3 0l39.61-39.605a5.873 5.873 0 0 0 0-8.305" />
|
||||||
style="stroke: none; fill-rule: nonzero; fill-opacity: 1"
|
|
||||||
d="M89.422 42.371 49.629 2.582a5.868 5.868 0 0 0-8.3 0l-8.263 8.262 10.48 10.484a6.965 6.965 0 0 1 7.173 1.668 6.98 6.98 0 0 1 1.656 7.215l10.102 10.105a6.963 6.963 0 0 1 7.214 1.657 6.976 6.976 0 0 1 0 9.875 6.98 6.98 0 0 1-9.879 0 6.987 6.987 0 0 1-1.519-7.594l-9.422-9.422v24.793a6.979 6.979 0 0 1 1.848 1.32 6.988 6.988 0 0 1 0 9.88c-2.73 2.726-7.153 2.726-9.875 0a6.98 6.98 0 0 1 0-9.88 6.893 6.893 0 0 1 2.285-1.523V34.398a6.893 6.893 0 0 1-2.285-1.523 6.988 6.988 0 0 1-1.508-7.637L29.004 14.902 1.719 42.187a5.868 5.868 0 0 0 0 8.301l39.793 39.793a5.868 5.868 0 0 0 8.3 0l39.61-39.605a5.873 5.873 0 0 0 0-8.305"
|
|
||||||
/>
|
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="text-xs">{{ .Config.Version }}</span>
|
<span class="text-xs">{{ .Config.Version }}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-xl font-bold dark:text-white px-6 lg:ml-44">
|
<h1 class="text-xl font-bold dark:text-white px-6 lg:ml-44">{{ block "header" . }}{{ end }}</h1>
|
||||||
{{ block "header" . }}{{ end }}
|
|
||||||
</h1>
|
|
||||||
<div class="relative flex items-center justify-end w-full p-4 space-x-4">
|
<div class="relative flex items-center justify-end w-full p-4 space-x-4">
|
||||||
<a href="#" class="relative block text-gray-800 dark:text-gray-200"
|
<a href="#" class="relative block text-gray-800 dark:text-gray-200">{{ template "svg/user" (dict "Size" 20) }}</a>
|
||||||
>{{ template "svg/user" (dict "Size" 20) }}</a
|
|
||||||
>
|
|
||||||
<input type="checkbox" id="user-dropdown-button" class="hidden" />
|
<input type="checkbox" id="user-dropdown-button" class="hidden" />
|
||||||
<div
|
<div id="user-dropdown"
|
||||||
id="user-dropdown"
|
class="transition duration-200 z-20 absolute right-4 top-16 pt-4">
|
||||||
class="transition duration-200 z-20 absolute right-4 top-16 pt-4"
|
<div class="w-40 origin-top-right bg-white rounded-md shadow-lg dark:shadow-gray-800 dark:bg-gray-700 ring-1 ring-black ring-opacity-5">
|
||||||
>
|
<div class="py-1"
|
||||||
<div
|
role="menu"
|
||||||
class="w-40 origin-top-right bg-white rounded-md shadow-lg dark:shadow-gray-800 dark:bg-gray-700 ring-1 ring-black ring-opacity-5"
|
aria-orientation="vertical"
|
||||||
>
|
aria-labelledby="options-menu">
|
||||||
<div
|
<a href="/settings"
|
||||||
class="py-1"
|
class="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600"
|
||||||
role="menu"
|
role="menuitem">
|
||||||
aria-orientation="vertical"
|
|
||||||
aria-labelledby="options-menu"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="/settings"
|
|
||||||
class="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600"
|
|
||||||
role="menuitem"
|
|
||||||
>
|
|
||||||
<span class="flex flex-col">
|
<span class="flex flex-col">
|
||||||
<span>Settings</span>
|
<span>Settings</span>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a href="/local"
|
||||||
href="/local"
|
class="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600"
|
||||||
class="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600"
|
role="menuitem">
|
||||||
role="menuitem"
|
|
||||||
>
|
|
||||||
<span class="flex flex-col">
|
<span class="flex flex-col">
|
||||||
<span>Offline</span>
|
<span>Offline</span>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a href="/logout"
|
||||||
href="/logout"
|
class="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600"
|
||||||
class="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600"
|
role="menuitem">
|
||||||
role="menuitem"
|
|
||||||
>
|
|
||||||
<span class="flex flex-col">
|
<span class="flex flex-col">
|
||||||
<span>Logout</span>
|
<span>Logout</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -398,32 +283,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label for="user-dropdown-button">
|
<label for="user-dropdown-button">
|
||||||
<div
|
<div class="flex items-center gap-2 text-gray-500 dark:text-white text-md py-4 cursor-pointer">
|
||||||
class="flex items-center gap-2 text-gray-500 dark:text-white text-md py-4 cursor-pointer"
|
|
||||||
>
|
|
||||||
<span>{{ .Authorization.UserName }}</span>
|
<span>{{ .Authorization.UserName }}</span>
|
||||||
<span class="text-gray-800 dark:text-gray-200"
|
<span class="text-gray-800 dark:text-gray-200">{{ template "svg/dropdown" (dict "Size" 20) }}</span>
|
||||||
>{{ template "svg/dropdown" (dict "Size" 20) }}</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<main class="relative overflow-hidden">
|
<main class="relative overflow-hidden">
|
||||||
<div
|
<div id="container" class="h-[100dvh] px-4 overflow-auto md:px-6 lg:ml-48">{{ block "content" . }}{{ end }}</div>
|
||||||
id="container"
|
|
||||||
class="h-[100dvh] px-4 overflow-auto md:px-6 lg:ml-48"
|
|
||||||
>
|
|
||||||
{{ block "content" . }}{{ end }}
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
<div class="absolute right-4 bottom-4">
|
|
||||||
{{ block "notifications" . }}{{ end }}
|
|
||||||
<!--
|
|
||||||
<div class="w-72 p-4 bg-red-500 rounded-xl">
|
|
||||||
<span>User Deleted</span>
|
|
||||||
</div>
|
|
||||||
-->
|
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
{{ template "base" . }}
|
|
||||||
{{ define "title" }}Activity{{ end }}
|
|
||||||
{{ define "header" }}<a href="./activity">Activity</a>{{ end }}
|
|
||||||
{{ define "content" }}
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<div class="inline-block min-w-full overflow-hidden rounded shadow">
|
|
||||||
<!-- Table Component - Utilizes Template "table-cell" -->
|
|
||||||
{{ template "component/table" (dict
|
|
||||||
"Columns" (slice "Document" "Time" "Duration" "Percent")
|
|
||||||
"Keys" (slice "Document" "StartTime" "Duration" "EndPercentage")
|
|
||||||
"Rows" .Data
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
<!-- Table Cell Definition -->
|
|
||||||
{{ define "table-cell" }}
|
|
||||||
{{ if eq .Name "Document" }}
|
|
||||||
<a href="./documents/{{ .Data.DocumentID }}"
|
|
||||||
>{{ .Data.Author }} - {{ .Data.Title }}</a
|
|
||||||
>
|
|
||||||
{{ else if eq .Name "EndPercentage" }}
|
|
||||||
{{ index (fields .Data) .Name }}%
|
|
||||||
{{ else }}
|
|
||||||
{{ index (fields .Data) .Name }}
|
|
||||||
{{ end }}
|
|
||||||
{{ end }}
|
|
||||||
@@ -1,21 +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 }}
|
|
||||||
@@ -1,50 +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>
|
|
||||||
@@ -1,42 +1,22 @@
|
|||||||
<div
|
<div class="absolute -translate-y-1/2 p-4 m-auto bg-gray-700 dark:bg-gray-300 rounded-lg shadow w-full text-black dark:text-white">
|
||||||
class="absolute -translate-y-1/2 p-4 m-auto bg-gray-700 dark:bg-gray-300 rounded-lg shadow w-full text-black dark:text-white"
|
<span class="inline-flex gap-2 items-center font-medium text-xs inline-block py-1 px-2 uppercase rounded-full {{ if .Error }} bg-red-500 {{ else if true }} bg-green-600 {{ end }}">
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="inline-flex gap-2 items-center font-medium text-xs inline-block py-1 px-2 uppercase rounded-full {{ if .Error }}
|
|
||||||
bg-red-500
|
|
||||||
{{ else if true }}
|
|
||||||
bg-green-600
|
|
||||||
{{ end }}"
|
|
||||||
>
|
|
||||||
{{ if and (ne .Progress 100) (not .Error) }}
|
{{ if and (ne .Progress 100) (not .Error) }}
|
||||||
{{ template "svg/loading" (dict "Size" 16) }}
|
{{ template "svg/loading" (dict "Size" 16) }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ .Message }}
|
{{ .Message }}
|
||||||
</span>
|
</span>
|
||||||
<div class="flex flex-col gap-2 mt-2">
|
<div class="flex flex-col gap-2 mt-2">
|
||||||
<div class="relative w-full h-4 bg-gray-300 dark:bg-gray-700 rounded-full">
|
<div class="relative w-full h-4 bg-gray-300 dark:bg-gray-700 rounded-full">
|
||||||
{{ if .Error }}
|
{{ if .Error }}
|
||||||
<div
|
<div class="absolute h-full bg-red-500 rounded-full" style="width: 100%"></div>
|
||||||
class="absolute h-full bg-red-500 rounded-full"
|
<p class="absolute w-full h-full font-bold text-center text-xs">ERROR</p>
|
||||||
style="width: 100%"
|
|
||||||
></div>
|
|
||||||
<p class="absolute w-full h-full font-bold text-center text-xs">
|
|
||||||
ERROR
|
|
||||||
</p>
|
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<div
|
<div class="absolute h-full bg-green-600 rounded-full"
|
||||||
class="absolute h-full bg-green-600 rounded-full"
|
style="width: {{ .Progress }}%"></div>
|
||||||
style="width: {{ .Progress }}%"
|
<p class="absolute w-full h-full font-bold text-center text-xs">{{ .Progress }}%</p>
|
||||||
></div>
|
|
||||||
<p class="absolute w-full h-full font-bold text-center text-xs">
|
|
||||||
{{ .Progress }}%
|
|
||||||
</p>
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a href="{{ .ButtonHref }}"
|
||||||
href="{{ .ButtonHref }}"
|
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">{{ .ButtonText }}</a>
|
||||||
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"
|
|
||||||
>{{ .ButtonText }}</a
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 }}
|
|
||||||
@@ -1,32 +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>
|
|
||||||
@@ -1,147 +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 }}
|
|
||||||
@@ -1,54 +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>
|
|
||||||
@@ -1,32 +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">
|
|
||||||
{{ template "table-cell" (dict "Data" $row "Name" $key ) }}
|
|
||||||
</td>
|
|
||||||
{{ end }}
|
|
||||||
</tr>
|
|
||||||
{{ end }}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
@@ -2,27 +2,43 @@
|
|||||||
{{ define "title" }}Activity{{ end }}
|
{{ define "title" }}Activity{{ end }}
|
||||||
{{ define "header" }}<a href="./activity">Activity</a>{{ end }}
|
{{ define "header" }}<a href="./activity">Activity</a>{{ end }}
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<div class="inline-block min-w-full overflow-hidden rounded shadow">
|
<div class="inline-block min-w-full overflow-hidden rounded shadow">
|
||||||
<!-- Table Component - Utilizes Template "table-cell" -->
|
<table class="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm">
|
||||||
{{ template "component/table" (dict
|
<thead class="text-gray-800 dark:text-gray-400">
|
||||||
"Columns" (slice "Document" "Time" "Duration" "Percent")
|
<tr>
|
||||||
"Keys" (slice "Document" "StartTime" "Duration" "EndPercentage")
|
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Document</th>
|
||||||
"Rows" .Data
|
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Time</th>
|
||||||
)
|
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Duration</th>
|
||||||
}}
|
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Percent</th>
|
||||||
</div>
|
</tr>
|
||||||
</div>
|
</thead>
|
||||||
{{ end }}
|
<tbody class="text-black dark:text-white">
|
||||||
<!-- Table Cell Definition -->
|
{{ if not .Data }}
|
||||||
{{ define "table-cell" }}
|
<tr>
|
||||||
{{ if eq .Name "Document" }}
|
<td class="text-center p-3" colspan="4">No Results</td>
|
||||||
<a href="./documents/{{ .Data.DocumentID }}"
|
</tr>
|
||||||
>{{ .Data.Author }} - {{ .Data.Title }}</a
|
{{ end }}
|
||||||
>
|
{{ range $activity := .Data }}
|
||||||
{{ else if eq .Name "EndPercentage" }}
|
<tr>
|
||||||
{{ index (fields .Data) .Name }}%
|
<td class="p-3 border-b border-gray-200">
|
||||||
{{ else }}
|
<a href="./documents/{{ $activity.DocumentID }}">{{ $activity.Author }} - {{ $activity.Title }}
|
||||||
{{ index (fields .Data) .Name }}
|
</p>
|
||||||
{{ end }}
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="p-3 border-b border-gray-200">
|
||||||
|
<p>{{ $activity.StartTime }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="p-3 border-b border-gray-200">
|
||||||
|
<p>{{ $activity.Duration }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="p-3 border-b border-gray-200">
|
||||||
|
<p>{{ $activity.EndPercentage }}%</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
@@ -1,64 +1,46 @@
|
|||||||
{{ template "base" . }}
|
{{ template "base" . }}
|
||||||
{{ define "title" }}Admin - Import Results{{ end }}
|
{{ define "title" }}Admin - Import Results{{ end }}
|
||||||
{{ define "header" }}
|
{{ define "header" }}<a class="whitespace-pre" href="../admin">Admin - Import Results</a>{{ end }}
|
||||||
<a class="whitespace-pre" href="../admin">Admin - Import Results</a>
|
|
||||||
{{ end }}
|
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<div class="inline-block min-w-full overflow-hidden rounded shadow">
|
<div class="inline-block min-w-full overflow-hidden rounded shadow">
|
||||||
<table
|
<table class="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm">
|
||||||
class="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm"
|
<thead class="text-gray-800 dark:text-gray-400">
|
||||||
>
|
<tr>
|
||||||
<thead class="text-gray-800 dark:text-gray-400">
|
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Document</th>
|
||||||
<tr>
|
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Status</th>
|
||||||
<th
|
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Error</th>
|
||||||
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
</tr>
|
||||||
>
|
</thead>
|
||||||
Document
|
<tbody class="text-black dark:text-white">
|
||||||
</th>
|
{{ if not .Data }}
|
||||||
<th
|
<tr>
|
||||||
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
<td class="text-center p-3" colspan="4">No Results</td>
|
||||||
>
|
</tr>
|
||||||
Status
|
{{ end }}
|
||||||
</th>
|
{{ range $result := .Data }}
|
||||||
<th
|
<tr>
|
||||||
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
<td class="p-3 border-b border-gray-200 grid"
|
||||||
>
|
style="grid-template-columns: 4rem auto">
|
||||||
Error
|
<span class="text-gray-800 dark:text-gray-400">Name:</span>
|
||||||
</th>
|
{{ if (eq $result.ID "") }}
|
||||||
</tr>
|
<span>N/A</span>
|
||||||
</thead>
|
{{ else }}
|
||||||
<tbody class="text-black dark:text-white">
|
<a href="../documents/{{ $result.ID }}">{{ $result.Name }}</a>
|
||||||
{{ if not .Data }}
|
{{ end }}
|
||||||
<tr>
|
<span class="text-gray-800 dark:text-gray-400">File:</span>
|
||||||
<td class="text-center p-3" colspan="4">No Results</td>
|
<span>{{ $result.Path }}</span>
|
||||||
</tr>
|
</td>
|
||||||
{{ end }}
|
<td class="p-3 border-b border-gray-200">
|
||||||
{{ range $result := .Data }}
|
<p>{{ $result.Status }}</p>
|
||||||
<tr>
|
</td>
|
||||||
<td
|
<td class="p-3 border-b border-gray-200">
|
||||||
class="p-3 border-b border-gray-200 grid"
|
<p>{{ $result.Error }}</p>
|
||||||
style="grid-template-columns: 4rem auto"
|
</td>
|
||||||
>
|
</tr>
|
||||||
<span class="text-gray-800 dark:text-gray-400">Name:</span>
|
{{ end }}
|
||||||
{{ if (eq $result.ID "") }}
|
</tbody>
|
||||||
<span>N/A</span>
|
</table>
|
||||||
{{ else }}
|
|
||||||
<a href="../documents/{{ $result.ID }}">{{ $result.Name }}</a>
|
|
||||||
{{ end }}
|
|
||||||
<span class="text-gray-800 dark:text-gray-400">File:</span>
|
|
||||||
<span>{{ $result.Path }}</span>
|
|
||||||
</td>
|
|
||||||
<td class="p-3 border-b border-gray-200">
|
|
||||||
<p>{{ $result.Status }}</p>
|
|
||||||
</td>
|
|
||||||
<td class="p-3 border-b border-gray-200">
|
|
||||||
<p>{{ $result.Error }}</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{{ end }}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
@@ -1,111 +1,79 @@
|
|||||||
{{ template "base" . }}
|
{{ template "base" . }}
|
||||||
{{ define "title" }}Admin - Import{{ end }}
|
{{ define "title" }}Admin - Import{{ end }}
|
||||||
{{ define "header" }}
|
{{ define "header" }}<a class="whitespace-pre" href="../admin">Admin - Import</a>{{ end }}
|
||||||
<a class="whitespace-pre" href="../admin">Admin - Import</a>
|
|
||||||
{{ end }}
|
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<div class="inline-block min-w-full overflow-hidden rounded shadow">
|
<div class="inline-block min-w-full overflow-hidden rounded shadow">
|
||||||
{{ if .SelectedDirectory }}
|
{{ if .SelectedDirectory }}
|
||||||
<div
|
<div class="flex flex-col grow gap-2 p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white">
|
||||||
class="flex flex-col grow gap-2 p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
<p class="text-lg font-semibold text-gray-500">Selected Import Directory</p>
|
||||||
>
|
<form class="flex gap-4 flex-col" action="./import" method="POST">
|
||||||
<p class="text-lg font-semibold text-gray-500">
|
<input type="text"
|
||||||
Selected Import Directory
|
name="directory"
|
||||||
</p>
|
value="{{ .SelectedDirectory }}"
|
||||||
<form class="flex gap-4 flex-col" action="./import" method="POST">
|
class="hidden" />
|
||||||
<input
|
<div class="flex justify-between gap-4 w-full">
|
||||||
type="text"
|
<div class="flex gap-4 items-center">
|
||||||
name="directory"
|
<span>{{ template "svg/import" }}</span>
|
||||||
value="{{ .SelectedDirectory }}"
|
<p class="font-medium text-lg break-all">{{ .SelectedDirectory }}</p>
|
||||||
class="hidden"
|
</div>
|
||||||
/>
|
<div class="flex flex-col justify-around gap-2 mr-4">
|
||||||
<div class="flex justify-between gap-4 w-full">
|
<div class="inline-flex gap-2 items-center">
|
||||||
<div class="flex gap-4 items-center">
|
<input checked type="radio" id="direct" name="type" value="DIRECT" />
|
||||||
<span>{{ template "svg/import" }}</span>
|
<label for="direct">Direct</label>
|
||||||
<p class="font-medium text-lg break-all">
|
|
||||||
{{ .SelectedDirectory }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col justify-around gap-2 mr-4">
|
|
||||||
<div class="inline-flex gap-2 items-center">
|
|
||||||
<input
|
|
||||||
checked
|
|
||||||
type="radio"
|
|
||||||
id="direct"
|
|
||||||
name="type"
|
|
||||||
value="DIRECT"
|
|
||||||
/>
|
|
||||||
<label for="direct">Direct</label>
|
|
||||||
</div>
|
|
||||||
<div class="inline-flex gap-2 items-center">
|
|
||||||
<input type="radio" id="copy" name="type" value="COPY" />
|
|
||||||
<label for="copy">Copy</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div class="inline-flex gap-2 items-center">
|
||||||
type="submit"
|
<input type="radio" id="copy" name="type" value="COPY" />
|
||||||
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"
|
<label for="copy">Copy</label>
|
||||||
>
|
</div>
|
||||||
<span class="w-full">Import Directory</span>
|
</div>
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
<button type="submit"
|
||||||
{{ if not .SelectedDirectory }}
|
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">
|
||||||
<table
|
<span class="w-full">Import Directory</span>
|
||||||
class="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm"
|
</button>
|
||||||
>
|
</form>
|
||||||
<thead class="text-gray-800 dark:text-gray-400">
|
|
||||||
<tr>
|
|
||||||
<th
|
|
||||||
class="p-3 font-normal text-left border-b border-gray-200 dark:border-gray-800 w-12"
|
|
||||||
></th>
|
|
||||||
<th
|
|
||||||
class="p-3 font-normal text-left border-b border-gray-200 dark:border-gray-800 break-all"
|
|
||||||
>
|
|
||||||
{{ .CurrentPath }}
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="text-black dark:text-white">
|
|
||||||
{{ if not (eq .CurrentPath "/") }}
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
class="p-3 border-b border-gray-200 text-gray-800 dark:text-gray-400"
|
|
||||||
></td>
|
|
||||||
<td class="p-3 border-b border-gray-200">
|
|
||||||
<a href="./import?directory={{ $.CurrentPath }}/../">
|
|
||||||
<p>../</p>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{{ end }}
|
|
||||||
{{ if not .Data }}
|
|
||||||
<tr>
|
|
||||||
<td class="text-center p-3" colspan="2">No Folders</td>
|
|
||||||
</tr>
|
|
||||||
{{ end }}
|
|
||||||
{{ range $item := .Data }}
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
class="p-3 border-b border-gray-200 text-gray-800 dark:text-gray-400"
|
|
||||||
>
|
|
||||||
<a href="./import?select={{ $.CurrentPath }}/{{ $item }}"
|
|
||||||
>{{ template "svg/import" }}</a
|
|
||||||
>
|
|
||||||
</td>
|
|
||||||
<td class="p-3 border-b border-gray-200">
|
|
||||||
<a href="./import?directory={{ $.CurrentPath }}/{{ $item }}">
|
|
||||||
<p>{{ $item }}</p>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{{ end }}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ if not .SelectedDirectory }}
|
||||||
|
<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>
|
||||||
|
<th class="p-3 font-normal text-left border-b border-gray-200 dark:border-gray-800 w-12"></th>
|
||||||
|
<th class="p-3 font-normal text-left border-b border-gray-200 dark:border-gray-800 break-all">{{ .CurrentPath }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-black dark:text-white">
|
||||||
|
{{ if not (eq .CurrentPath "/") }}
|
||||||
|
<tr>
|
||||||
|
<td class="p-3 border-b border-gray-200 text-gray-800 dark:text-gray-400"></td>
|
||||||
|
<td class="p-3 border-b border-gray-200">
|
||||||
|
<a href="./import?directory={{$.CurrentPath}}/../">
|
||||||
|
<p>../</p>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
{{ if not .Data }}
|
||||||
|
<tr>
|
||||||
|
<td class="text-center p-3" colspan="2">No Folders</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
{{ range $item := .Data }}
|
||||||
|
<tr>
|
||||||
|
<td class="p-3 border-b border-gray-200 text-gray-800 dark:text-gray-400">
|
||||||
|
<a href="./import?select={{ $.CurrentPath }}/{{ $item }}">{{ template "svg/import" }}</a>
|
||||||
|
</td>
|
||||||
|
<td class="p-3 border-b border-gray-200">
|
||||||
|
<a href="./import?directory={{ $.CurrentPath }}/{{ $item }}">
|
||||||
|
<p>{{ $item }}</p>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
@@ -1,47 +1,34 @@
|
|||||||
{{ template "base" . }}
|
{{ template "base" . }}
|
||||||
{{ define "title" }}Admin - Logs{{ end }}
|
{{ define "title" }}Admin - Logs{{ end }}
|
||||||
{{ define "header" }}
|
{{ define "header" }}<a class="whitespace-pre" href="../admin">Admin - Logs</a>{{ end }}
|
||||||
<a class="whitespace-pre" href="../admin">Admin - Logs</a>
|
|
||||||
{{ end }}
|
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<div
|
<div class="flex flex-col gap-2 grow p-4 mb-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white">
|
||||||
class="flex flex-col gap-2 grow p-4 mb-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
<form class="flex gap-4 flex-col lg:flex-row" action="./logs" method="GET">
|
||||||
>
|
<div class="flex flex-col w-full grow">
|
||||||
<form class="flex gap-4 flex-col lg:flex-row" action="./logs" method="GET">
|
<div class="flex relative">
|
||||||
<div class="flex flex-col w-full grow">
|
<span class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm">
|
||||||
<div class="flex relative">
|
{{ template "svg/search2" (dict "Size" 15) }}
|
||||||
<span
|
</span>
|
||||||
class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
<input type="text"
|
||||||
>
|
id="filter"
|
||||||
{{ template "svg/search2" (dict "Size" 15) }}
|
name="filter"
|
||||||
</span>
|
value="{{ .Filter }}"
|
||||||
<input
|
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-2 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
||||||
type="text"
|
placeholder="JQ Filter" />
|
||||||
id="filter"
|
|
||||||
name="filter"
|
|
||||||
value="{{ .Filter }}"
|
|
||||||
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-2 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
|
||||||
placeholder="JQ Filter"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="lg:w-60">
|
</div>
|
||||||
{{ template "component/button" (dict
|
<button type="submit"
|
||||||
"Title" "Filter"
|
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">
|
||||||
"Variant" "Secondary"
|
<span class="w-full">Filter</span>
|
||||||
)
|
</button>
|
||||||
}}
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
<!-- Required for iOS "Hover" Events (onclick) -->
|
||||||
</div>
|
<div onclick
|
||||||
<!-- Required for iOS "Hover" Events (onclick) -->
|
class="flex flex-col-reverse text-black dark:text-white w-full overflow-scroll"
|
||||||
<div
|
style="font-family: monospace">
|
||||||
onclick
|
{{ range $log := .Data }}
|
||||||
class="flex flex-col-reverse text-black dark:text-white w-full overflow-scroll"
|
<span class="whitespace-nowrap hover:whitespace-pre">{{ $log }}</span>
|
||||||
style="font-family: monospace"
|
{{ end }}
|
||||||
>
|
</div>
|
||||||
{{ range $log := .Data }}
|
|
||||||
<span class="whitespace-nowrap hover:whitespace-pre">{{ $log }}</span>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
type="submit">Create</button>
|
type="submit">Create</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="min-w-full overflow-scroll rounded shadow">
|
<div class="min-w-full overflow-hidden rounded shadow">
|
||||||
<table class="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm">
|
<table class="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm">
|
||||||
<thead class="text-gray-800 dark:text-gray-400">
|
<thead class="text-gray-800 dark:text-gray-400">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -50,27 +50,12 @@
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
{{ range $user := .Data }}
|
{{ range $user := .Data }}
|
||||||
<tr>
|
<tr>
|
||||||
<!-- User Deletion -->
|
<td class="p-3 border-b border-gray-200 text-gray-800 dark:text-gray-400 cursor-pointer">
|
||||||
<td class="p-3 border-b border-gray-200 text-gray-800 dark:text-gray-400 cursor-pointer relative">
|
{{ template "svg/delete" }}
|
||||||
<label for="delete-{{ $user.ID }}-button" class="cursor-pointer">{{ template "svg/delete" }}</label>
|
|
||||||
<input type="checkbox"
|
|
||||||
id="delete-{{ $user.ID }}-button"
|
|
||||||
class="hidden css-button" />
|
|
||||||
<div class="absolute z-30 top-1.5 left-10 p-1.5 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="./users"
|
|
||||||
class="text-black dark:text-white text-sm w-40">
|
|
||||||
<input type="hidden" id="operation" name="operation" value="DELETE" />
|
|
||||||
<input type="hidden" id="user" name="user" value="{{ $user.ID }}" />
|
|
||||||
{{ template "component/button" (dict "Title" (printf "Delete (%s)" $user.ID )) }}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
<!-- User ID -->
|
|
||||||
<td class="p-3 border-b border-gray-200">
|
<td class="p-3 border-b border-gray-200">
|
||||||
<p>{{ $user.ID }}</p>
|
<p>{{ $user.ID }}</p>
|
||||||
</td>
|
</td>
|
||||||
<!-- User Password Change -->
|
|
||||||
<td class="border-b border-gray-200 relative px-3">
|
<td class="border-b border-gray-200 relative px-3">
|
||||||
<label for="edit-{{ $user.ID }}-button" class="cursor-pointer">
|
<label for="edit-{{ $user.ID }}-button" class="cursor-pointer">
|
||||||
<span 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"
|
<span 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"
|
||||||
@@ -79,54 +64,36 @@
|
|||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
id="edit-{{ $user.ID }}-button"
|
id="edit-{{ $user.ID }}-button"
|
||||||
class="hidden css-button" />
|
class="hidden css-button" />
|
||||||
<div class="absolute z-30 top-1 left-16 ml-2 p-1.5 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-1.5 left-16 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="./users"
|
action="./users"
|
||||||
class="flex flex gap-2 text-black dark:text-white text-sm">
|
class="flex flex gap-2 text-black dark:text-white text-sm">
|
||||||
<input type="hidden" id="operation" name="operation" value="UPDATE" />
|
<input type="text"
|
||||||
<input type="hidden" id="user" name="user" value="{{ $user.ID }}" />
|
id="operation"
|
||||||
|
name="operation"
|
||||||
|
value="UPDATE"
|
||||||
|
class="hidden" />
|
||||||
<input type="password"
|
<input type="password"
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
placeholder="{{ printf "Password (%s)" $user.ID }}"
|
placeholder="Password"
|
||||||
class="p-1.5 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"
|
<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">Change</button>
|
type="submit">Change</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<!-- User Role -->
|
<td class="p-3 border-b border-gray-200 text-center min-w-40">
|
||||||
<td class="flex gap-2 justify-center p-3 border-b border-gray-200 text-center min-w-40">
|
<span class="px-2 py-1 rounded-md text-white dark:text-black {{ if $user.Admin }}bg-gray-800 dark:bg-gray-100{{ else }}bg-gray-400 dark:bg-gray-600 cursor-pointer{{ end }}">admin</span>
|
||||||
<!-- Set Admin & User Styles -->
|
<span class="px-2 py-1 rounded-md text-white dark:text-black {{ if $user.Admin }}bg-gray-400 dark:bg-gray-600 cursor-pointer{{ else }}bg-gray-800 dark:bg-gray-100{{ end }}">user</span>
|
||||||
{{ $adminStyle := "bg-gray-400 dark:bg-gray-600 cursor-pointer" }}
|
</td>
|
||||||
{{ $userStyle := "bg-gray-400 dark:bg-gray-600 cursor-pointer" }}
|
<td class="p-3 border-b border-gray-200">
|
||||||
{{ if $user.Admin }}{{ $adminStyle = "bg-gray-800 dark:bg-gray-100 cursor-default" }}{{ end }}
|
<p>{{ $user.CreatedAt }}</p>
|
||||||
{{ if not $user.Admin }}{{ $userStyle = "bg-gray-800 dark:bg-gray-100 cursor-default" }}{{ end }}
|
</td>
|
||||||
<form method="POST"
|
</tr>
|
||||||
action="./users"
|
{{ end }}
|
||||||
class="flex flex gap-2 text-black dark:text-white text-sm">
|
</tbody>
|
||||||
<input type="hidden" id="operation" name="operation" value="UPDATE" />
|
</table>
|
||||||
<input type="hidden" id="user" name="user" value="{{ $user.ID }}" />
|
</div>
|
||||||
<input type="hidden" id="is_admin" name="is_admin" value="true" />
|
|
||||||
<button {{ if $user.Admin }}type="button"{{ else }}type="submit"{{ end }} class="px-2 py-1 rounded-md text-white dark:text-black {{ $adminStyle }}">admin
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<form method="POST"
|
|
||||||
action="./users"
|
|
||||||
class="flex flex gap-2 text-black dark:text-white text-sm">
|
|
||||||
<input type="hidden" id="operation" name="operation" value="UPDATE" />
|
|
||||||
<input type="hidden" id="user" name="user" value="{{ $user.ID }}" />
|
|
||||||
<input type="hidden" id="is_admin" name="is_admin" value="false" />
|
|
||||||
<button {{ if $user.Admin }}type="submit"{{ else }}type="button"{{ end }} class="px-2 py-1 rounded-md text-white dark:text-black {{ $userStyle }}">user
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
<td class="p-3 border-b border-gray-200">
|
|
||||||
<p>{{ $user.CreatedAt }}</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{{ end }}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
@@ -1,127 +1,85 @@
|
|||||||
{{ template "base" . }}
|
{{ template "base" . }}
|
||||||
{{ define "title" }}Admin - General{{ end }}
|
{{ define "title" }}Admin - General{{ end }}
|
||||||
{{ define "header" }}
|
{{ define "header" }}<a class="whitespace-pre" href="./admin">Admin - General</a>{{ end }}
|
||||||
<a class="whitespace-pre" href="./admin">Admin - General</a>
|
|
||||||
{{ end }}
|
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<div class="w-full flex flex-col gap-4 grow">
|
<div class="w-full flex flex-col gap-4 grow">
|
||||||
<div
|
<div class="flex flex-col gap-2 grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white">
|
||||||
class="flex flex-col gap-2 grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
<p class="text-lg font-semibold mb-2">Backup & Restore</p>
|
||||||
>
|
<div class="flex flex-col gap-4">
|
||||||
<p class="text-lg font-semibold mb-2">Backup & Restore</p>
|
<form class="flex justify-between" action="./admin" method="POST">
|
||||||
<div class="flex flex-col gap-4">
|
<input type="text" name="action" value="BACKUP" class="hidden" />
|
||||||
<form class="flex justify-between" action="./admin" method="POST">
|
<div class="flex gap-8 items-center">
|
||||||
<input type="text" name="action" value="BACKUP" class="hidden" />
|
<div>
|
||||||
<div class="flex gap-8 items-center">
|
<input type="checkbox" id="backup_covers" name="backup_types" value="COVERS" />
|
||||||
<div>
|
<label for="backup_covers">Covers</label>
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="backup_covers"
|
|
||||||
name="backup_types"
|
|
||||||
value="COVERS"
|
|
||||||
/>
|
|
||||||
<label for="backup_covers">Covers</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="backup_documents"
|
|
||||||
name="backup_types"
|
|
||||||
value="DOCUMENTS"
|
|
||||||
/>
|
|
||||||
<label for="backup_documents">Documents</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="w-40 h-10">
|
<div>
|
||||||
{{ template "component/button" (dict
|
<input type="checkbox"
|
||||||
"Title" "Backup"
|
id="backup_documents"
|
||||||
"Variant" "Secondary"
|
name="backup_types"
|
||||||
)
|
value="DOCUMENTS" />
|
||||||
}}
|
<label for="backup_documents">Documents</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
<form
|
<button type="submit"
|
||||||
method="POST"
|
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">
|
||||||
enctype="multipart/form-data"
|
<span class="w-full">Backup</span>
|
||||||
action="./admin"
|
</button>
|
||||||
class="flex justify-between grow"
|
</form>
|
||||||
>
|
<form method="POST"
|
||||||
<input type="text" name="action" value="RESTORE" class="hidden" />
|
enctype="multipart/form-data"
|
||||||
<div class="flex items-center w-1/2">
|
action="./admin"
|
||||||
<input
|
class="flex justify-between grow">
|
||||||
type="file"
|
<input type="text" name="action" value="RESTORE" class="hidden" />
|
||||||
accept=".zip"
|
<div class="flex items-center w-1/2">
|
||||||
name="restore_file"
|
<input type="file" accept=".zip" name="restore_file" class="w-full" />
|
||||||
class="w-full"
|
</div>
|
||||||
/>
|
<button type="submit"
|
||||||
</div>
|
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">
|
||||||
<div class="w-40 h-10">
|
<span class="w-full">Restore</span>
|
||||||
{{ template "component/button" (dict
|
</button>
|
||||||
"Title" "Restore"
|
</form>
|
||||||
"Variant" "Secondary"
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{{ if .PasswordErrorMessage }}
|
|
||||||
<span class="text-red-400 text-xs">{{ .PasswordErrorMessage }}</span>
|
|
||||||
{{ else if .PasswordMessage }}
|
|
||||||
<span class="text-green-400 text-xs">{{ .PasswordMessage }}</span>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="flex flex-col grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
|
||||||
>
|
|
||||||
<p class="text-lg font-semibold">Tasks</p>
|
|
||||||
<table class="min-w-full bg-white dark:bg-gray-700 text-sm">
|
|
||||||
<tbody class="text-black dark:text-white">
|
|
||||||
<tr>
|
|
||||||
<td class="pl-0">
|
|
||||||
<p>Metadata Matching</p>
|
|
||||||
</td>
|
|
||||||
<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>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<p>Cache Tables</p>
|
|
||||||
</td>
|
|
||||||
<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>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
{{ if .PasswordErrorMessage }}
|
||||||
|
<span class="text-red-400 text-xs">{{ .PasswordErrorMessage }}</span>
|
||||||
|
{{ else if .PasswordMessage }}
|
||||||
|
<span class="text-green-400 text-xs">{{ .PasswordMessage }}</span>
|
||||||
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-col grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white">
|
||||||
|
<p class="text-lg font-semibold">Tasks</p>
|
||||||
|
<table class="min-w-full bg-white dark:bg-gray-700 text-sm">
|
||||||
|
<tbody class="text-black dark:text-white">
|
||||||
|
<tr>
|
||||||
|
<td class="pl-0">
|
||||||
|
<p>Metadata Matching</p>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 float-right">
|
||||||
|
<form action="./admin" method="POST">
|
||||||
|
<input type="text" name="action" value="METADATA_MATCH" class="hidden" />
|
||||||
|
<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>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<p>Cache Tables</p>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 float-right">
|
||||||
|
<form action="./admin" method="POST">
|
||||||
|
<input type="text" name="action" value="CACHE_TABLES" class="hidden" />
|
||||||
|
<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>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
@@ -2,253 +2,298 @@
|
|||||||
{{ define "title" }}Documents{{ end }}
|
{{ define "title" }}Documents{{ end }}
|
||||||
{{ define "header" }}<a href="/documents">Documents</a>{{ end }}
|
{{ define "header" }}<a href="/documents">Documents</a>{{ end }}
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<div class="h-full w-full relative">
|
<div class="h-full w-full relative">
|
||||||
<!-- Document Info -->
|
<!-- Document Info -->
|
||||||
<div
|
<div class="h-full w-full overflow-scroll bg-white shadow-lg dark:bg-gray-700 rounded dark:text-white p-4">
|
||||||
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">
|
||||||
<div
|
<img class="rounded object-fill w-full"
|
||||||
class="flex flex-col gap-2 float-left w-44 md:w-60 lg:w-80 mr-4 mb-2 relative"
|
src="/documents/{{.Data.ID}}/cover" />
|
||||||
>
|
</label>
|
||||||
<label class="z-10 cursor-pointer" for="edit-cover-button">
|
{{ if .Data.Filepath }}
|
||||||
<img
|
<a href="/reader#id={{ .Data.ID }}&type=REMOTE"
|
||||||
class="rounded object-fill w-full"
|
class="z-10 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded text-sm text-center py-1 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">Read</a>
|
||||||
src="/documents/{{ .Data.ID }}/cover"
|
{{ end }}
|
||||||
/>
|
<div class="flex flex-wrap-reverse justify-between z-20 gap-2 relative">
|
||||||
</label>
|
<div class="min-w-[50%] md:mr-2">
|
||||||
{{ if .Data.Filepath }}
|
<div class="flex gap-1 text-sm">
|
||||||
<a
|
<p class="text-gray-500">ISBN-10:</p>
|
||||||
href="/reader#id={{ .Data.ID }}&type=REMOTE"
|
<p class="font-medium">{{ or .Data.Isbn10 "N/A" }}</p>
|
||||||
class="z-10 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded text-sm text-center py-1 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
|
||||||
>Read</a
|
|
||||||
>
|
|
||||||
{{ end }}
|
|
||||||
<div class="flex flex-wrap-reverse justify-between z-20 gap-2 relative">
|
|
||||||
<div class="min-w-[50%] md:mr-2">
|
|
||||||
<div class="flex gap-1 text-sm">
|
|
||||||
<p class="text-gray-500">ISBN-10:</p>
|
|
||||||
<p class="font-medium">{{ or .Data.Isbn10 "N/A" }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-1 text-sm">
|
|
||||||
<p class="text-gray-500">ISBN-13:</p>
|
|
||||||
<p class="font-medium">{{ or .Data.Isbn13 "N/A" }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="flex gap-1 text-sm">
|
||||||
class="flex grow justify-between my-auto text-gray-500 dark:text-gray-500"
|
<p class="text-gray-500">ISBN-13:</p>
|
||||||
>
|
<p class="font-medium">{{ or .Data.Isbn13 "N/A" }}</p>
|
||||||
<input
|
</div>
|
||||||
type="checkbox"
|
</div>
|
||||||
id="edit-cover-button"
|
<div class="flex grow justify-between my-auto text-gray-500 dark:text-gray-500">
|
||||||
class="hidden css-button"
|
<input type="checkbox" id="edit-cover-button" class="hidden css-button" />
|
||||||
/>
|
<div class="absolute z-30 flex flex-col gap-2 top-0 left-0 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600">
|
||||||
<div
|
<form method="POST"
|
||||||
class="absolute z-30 flex flex-col gap-2 top-0 left-0 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600"
|
enctype="multipart/form-data"
|
||||||
>
|
action="./{{ .Data.ID }}/edit"
|
||||||
<form
|
class="flex flex-col gap-2 w-72 text-black dark:text-white text-sm">
|
||||||
method="POST"
|
<input type="file" id="cover_file" name="cover_file">
|
||||||
enctype="multipart/form-data"
|
<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"
|
||||||
action="./{{ .Data.ID }}/edit"
|
type="submit">Upload Cover</button>
|
||||||
class="flex flex-col gap-2 w-72 text-black dark:text-white text-sm"
|
</form>
|
||||||
>
|
<form method="POST"
|
||||||
<input type="file" id="cover_file" name="cover_file" />
|
action="./{{ .Data.ID }}/edit"
|
||||||
{{ template "component/button" (dict "Title" "Upload Cover") }}
|
class="flex flex-col gap-2 w-72 text-black dark:text-white text-sm">
|
||||||
</form>
|
<input type="checkbox"
|
||||||
<form
|
checked
|
||||||
method="POST"
|
id="remove_cover"
|
||||||
action="./{{ .Data.ID }}/edit"
|
name="remove_cover"
|
||||||
class="flex flex-col gap-2 w-72 text-black dark:text-white text-sm"
|
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"
|
||||||
<input
|
type="submit">Remove Cover</button>
|
||||||
type="checkbox"
|
</form>
|
||||||
checked
|
</div>
|
||||||
id="remove_cover"
|
<div class="relative">
|
||||||
name="remove_cover"
|
<label for="delete-button" class="cursor-pointer">{{ template "svg/delete" (dict "Size" 28) }}</label>
|
||||||
class="hidden"
|
<input type="checkbox" id="delete-button" class="hidden css-button" />
|
||||||
/>
|
<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">
|
||||||
{{ template "component/button" (dict "Title" "Remove Cover") }}
|
<form method="POST"
|
||||||
|
action="./{{ .Data.ID }}/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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative">
|
|
||||||
<label for="delete-button" class="cursor-pointer"
|
|
||||||
>{{ template "svg/delete" (dict "Size" 28) }}</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="delete-button"
|
|
||||||
class="hidden css-button"
|
|
||||||
/>
|
|
||||||
<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") }}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a href="../activity?document={{ .Data.ID }}"
|
|
||||||
>{{ template "svg/activity" (dict "Size" 28) }}</a
|
|
||||||
>
|
|
||||||
<div class="relative">
|
|
||||||
<label for="search-button"
|
|
||||||
>{{ template "svg/search" (dict "Size" 28) }}</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="search-button"
|
|
||||||
class="hidden css-button"
|
|
||||||
/>
|
|
||||||
<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 }}/identify"
|
|
||||||
class="flex flex-col gap-2 text-black dark:text-white text-sm"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="title"
|
|
||||||
name="title"
|
|
||||||
placeholder="Title"
|
|
||||||
value="{{ or .Data.Title nil }}"
|
|
||||||
class="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="author"
|
|
||||||
name="author"
|
|
||||||
placeholder="Author"
|
|
||||||
value="{{ or .Data.Author nil }}"
|
|
||||||
class="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="isbn"
|
|
||||||
name="isbn"
|
|
||||||
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") }}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{ if .Data.Filepath }}
|
|
||||||
<a href="./{{ .Data.ID }}/file"
|
|
||||||
>{{ template "svg/download" (dict "Size" 28) }}</a
|
|
||||||
>
|
|
||||||
{{ else }}
|
|
||||||
{{ template "svg/download" (dict "Size" 28 "Disabled" true) }}
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
</div>
|
||||||
|
<a href="../activity?document={{ .Data.ID }}">{{ template "svg/activity" (dict "Size" 28) }}</a>
|
||||||
|
<div class="relative">
|
||||||
|
<label for="search-button">{{ template "svg/search" (dict "Size" 28) }}</label>
|
||||||
|
<input type="checkbox" id="search-button" class="hidden css-button" />
|
||||||
|
<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 }}/identify"
|
||||||
|
class="flex flex-col gap-2 text-black dark:text-white text-sm">
|
||||||
|
<input type="text"
|
||||||
|
id="title"
|
||||||
|
name="title"
|
||||||
|
placeholder="Title"
|
||||||
|
value="{{ or .Data.Title nil }}"
|
||||||
|
class="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white">
|
||||||
|
<input type="text"
|
||||||
|
id="author"
|
||||||
|
name="author"
|
||||||
|
placeholder="Author"
|
||||||
|
value="{{ or .Data.Author nil }}"
|
||||||
|
class="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white">
|
||||||
|
<input type="text"
|
||||||
|
id="isbn"
|
||||||
|
name="isbn"
|
||||||
|
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">
|
||||||
|
<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>
|
||||||
|
{{ if .Data.Filepath }}
|
||||||
|
<a href="./{{.Data.ID}}/file">{{ template "svg/download" (dict "Size" 28) }}</a>
|
||||||
|
{{ else }}
|
||||||
|
{{ template "svg/download" (dict "Size" 28 "Disabled" true) }}
|
||||||
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid sm:grid-cols-2 justify-between gap-4 pb-4">
|
</div>
|
||||||
{{ template "component/key-val-edit" (dict
|
<div class="grid sm:grid-cols-2 justify-between gap-4 pb-4">
|
||||||
"Title" "Title"
|
<div class="relative">
|
||||||
"Value" .Data.Title
|
<div class="text-gray-500 inline-flex gap-2 relative">
|
||||||
"URL" (printf "./%s/edit" .Data.ID)
|
<p>Title</p>
|
||||||
"FormValue" "title"
|
<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">
|
||||||
{{ template "component/key-val-edit" (dict
|
<form method="POST"
|
||||||
"Title" "Author"
|
action="./{{ .Data.ID }}/edit"
|
||||||
"Value" .Data.Author
|
class="flex flex-col gap-2 text-black dark:text-white text-sm">
|
||||||
"URL" (printf "./%s/edit" .Data.ID)
|
<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">
|
||||||
"FormValue" "author"
|
<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 class="relative">
|
|
||||||
<div class="text-gray-500 inline-flex gap-2 relative">
|
|
||||||
<p>Time Read</p>
|
|
||||||
<label class="my-auto" for="progress-info-button"
|
|
||||||
>{{ template "svg/info" (dict "Size" 18) }}</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="progress-info-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"
|
|
||||||
>
|
|
||||||
<div class="text-xs flex">
|
|
||||||
<p class="text-gray-400 w-32">Seconds / Percent</p>
|
|
||||||
<p class="font-medium dark:text-white">
|
|
||||||
{{ .Data.SecondsPerPercent }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="text-xs flex">
|
|
||||||
<p class="text-gray-400 w-32">Words / Minute</p>
|
|
||||||
<p class="font-medium dark:text-white">{{ .Data.Wpm }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="text-xs flex">
|
|
||||||
<p class="text-gray-400 w-32">Est. Time Left</p>
|
|
||||||
<p class="font-medium dark:text-white whitespace-nowrap">
|
|
||||||
{{ niceSeconds .TotalTimeLeftSeconds }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="font-medium text-lg">
|
|
||||||
{{ niceSeconds .Data.TotalTimeSeconds }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-gray-500">Progress</p>
|
|
||||||
<p class="font-medium text-lg">{{ .Data.Percentage }}%</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="font-medium text-lg">{{ or .Data.Title "N/A" }}</p>
|
||||||
</div>
|
</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>Description</p>
|
<p>Author</p>
|
||||||
<label class="my-auto" for="edit-description-button"
|
<label class="my-auto" for="edit-author-button">{{ template "svg/edit" (dict "Size" 18) }}</label>
|
||||||
>{{ 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>
|
</div>
|
||||||
|
<p class="font-medium text-lg">{{ or .Data.Author "N/A" }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative font-medium text-justify hyphens-auto">
|
<div class="relative">
|
||||||
<input
|
<div class="text-gray-500 inline-flex gap-2 relative">
|
||||||
type="checkbox"
|
<p>Time Read</p>
|
||||||
id="edit-description-button"
|
<label class="my-auto" for="progress-info-button">{{ template "svg/info" (dict "Size" 18) }}</label>
|
||||||
class="hidden css-button"
|
<input type="checkbox" id="progress-info-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">
|
||||||
<div
|
<div class="text-xs flex">
|
||||||
class="absolute h-full w-full min-h-[10em] z-30 top-1 right-0 gap-4 flex transition-all duration-200"
|
<p class="text-gray-400 w-32">Seconds / Percent</p>
|
||||||
>
|
<p class="font-medium dark:text-white">{{ .Data.SecondsPerPercent }}</p>
|
||||||
<img
|
</div>
|
||||||
class="hidden md:block invisible rounded w-44 md:w-60 lg:w-80 object-fill"
|
<div class="text-xs flex">
|
||||||
src="/documents/{{ .Data.ID }}/cover"
|
<p class="text-gray-400 w-32">Words / Minute</p>
|
||||||
/>
|
<p class="font-medium dark:text-white">{{ .Data.Wpm }}</p>
|
||||||
<form
|
</div>
|
||||||
method="POST"
|
<div class="text-xs flex">
|
||||||
action="./{{ .Data.ID }}/edit"
|
<p class="text-gray-400 w-32">Est. Time Left</p>
|
||||||
class="flex flex-col gap-2 w-full text-black bg-gray-200 rounded shadow-lg shadow-gray-500 dark:text-white dark:shadow-gray-900 dark:bg-gray-600 text-sm p-3"
|
<p class="font-medium dark:text-white whitespace-nowrap">{{ niceSeconds .TotalTimeLeftSeconds }}</p>
|
||||||
>
|
</div>
|
||||||
<textarea
|
</div>
|
||||||
type="text"
|
|
||||||
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") }}
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
<p>{{ or .Data.Description "N/A" }}</p>
|
<p class="font-medium text-lg">{{ niceSeconds .Data.TotalTimeSeconds }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-500">Progress</p>
|
||||||
|
<p class="font-medium text-lg">{{ .Data.Percentage }}%</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ template "component/metadata" (dict
|
<div class="relative">
|
||||||
"ID" .Data.ID
|
<div class="text-gray-500 inline-flex gap-2 relative">
|
||||||
"Metadata" .Metadata
|
<p>Description</p>
|
||||||
"Error" .MetadataError
|
<label class="my-auto" for="edit-description-button">{{ template "svg/edit" (dict "Size" 18) }}</label>
|
||||||
)
|
</div>
|
||||||
}}
|
</div>
|
||||||
|
<div class="relative font-medium text-justify hyphens-auto">
|
||||||
|
<input type="checkbox"
|
||||||
|
id="edit-description-button"
|
||||||
|
class="hidden css-button" />
|
||||||
|
<div class="absolute h-full w-full min-h-[10em] z-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="/documents/{{.Data.ID}}/cover" />
|
||||||
|
<form method="POST"
|
||||||
|
action="./{{ .Data.ID }}/edit"
|
||||||
|
class="flex flex-col gap-2 w-full text-black bg-gray-200 rounded shadow-lg shadow-gray-500 dark:text-white dark:shadow-gray-900 dark:bg-gray-600 text-sm p-3">
|
||||||
|
<textarea type="text"
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
class="h-full w-full p-2 bg-gray-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"
|
||||||
|
type="submit">Save</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<p>{{ or .Data.Description "N/A" }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{ 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 }}
|
{{ end }}
|
||||||
|
|||||||
@@ -2,98 +2,104 @@
|
|||||||
{{ define "title" }}Documents{{ end }}
|
{{ define "title" }}Documents{{ end }}
|
||||||
{{ define "header" }}<a href="./documents">Documents</a>{{ end }}
|
{{ define "header" }}<a href="./documents">Documents</a>{{ end }}
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<div
|
<div class="flex flex-col gap-2 grow p-4 mb-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white">
|
||||||
class="flex flex-col gap-2 grow p-4 mb-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
<form class="flex gap-4 flex-col lg:flex-row"
|
||||||
>
|
|
||||||
<form
|
|
||||||
class="flex gap-4 flex-col lg:flex-row"
|
|
||||||
action="./documents"
|
|
||||||
method="GET"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col w-full grow">
|
|
||||||
<div class="flex relative">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
|
||||||
>
|
|
||||||
{{ template "svg/search2" (dict "Size" 15) }}
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="search"
|
|
||||||
name="search"
|
|
||||||
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-2 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
|
||||||
placeholder="Search Author / Title"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="lg:w-60">
|
|
||||||
{{ template "component/button" (dict
|
|
||||||
"Title" "Search"
|
|
||||||
"Variant" "Secondary"
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</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 }}
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
<div class="w-full flex gap-4 justify-center mt-4 text-black dark:text-white">
|
|
||||||
{{ if .PreviousPage }}
|
|
||||||
<a
|
|
||||||
href="./documents?page={{ .PreviousPage }}&limit={{ .PageLimit }}"
|
|
||||||
class="bg-white shadow-lg dark:bg-gray-600 hover:bg-gray-400 font-medium rounded text-sm text-center p-2 w-24 dark:hover:bg-gray-700 focus:outline-none"
|
|
||||||
>◄</a
|
|
||||||
>
|
|
||||||
{{ end }}
|
|
||||||
{{ if .NextPage }}
|
|
||||||
<a
|
|
||||||
href="./documents?page={{ .NextPage }}&limit={{ .PageLimit }}"
|
|
||||||
class="bg-white shadow-lg dark:bg-gray-600 hover:bg-gray-400 font-medium rounded text-sm text-center p-2 w-24 dark:hover:bg-gray-700 focus:outline-none"
|
|
||||||
>►</a
|
|
||||||
>
|
|
||||||
{{ 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="absolute right-0 z-10 bottom-0 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"
|
action="./documents"
|
||||||
class="flex flex-col gap-2"
|
method="GET">
|
||||||
>
|
<div class="flex flex-col w-full grow">
|
||||||
<input
|
<div class="flex relative">
|
||||||
type="file"
|
<span class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm">
|
||||||
accept=".epub"
|
{{ template "svg/search2" (dict "Size" 15) }}
|
||||||
id="document_file"
|
</span>
|
||||||
name="document_file"
|
<input type="text"
|
||||||
/>
|
id="search"
|
||||||
<button
|
name="search"
|
||||||
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"
|
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-2 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
||||||
type="submit"
|
placeholder="Search Author / Title" />
|
||||||
>
|
</div>
|
||||||
Upload File
|
</div>
|
||||||
</button>
|
<button type="submit"
|
||||||
</form>
|
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">
|
||||||
<label for="upload-file-button">
|
<span class="w-full">Search</span>
|
||||||
<div
|
</button>
|
||||||
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"
|
</form>
|
||||||
>
|
</div>
|
||||||
Cancel Upload
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
</div>
|
{{ range $doc := .Data }}
|
||||||
</label>
|
<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>
|
||||||
<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"
|
|
||||||
>{{ template "svg/upload" (dict "Size" 34) }}</label
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
<div class="w-full flex gap-4 justify-center mt-4 text-black dark:text-white">
|
||||||
|
{{ if .PreviousPage }}
|
||||||
|
<a href="./documents?page={{ .PreviousPage }}&limit={{ .PageLimit }}"
|
||||||
|
class="bg-white shadow-lg dark:bg-gray-600 hover:bg-gray-400 font-medium rounded text-sm text-center p-2 w-24 dark:hover:bg-gray-700 focus:outline-none">◄</a>
|
||||||
|
{{ end }}
|
||||||
|
{{ if .NextPage }}
|
||||||
|
<a href="./documents?page={{ .NextPage }}&limit={{ .PageLimit }}"
|
||||||
|
class="bg-white shadow-lg dark:bg-gray-600 hover:bg-gray-400 font-medium rounded text-sm text-center p-2 w-24 dark:hover:bg-gray-700 focus:outline-none">►</a>
|
||||||
|
{{ 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">{{ template "svg/upload" (dict "Size" 34) }}</label>
|
||||||
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
@@ -1,53 +1,30 @@
|
|||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta
|
<meta name="viewport"
|
||||||
name="viewport"
|
content="width=device-width, initial-scale=0.90, user-scalable=no, viewport-fit=cover" />
|
||||||
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-capable" content="yes" />
|
||||||
<meta
|
<meta name="apple-mobile-web-app-status-bar-style"
|
||||||
name="apple-mobile-web-app-status-bar-style"
|
content="black-translucent" />
|
||||||
content="black-translucent"
|
<meta name="theme-color"
|
||||||
/>
|
content="#F3F4F6"
|
||||||
<meta
|
media="(prefers-color-scheme: light)" />
|
||||||
name="theme-color"
|
<meta name="theme-color"
|
||||||
content="#F3F4F6"
|
content="#1F2937"
|
||||||
media="(prefers-color-scheme: light)"
|
media="(prefers-color-scheme: dark)" />
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
name="theme-color"
|
|
||||||
content="#1F2937"
|
|
||||||
media="(prefers-color-scheme: dark)"
|
|
||||||
/>
|
|
||||||
<title>AnthoLume - Error</title>
|
<title>AnthoLume - Error</title>
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<link rel="stylesheet" href="/assets/style.css" />
|
<link rel="stylesheet" href="/assets/style.css" />
|
||||||
</head>
|
</head>
|
||||||
<body
|
<body class="bg-gray-100 dark:bg-gray-800 flex flex-col justify-center h-screen">
|
||||||
class="bg-gray-100 dark:bg-gray-800 flex flex-col justify-center h-screen"
|
|
||||||
>
|
|
||||||
<div class="py-8 px-4 mx-auto max-w-screen-xl lg:py-16 lg:px-6">
|
<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">
|
<div class="mx-auto max-w-screen-sm text-center">
|
||||||
<h1
|
<h1 class="mb-4 text-7xl tracking-tight font-extrabold lg:text-9xl text-gray-600 dark:text-gray-500">{{ .Status }}</h1>
|
||||||
class="mb-4 text-7xl tracking-tight font-extrabold lg:text-9xl text-gray-600 dark:text-gray-500"
|
<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>
|
||||||
{{ .Status }}
|
<a href="/"
|
||||||
</h1>
|
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>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -2,108 +2,136 @@
|
|||||||
{{ define "title" }}Home{{ end }}
|
{{ define "title" }}Home{{ end }}
|
||||||
{{ define "header" }}<a href="./">Home</a>{{ end }}
|
{{ define "header" }}<a href="./">Home</a>{{ end }}
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<div class="relative w-full bg-white shadow-lg dark:bg-gray-700 rounded">
|
<div class="relative w-full bg-white shadow-lg dark:bg-gray-700 rounded">
|
||||||
<p
|
<p class="absolute top-3 left-5 text-sm font-semibold text-gray-700 border-b border-gray-200 w-max dark:text-white dark:border-gray-500">
|
||||||
class="absolute top-3 left-5 text-sm font-semibold text-gray-700 border-b border-gray-200 w-max dark:text-white dark:border-gray-500"
|
Daily Read Totals
|
||||||
>
|
</p>
|
||||||
Daily Read Totals
|
{{ $data := (getSVGGraphData .Data.GraphData 800 70 )}}
|
||||||
</p>
|
<div class="relative">
|
||||||
{{ $data := (getSVGGraphData .Data.GraphData 800 70 ) }}
|
<svg viewBox="26 0 755 {{ $data.Height }}"
|
||||||
<div class="relative">
|
preserveAspectRatio="none"
|
||||||
<svg
|
width="100%"
|
||||||
viewBox="26 0 755 {{ $data.Height }}"
|
height="6em">
|
||||||
preserveAspectRatio="none"
|
<!-- Bezier Line Graph -->
|
||||||
width="100%"
|
<path fill="#316BBE" fill-opacity="0.5" stroke="none" d="{{ $data.BezierPath }} {{ $data.BezierFill }}" />
|
||||||
height="6em"
|
<path fill="none" stroke="#316BBE" d="{{ $data.BezierPath }}" />
|
||||||
>
|
</svg>
|
||||||
<!-- Bezier Line Graph -->
|
<div class="flex absolute w-full h-full top-0"
|
||||||
<path
|
style="width: calc(100%*31/30);
|
||||||
fill="#316BBE"
|
|
||||||
fill-opacity="0.5"
|
|
||||||
stroke="none"
|
|
||||||
d="{{ $data.BezierPath }} {{ $data.BezierFill }}"
|
|
||||||
/>
|
|
||||||
<path fill="none" stroke="#316BBE" d="{{ $data.BezierPath }}" />
|
|
||||||
</svg>
|
|
||||||
<div
|
|
||||||
class="flex absolute w-full h-full top-0"
|
|
||||||
style="width: calc(100%*31/30);
|
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
left: 50%"
|
left: 50%">
|
||||||
>
|
{{ range $index, $item := $data.LinePoints }}
|
||||||
{{ range $index, $item := $data.LinePoints }}
|
<!-- Required for iOS "Hover" Events (onclick) -->
|
||||||
<!-- Required for iOS "Hover" Events (onclick) -->
|
<div onclick
|
||||||
<div
|
class="opacity-0 hover:opacity-100 w-full"
|
||||||
onclick
|
style="background: linear-gradient(rgba(128, 128, 128, 0.5), rgba(128, 128, 128, 0.5)) no-repeat center/2px 100%">
|
||||||
class="opacity-0 hover:opacity-100 w-full"
|
<div class="flex flex-col items-center p-2 rounded absolute top-3 dark:text-white text-xs pointer-events-none"
|
||||||
style="background: linear-gradient(rgba(128, 128, 128, 0.5), rgba(128, 128, 128, 0.5)) no-repeat center/2px 100%"
|
style="transform: translateX(-50%);
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="flex flex-col items-center p-2 rounded absolute top-3 dark:text-white text-xs pointer-events-none"
|
|
||||||
style="transform: translateX(-50%);
|
|
||||||
background-color: rgba(128, 128, 128, 0.2);
|
background-color: rgba(128, 128, 128, 0.2);
|
||||||
left: 50%"
|
left: 50%">
|
||||||
>
|
<span>{{ (index $.Data.GraphData $index).Date }}</span>
|
||||||
<span>{{ (index $.Data.GraphData $index).Date }}</span>
|
<span>{{ (index $.Data.GraphData $index).MinutesRead }} minutes</span>
|
||||||
<span
|
</div>
|
||||||
>{{ (index $.Data.GraphData $index).MinutesRead }}
|
</div>
|
||||||
minutes</span
|
{{ end }}
|
||||||
>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||||
|
<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 }}
|
||||||
|
<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>
|
||||||
{{ end }}
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-4 md:grid-cols-4">
|
{{ end }}
|
||||||
{{ 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
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
||||||
{{ range $item := .Data.Streaks }}
|
|
||||||
{{ template "component/streak-card" $item }}
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
||||||
{{ template "component/leaderboard-card" (dict
|
|
||||||
"Name" "WPM"
|
|
||||||
"Data" .Data.UserStatistics.WPM
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
{{ template "component/leaderboard-card" (dict
|
|
||||||
"Name" "Duration"
|
|
||||||
"Data" .Data.UserStatistics.Duration
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
{{ template "component/leaderboard-card" (dict
|
|
||||||
"Name" "Words"
|
|
||||||
"Data" .Data.UserStatistics.Words
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{{ template "component/leaderboard-card" (dict
|
||||||
|
"Name" "WPM"
|
||||||
|
"Data" .Data.UserStatistics.WPM
|
||||||
|
)}}
|
||||||
|
{{ template "component/leaderboard-card" (dict
|
||||||
|
"Name" "Duration"
|
||||||
|
"Data" .Data.UserStatistics.Duration
|
||||||
|
)}}
|
||||||
|
{{ template "component/leaderboard-card" (dict
|
||||||
|
"Name" "Words"
|
||||||
|
"Data" .Data.UserStatistics.Words
|
||||||
|
)}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
@@ -1,29 +1,19 @@
|
|||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta
|
<meta name="viewport"
|
||||||
name="viewport"
|
content="width=device-width, initial-scale=0.90, user-scalable=no, viewport-fit=cover" />
|
||||||
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-capable" content="yes" />
|
||||||
<meta
|
<meta name="apple-mobile-web-app-status-bar-style"
|
||||||
name="apple-mobile-web-app-status-bar-style"
|
content="black-translucent" />
|
||||||
content="black-translucent"
|
<meta name="theme-color"
|
||||||
/>
|
content="#F3F4F6"
|
||||||
<meta
|
media="(prefers-color-scheme: light)" />
|
||||||
name="theme-color"
|
<meta name="theme-color"
|
||||||
content="#F3F4F6"
|
content="#1F2937"
|
||||||
media="(prefers-color-scheme: light)"
|
media="(prefers-color-scheme: dark)" />
|
||||||
/>
|
<title>AnthoLume - {{ if .Register }}Register{{ else }}Login{{ end }}</title>
|
||||||
<meta
|
|
||||||
name="theme-color"
|
|
||||||
content="#1F2937"
|
|
||||||
media="(prefers-color-scheme: dark)"
|
|
||||||
/>
|
|
||||||
<title>
|
|
||||||
AnthoLume - {{ if .Register }}Register{{ else }}Login{{ end }}
|
|
||||||
</title>
|
|
||||||
<link rel="manifest" href="./manifest.json" />
|
<link rel="manifest" href="./manifest.json" />
|
||||||
<link rel="stylesheet" href="./assets/style.css" />
|
<link rel="stylesheet" href="./assets/style.css" />
|
||||||
<!-- Service Worker / Offline Cache Flush -->
|
<!-- Service Worker / Offline Cache Flush -->
|
||||||
@@ -42,8 +32,7 @@
|
|||||||
|
|
||||||
html {
|
html {
|
||||||
height: calc(100% + env(safe-area-inset-bottom));
|
height: calc(100% + env(safe-area-inset-bottom));
|
||||||
padding: env(safe-area-inset-top) env(safe-area-inset-right) 0
|
padding: env(safe-area-inset-top) env(safe-area-inset-right) 0 env(safe-area-inset-left);
|
||||||
env(safe-area-inset-left);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* No Scrollbar - IE, Edge, Firefox */
|
/* No Scrollbar - IE, Edge, Firefox */
|
||||||
@@ -61,112 +50,77 @@
|
|||||||
<body class="bg-gray-100 dark:bg-gray-800 dark:text-white">
|
<body class="bg-gray-100 dark:bg-gray-800 dark:text-white">
|
||||||
<div class="flex flex-wrap w-full">
|
<div class="flex flex-wrap w-full">
|
||||||
<div class="flex flex-col w-full md:w-1/2">
|
<div class="flex flex-col w-full md:w-1/2">
|
||||||
<div
|
<div class="flex flex-col justify-center px-8 pt-8 my-auto md:justify-start md:pt-0 md:px-24 lg:px-32">
|
||||||
class="flex flex-col justify-center px-8 pt-8 my-auto md:justify-start md:pt-0 md:px-24 lg:px-32"
|
|
||||||
>
|
|
||||||
<p class="text-3xl text-center">Welcome.</p>
|
<p class="text-3xl text-center">Welcome.</p>
|
||||||
<form
|
<form
|
||||||
class="flex flex-col pt-3 md:pt-8"
|
class="flex flex-col pt-3 md:pt-8"
|
||||||
{{ if
|
{{if
|
||||||
.Register
|
.Register}}action="./register"
|
||||||
}}
|
{{ else }}action="./login"
|
||||||
action="./register"
|
|
||||||
{{ else }}
|
|
||||||
action="./login"
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
method="POST"
|
method="POST"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col pt-4">
|
<div class="flex flex-col pt-4">
|
||||||
<div class="flex relative">
|
<div class="flex relative">
|
||||||
<span
|
<span class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm">
|
||||||
class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
|
||||||
>
|
|
||||||
{{ template "svg/user" (dict "Size" 15) }}
|
{{ template "svg/user" (dict "Size" 15) }}
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input type="text"
|
||||||
type="text"
|
id="username"
|
||||||
id="username"
|
name="username"
|
||||||
name="username"
|
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
||||||
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
placeholder="Username" />
|
||||||
placeholder="Username"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col pt-4 mb-12">
|
<div class="flex flex-col pt-4 mb-12">
|
||||||
<div class="flex relative">
|
<div class="flex relative">
|
||||||
<span
|
<span class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm">
|
||||||
class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
|
||||||
>
|
|
||||||
{{ template "svg/password" (dict "Size" 15) }}
|
{{ template "svg/password" (dict "Size" 15) }}
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input type="password"
|
||||||
type="password"
|
id="password"
|
||||||
id="password"
|
name="password"
|
||||||
name="password"
|
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
||||||
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
placeholder="Password" />
|
||||||
placeholder="Password"
|
<span class="absolute -bottom-5 text-red-400 text-xs">{{ .Error }}</span>
|
||||||
/>
|
|
||||||
<span class="absolute -bottom-5 text-red-400 text-xs"
|
|
||||||
>{{ .Error }}</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button type="submit"
|
||||||
type="submit"
|
class="w-full px-4 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">
|
||||||
class="w-full px-4 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"
|
|
||||||
>
|
|
||||||
{{ if .Register }}
|
{{ if .Register }}
|
||||||
<span class="w-full">Register</span>
|
<span class="w-full">Register</span>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<span class="w-full">Submit</span>
|
<span class="w-full">Submit</span>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="pt-12 pb-12 text-center">
|
<div class="pt-12 pb-12 text-center">
|
||||||
{{ if .Config.RegistrationEnabled }}
|
{{ if .Config.RegistrationEnabled }} {{ if .Register }}
|
||||||
{{ if .Register }}
|
<p>
|
||||||
<p>
|
Trying to login?
|
||||||
Trying to login?
|
<a href="./login" class="font-semibold underline">Login here.</a>
|
||||||
<a href="./login" class="font-semibold underline"
|
</p>
|
||||||
>Login here.</a
|
{{ else }}
|
||||||
>
|
<p>
|
||||||
</p>
|
Don't have an account?
|
||||||
{{ else }}
|
<a href="./register" class="font-semibold underline">Register here.</a>
|
||||||
<p>
|
</p>
|
||||||
Don't have an account?
|
{{ end }} {{ end }}
|
||||||
<a href="./register" class="font-semibold underline"
|
|
||||||
>Register here.</a
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
{{ end }}
|
|
||||||
{{ end }}
|
|
||||||
<p class="mt-4">
|
<p class="mt-4">
|
||||||
<a href="./local" class="font-semibold underline"
|
<a href="./local" class="font-semibold underline">Offline / Local Mode</a>
|
||||||
>Offline / Local Mode</a
|
|
||||||
>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="hidden image-fader w-1/2 shadow-2xl h-screen relative md:block">
|
||||||
class="hidden image-fader w-1/2 shadow-2xl h-screen relative md:block"
|
<img class="w-full h-screen object-cover ease-in-out top-0 left-0"
|
||||||
>
|
src="/assets/images/book1.jpg" />
|
||||||
<img
|
<img class="w-full h-screen object-cover ease-in-out top-0 left-0"
|
||||||
class="w-full h-screen object-cover ease-in-out top-0 left-0"
|
src="/assets/images/book2.jpg" />
|
||||||
src="/assets/images/book1.jpg"
|
<img class="w-full h-screen object-cover ease-in-out top-0 left-0"
|
||||||
/>
|
src="/assets/images/book3.jpg" />
|
||||||
<img
|
<img class="w-full h-screen object-cover ease-in-out top-0 left-0"
|
||||||
class="w-full h-screen object-cover ease-in-out top-0 left-0"
|
src="/assets/images/book4.jpg" />
|
||||||
src="/assets/images/book2.jpg"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
class="w-full h-screen object-cover ease-in-out top-0 left-0"
|
|
||||||
src="/assets/images/book3.jpg"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
class="w-full h-screen object-cover ease-in-out top-0 left-0"
|
|
||||||
src="/assets/images/book4.jpg"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -2,27 +2,43 @@
|
|||||||
{{ define "title" }}Progress{{ end }}
|
{{ define "title" }}Progress{{ end }}
|
||||||
{{ define "header" }}<a href="./progress">Progress</a>{{ end }}
|
{{ define "header" }}<a href="./progress">Progress</a>{{ end }}
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<div class="inline-block min-w-full overflow-hidden rounded shadow">
|
<div class="inline-block min-w-full overflow-hidden rounded shadow">
|
||||||
<!-- Table Component - Utilizes Template "table-cell" -->
|
<table class="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm">
|
||||||
{{ template "component/table" (dict
|
<thead class="text-gray-800 dark:text-gray-400">
|
||||||
"Columns" (slice "Document" "Device Name" "Percentage" "Created At")
|
<tr>
|
||||||
"Keys" (slice "Document" "DeviceName" "Percentage" "CreatedAt")
|
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Document</th>
|
||||||
"Rows" .Data
|
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Device</th>
|
||||||
)
|
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Percent</th>
|
||||||
}}
|
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Time</th>
|
||||||
</div>
|
</tr>
|
||||||
</div>
|
</thead>
|
||||||
{{ end }}
|
<tbody class="text-black dark:text-white">
|
||||||
<!-- Table Cell Definition -->
|
{{ if not .Data }}
|
||||||
{{ define "table-cell" }}
|
<tr>
|
||||||
{{ if eq .Name "Document" }}
|
<td class="text-center p-3" colspan="4">No Results</td>
|
||||||
<a href="./documents/{{ .Data.DocumentID }}"
|
</tr>
|
||||||
>{{ .Data.Author }} - {{ .Data.Title }}</a
|
{{ end }}
|
||||||
>
|
{{ range $progress := .Data }}
|
||||||
{{ else if eq .Name "Percentage" }}
|
<tr>
|
||||||
{{ index (fields .Data) .Name }}%
|
<td class="p-3 border-b border-gray-200">
|
||||||
{{ else }}
|
<a href="./documents/{{ $progress.DocumentID }}">{{ $progress.Author }} - {{ $progress.Title }}
|
||||||
{{ index (fields .Data) .Name }}
|
</p>
|
||||||
{{ end }}
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="p-3 border-b border-gray-200">
|
||||||
|
<p>{{ $progress.DeviceName }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="p-3 border-b border-gray-200">
|
||||||
|
<p>{{ $progress.Percentage }}%</p>
|
||||||
|
</td>
|
||||||
|
<td class="p-3 border-b border-gray-200">
|
||||||
|
<p>{{ $progress.CreatedAt }}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
@@ -2,156 +2,108 @@
|
|||||||
{{ define "title" }}Search{{ end }}
|
{{ define "title" }}Search{{ end }}
|
||||||
{{ define "header" }}<a href="./search">Search</a>{{ end }}
|
{{ define "header" }}<a href="./search">Search</a>{{ end }}
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<div class="w-full flex flex-col md:flex-row gap-4">
|
<div class="w-full flex flex-col md:flex-row gap-4">
|
||||||
<div class="flex flex-col gap-4 grow">
|
<div class="flex flex-col gap-4 grow">
|
||||||
<div
|
<div class="flex flex-col gap-2 grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white">
|
||||||
class="flex flex-col gap-2 grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
<form class="flex gap-4 flex-col lg:flex-row" action="./search">
|
||||||
>
|
<div class="flex flex-col w-full grow">
|
||||||
<form class="flex gap-4 flex-col lg:flex-row" action="./search">
|
<div class="flex relative">
|
||||||
<div class="flex flex-col w-full grow">
|
<span class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm">
|
||||||
<div class="flex relative">
|
{{ template "svg/search2" (dict "Size" 15) }}
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
|
||||||
>
|
|
||||||
{{ template "svg/search2" (dict "Size" 15) }}
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="query"
|
|
||||||
name="query"
|
|
||||||
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
|
||||||
placeholder="Query"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex relative min-w-[12em]">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
|
||||||
>
|
|
||||||
{{ template "svg/documents" (dict "Size" 15) }}
|
|
||||||
</span>
|
</span>
|
||||||
<select
|
<input type="text"
|
||||||
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
id="query"
|
||||||
id="source"
|
name="query"
|
||||||
name="source"
|
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
||||||
>
|
placeholder="Query" />
|
||||||
<option value="LibGen">Library Genesis</option>
|
|
||||||
<option value="Annas Archive">Annas Archive</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="lg:w-60">
|
</div>
|
||||||
{{ template "component/button" (dict
|
<div class="flex relative min-w-[12em]">
|
||||||
"Title" "Search"
|
<span class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm">
|
||||||
"Variant" "Secondary"
|
{{ template "svg/documents" (dict "Size" 15) }}
|
||||||
)
|
</span>
|
||||||
}}
|
<select class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
||||||
</div>
|
id="source"
|
||||||
</form>
|
name="source">
|
||||||
{{ if .SearchErrorMessage }}
|
<option value="Annas Archive">Annas Archive</option>
|
||||||
<span class="text-red-400 text-xs">{{ .SearchErrorMessage }}</span>
|
<option value="LibGen Fiction">LibGen Fiction</option>
|
||||||
{{ end }}
|
<option value="LibGen Non-fiction">LibGen Non-fiction</option>
|
||||||
</div>
|
</select>
|
||||||
<div class="inline-block min-w-full overflow-hidden rounded shadow">
|
</div>
|
||||||
<table
|
<button type="submit"
|
||||||
class="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm md:text-sm"
|
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>
|
||||||
<thead class="text-gray-800 dark:text-gray-400">
|
</button>
|
||||||
<tr>
|
</form>
|
||||||
<th
|
{{ if .SearchErrorMessage }}
|
||||||
scope="col"
|
<span class="text-red-400 text-xs">{{ .SearchErrorMessage }}</span>
|
||||||
class="w-12 p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
{{ end }}
|
||||||
></th>
|
</div>
|
||||||
<th
|
<div class="inline-block min-w-full overflow-hidden rounded shadow">
|
||||||
scope="col"
|
<table class="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm md:text-sm">
|
||||||
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
<thead class="text-gray-800 dark:text-gray-400">
|
||||||
>
|
<tr>
|
||||||
Document
|
<th scope="col"
|
||||||
</th>
|
class="w-12 p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"></th>
|
||||||
<th
|
<th scope="col"
|
||||||
scope="col"
|
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Document</th>
|
||||||
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
<th scope="col"
|
||||||
>
|
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Series</th>
|
||||||
Series
|
<th scope="col"
|
||||||
</th>
|
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Type</th>
|
||||||
<th
|
<th scope="col"
|
||||||
scope="col"
|
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Size</th>
|
||||||
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
<th scope="col"
|
||||||
>
|
class="p-3 hidden md:block font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">
|
||||||
Type
|
Date
|
||||||
</th>
|
</th>
|
||||||
<th
|
</tr>
|
||||||
scope="col"
|
</thead>
|
||||||
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
<tbody class="text-black dark:text-white">
|
||||||
>
|
{{ if not .Data }}
|
||||||
Size
|
<tr>
|
||||||
</th>
|
<td class="text-center p-3" colspan="6">No Results</td>
|
||||||
<th
|
</tr>
|
||||||
scope="col"
|
{{ end }} {{ range $item := .Data }}
|
||||||
class="p-3 hidden md:block font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
<tr>
|
||||||
>
|
<td class="p-3 border-b border-gray-200 text-gray-500 dark:text-gray-500">
|
||||||
Date
|
<form action="./search" method="POST">
|
||||||
</th>
|
<input class="hidden"
|
||||||
</tr>
|
type="text"
|
||||||
</thead>
|
id="source"
|
||||||
<tbody class="text-black dark:text-white">
|
name="source"
|
||||||
{{ if not .Data }}
|
value="{{ $.Source }}" />
|
||||||
<tr>
|
<input class="hidden"
|
||||||
<td class="text-center p-3" colspan="6">No Results</td>
|
type="text"
|
||||||
</tr>
|
id="title"
|
||||||
{{ end }}
|
name="title"
|
||||||
{{ range $item := .Data }}
|
value="{{ $item.Title }}" />
|
||||||
<tr>
|
<input class="hidden"
|
||||||
<td
|
type="text"
|
||||||
class="p-3 border-b border-gray-200 text-gray-500 dark:text-gray-500"
|
id="author"
|
||||||
>
|
name="author"
|
||||||
<form action="./search" method="POST">
|
value="{{ $item.Author }}" />
|
||||||
<input
|
<button name="id" value="{{ $item.ID }}">{{ template "svg/download" }}</button>
|
||||||
class="hidden"
|
</form>
|
||||||
type="text"
|
</td>
|
||||||
id="source"
|
<td class="p-3 border-b border-gray-200">{{ $item.Author }} - {{ $item.Title }}</td>
|
||||||
name="source"
|
<td class="p-3 border-b border-gray-200">
|
||||||
value="{{ $.Source }}"
|
<p>{{ or $item.Series "N/A" }}</p>
|
||||||
/>
|
</td>
|
||||||
<input
|
<td class="p-3 border-b border-gray-200">
|
||||||
class="hidden"
|
<p>{{ or $item.FileType "N/A" }}</p>
|
||||||
type="text"
|
</td>
|
||||||
id="title"
|
<td class="p-3 border-b border-gray-200">
|
||||||
name="title"
|
<p>{{ or $item.FileSize "N/A" }}</p>
|
||||||
value="{{ $item.Title }}"
|
</td>
|
||||||
/>
|
<td class="hidden md:table-cell p-3 border-b border-gray-200">
|
||||||
<input
|
<p>{{ or $item.UploadDate "N/A" }}</p>
|
||||||
class="hidden"
|
</td>
|
||||||
type="text"
|
</tr>
|
||||||
id="author"
|
{{ end }}
|
||||||
name="author"
|
</tbody>
|
||||||
value="{{ $item.Author }}"
|
</table>
|
||||||
/>
|
|
||||||
<button name="id" value="{{ $item.ID }}">
|
|
||||||
{{ template "svg/download" }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
<td class="p-3 border-b border-gray-200">
|
|
||||||
{{ $item.Author }} -
|
|
||||||
{{ $item.Title }}
|
|
||||||
</td>
|
|
||||||
<td class="p-3 border-b border-gray-200">
|
|
||||||
<p>{{ or $item.Series "N/A" }}</p>
|
|
||||||
</td>
|
|
||||||
<td class="p-3 border-b border-gray-200">
|
|
||||||
<p>{{ or $item.FileType "N/A" }}</p>
|
|
||||||
</td>
|
|
||||||
<td class="p-3 border-b border-gray-200">
|
|
||||||
<p>{{ or $item.FileSize "N/A" }}</p>
|
|
||||||
</td>
|
|
||||||
<td class="hidden md:table-cell p-3 border-b border-gray-200">
|
|
||||||
<p>{{ or $item.UploadDate "N/A" }}</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{{ end }}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
@@ -2,166 +2,123 @@
|
|||||||
{{ define "title" }}Settings{{ end }}
|
{{ define "title" }}Settings{{ end }}
|
||||||
{{ define "header" }}<a href="./settings">Settings</a>{{ end }}
|
{{ define "header" }}<a href="./settings">Settings</a>{{ end }}
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<div class="w-full flex flex-col md:flex-row gap-4">
|
<div class="w-full flex flex-col md:flex-row gap-4">
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div class="flex flex-col p-4 items-center rounded shadow-lg md:w-60 lg:w-80 bg-white dark:bg-gray-700 text-gray-500 dark:text-white">
|
||||||
class="flex flex-col p-4 items-center rounded shadow-lg md:w-60 lg:w-80 bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
{{ template "svg/user" (dict "Size" 60) }}
|
||||||
>
|
<p class="text-lg">{{ .Authorization.UserName }}</p>
|
||||||
{{ template "svg/user" (dict "Size" 60) }}
|
|
||||||
<p class="text-lg">{{ .Authorization.UserName }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-4 grow">
|
|
||||||
<div
|
|
||||||
class="flex flex-col gap-2 grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
|
||||||
>
|
|
||||||
<p class="text-lg font-semibold mb-2">Change Password</p>
|
|
||||||
<form
|
|
||||||
class="flex gap-4 flex-col lg:flex-row"
|
|
||||||
action="./settings"
|
|
||||||
method="POST"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col grow">
|
|
||||||
<div class="flex relative">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
|
||||||
>
|
|
||||||
{{ template "svg/password" (dict "Size" 15) }}
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
|
||||||
placeholder="Password"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col grow">
|
|
||||||
<div class="flex relative">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
|
||||||
>
|
|
||||||
{{ template "svg/password" (dict "Size" 15) }}
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="new_password"
|
|
||||||
name="new_password"
|
|
||||||
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
|
||||||
placeholder="New Password"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="lg:w-60">
|
|
||||||
{{ template "component/button" (dict
|
|
||||||
"Title" "Submit"
|
|
||||||
"Variant" "Secondary"
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{{ if .PasswordErrorMessage }}
|
|
||||||
<span class="text-red-400 text-xs">{{ .PasswordErrorMessage }}</span>
|
|
||||||
{{ else if .PasswordMessage }}
|
|
||||||
<span class="text-green-400 text-xs">{{ .PasswordMessage }}</span>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="flex flex-col grow gap-2 p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
|
||||||
>
|
|
||||||
<p class="text-lg font-semibold mb-2">Change Timezone</p>
|
|
||||||
<form
|
|
||||||
class="flex gap-4 flex-col lg:flex-row"
|
|
||||||
action="./settings"
|
|
||||||
method="POST"
|
|
||||||
>
|
|
||||||
<div class="flex relative grow">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
|
||||||
>
|
|
||||||
{{ template "svg/clock" (dict "Size" 15) }}
|
|
||||||
</span>
|
|
||||||
<select
|
|
||||||
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
|
||||||
id="timezone"
|
|
||||||
name="timezone"
|
|
||||||
>
|
|
||||||
{{ range $item := getTimeZones }}
|
|
||||||
<option
|
|
||||||
{{ if (eq $item $.Data.Timezone) }}selected{{ end }}
|
|
||||||
value="{{ $item }}"
|
|
||||||
>
|
|
||||||
{{ $item }}
|
|
||||||
</option>
|
|
||||||
{{ end }}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="lg:w-60">
|
|
||||||
{{ template "component/button" (dict
|
|
||||||
"Title" "Submit"
|
|
||||||
"Variant" "Secondary"
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{{ if .TimeOffsetErrorMessage }}
|
|
||||||
<span class="text-red-400 text-xs"
|
|
||||||
>{{ .TimeOffsetErrorMessage }}</span
|
|
||||||
>
|
|
||||||
{{ else if .TimeOffsetMessage }}
|
|
||||||
<span class="text-green-400 text-xs">{{ .TimeOffsetMessage }}</span>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="flex flex-col grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
|
||||||
>
|
|
||||||
<p class="text-lg font-semibold">Devices</p>
|
|
||||||
<table class="min-w-full bg-white dark:bg-gray-700 text-sm">
|
|
||||||
<thead class="text-gray-800 dark:text-gray-400">
|
|
||||||
<tr>
|
|
||||||
<th
|
|
||||||
scope="col"
|
|
||||||
class="p-3 pl-0 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
|
||||||
>
|
|
||||||
Name
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
scope="col"
|
|
||||||
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
|
||||||
>
|
|
||||||
Last Sync
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
scope="col"
|
|
||||||
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
|
||||||
>
|
|
||||||
Created
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="text-black dark:text-white">
|
|
||||||
{{ if not .Data.Devices }}
|
|
||||||
<tr>
|
|
||||||
<td class="text-center p-3" colspan="3">No Results</td>
|
|
||||||
</tr>
|
|
||||||
{{ end }}
|
|
||||||
{{ range $device := .Data.Devices }}
|
|
||||||
<tr>
|
|
||||||
<td class="p-3 pl-0">
|
|
||||||
<p>{{ $device.DeviceName }}</p>
|
|
||||||
</td>
|
|
||||||
<td class="p-3">
|
|
||||||
<p>{{ $device.LastSynced }}</p>
|
|
||||||
</td>
|
|
||||||
<td class="p-3">
|
|
||||||
<p>{{ $device.CreatedAt }}</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{{ end }}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-col gap-4 grow">
|
||||||
|
<div class="flex flex-col gap-2 grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white">
|
||||||
|
<p class="text-lg font-semibold mb-2">Change Password</p>
|
||||||
|
<form class="flex gap-4 flex-col lg:flex-row"
|
||||||
|
action="./settings"
|
||||||
|
method="POST">
|
||||||
|
<div class="flex flex-col grow">
|
||||||
|
<div class="flex relative">
|
||||||
|
<span class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm">
|
||||||
|
{{ template "svg/password" (dict "Size" 15) }}
|
||||||
|
</span>
|
||||||
|
<input type="password"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
||||||
|
placeholder="Password" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col grow">
|
||||||
|
<div class="flex relative">
|
||||||
|
<span class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm">
|
||||||
|
{{ template "svg/password" (dict "Size" 15) }}
|
||||||
|
</span>
|
||||||
|
<input type="password"
|
||||||
|
id="new_password"
|
||||||
|
name="new_password"
|
||||||
|
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
||||||
|
placeholder="New Password" />
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
{{ else if .PasswordMessage }}
|
||||||
|
<span class="text-green-400 text-xs">{{ .PasswordMessage }}</span>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col grow gap-2 p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white">
|
||||||
|
<p class="text-lg font-semibold mb-2">Change Timezone</p>
|
||||||
|
<form class="flex gap-4 flex-col lg:flex-row"
|
||||||
|
action="./settings"
|
||||||
|
method="POST">
|
||||||
|
<div class="flex relative grow">
|
||||||
|
<span class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm">
|
||||||
|
{{ template "svg/clock" (dict "Size" 15) }}
|
||||||
|
</span>
|
||||||
|
<select class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
||||||
|
id="timezone"
|
||||||
|
name="timezone">
|
||||||
|
{{ range $item := getTimeZones }}
|
||||||
|
<option {{ if (eq $item $.Data.Timezone) }}selected{{ end }} value="{{ $item }}">
|
||||||
|
{{ $item }}
|
||||||
|
</option>
|
||||||
|
{{ end }}
|
||||||
|
</select>
|
||||||
|
</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>
|
||||||
|
{{ else if .TimeOffsetMessage }}
|
||||||
|
<span class="text-green-400 text-xs">{{ .TimeOffsetMessage }}</span>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white">
|
||||||
|
<p class="text-lg font-semibold">Devices</p>
|
||||||
|
<table class="min-w-full bg-white dark:bg-gray-700 text-sm">
|
||||||
|
<thead class="text-gray-800 dark:text-gray-400">
|
||||||
|
<tr>
|
||||||
|
<th scope="col"
|
||||||
|
class="p-3 pl-0 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">
|
||||||
|
Name
|
||||||
|
</th>
|
||||||
|
<th scope="col"
|
||||||
|
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">
|
||||||
|
Last Sync
|
||||||
|
</th>
|
||||||
|
<th scope="col"
|
||||||
|
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Created</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-black dark:text-white">
|
||||||
|
{{ if not .Data.Devices }}
|
||||||
|
<tr>
|
||||||
|
<td class="text-center p-3" colspan="3">No Results</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
{{ range $device := .Data.Devices }}
|
||||||
|
<tr>
|
||||||
|
<td class="p-3 pl-0">
|
||||||
|
<p>{{ $device.DeviceName }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="p-3">
|
||||||
|
<p>{{ $device.LastSynced }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="p-3">
|
||||||
|
<p>{{ $device.CreatedAt }}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
Reference in New Issue
Block a user