Merge branch 'next'
This commit is contained in:
commit
dcdff94712
7
.gitignore
vendored
7
.gitignore
vendored
@ -1,2 +1,5 @@
|
||||
node_modules
|
||||
.credentials
|
||||
/server
|
||||
/manager
|
||||
*.properties
|
||||
.DS_Store
|
||||
*.csv
|
8
Dockerfile
Normal file
8
Dockerfile
Normal file
@ -0,0 +1,8 @@
|
||||
FROM golang:1.16
|
||||
WORKDIR /src
|
||||
COPY . ./
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o server ./cmd/serve
|
||||
|
||||
FROM scratch
|
||||
COPY --from=0 /src/server ./
|
||||
CMD ["/server"]
|
34
Makefile
Normal file
34
Makefile
Normal file
@ -0,0 +1,34 @@
|
||||
.PHONY: up down run-server run-manager test
|
||||
|
||||
GOFILES=$(shell find . -name '*.go' -o -name 'go.*')
|
||||
STATICFILES=$(shell find . -name '*.js' -o -name '*.css' -o -name '*.html')
|
||||
SQLFILES=$(shell find . -name '*.sql')
|
||||
|
||||
ifneq (,$(wildcard ./local.properties))
|
||||
include local.properties
|
||||
export
|
||||
endif
|
||||
|
||||
build: server manager
|
||||
|
||||
run-server: build
|
||||
./server
|
||||
|
||||
run-manager: build
|
||||
./manager
|
||||
|
||||
server: $(GOFILES) $(STATICFILES)
|
||||
go build -o server ./cmd/serve
|
||||
|
||||
manager: $(GOFILES) $(SQLFILES)
|
||||
go build -o manager ./cmd/manage
|
||||
|
||||
test:
|
||||
go test ./... -cover
|
||||
|
||||
# dev dependencies
|
||||
up:
|
||||
docker compose up -d
|
||||
|
||||
down:
|
||||
docker compose down
|
21
book/book.go
Normal file
21
book/book.go
Normal file
@ -0,0 +1,21 @@
|
||||
package book
|
||||
|
||||
type Book struct {
|
||||
ID int `json:"-"`
|
||||
Title string `json:"title"`
|
||||
Authors []string `json:"authors"`
|
||||
SortAuthor string `json:"sortAuthor"`
|
||||
ISBN10 string `json:"isbn-10"`
|
||||
ISBN13 string `json:"isbn-13"`
|
||||
Format string `json:"format"`
|
||||
Genre string `json:"genre"`
|
||||
Publisher string `json:"publisher"`
|
||||
Series string `json:"series"`
|
||||
Volume string `json:"volume"`
|
||||
Year string `json:"year"`
|
||||
Signed bool `json:"signed"`
|
||||
Description string `json:"description"`
|
||||
Notes string `json:"notes"`
|
||||
OnLoan string `json:"onLoan"`
|
||||
CoverURL string `json:"coverURL"`
|
||||
}
|
113
cmd/manage/events.go
Normal file
113
cmd/manage/events.go
Normal file
@ -0,0 +1,113 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"git.yetaga.in/alazyreader/library/book"
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
// error message
|
||||
type EventError struct {
|
||||
tcell.EventTime
|
||||
err error
|
||||
}
|
||||
|
||||
func NewEventError(err error) *EventError {
|
||||
e := &EventError{err: err}
|
||||
e.SetEventNow()
|
||||
return e
|
||||
}
|
||||
|
||||
// save change to book
|
||||
type EventBookUpdate struct {
|
||||
tcell.EventTime
|
||||
book *book.Book
|
||||
}
|
||||
|
||||
func NewEventBookUpdate(b *book.Book) *EventBookUpdate {
|
||||
e := &EventBookUpdate{book: b}
|
||||
e.SetEventNow()
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *EventBookUpdate) Book() *book.Book {
|
||||
return e.book
|
||||
}
|
||||
|
||||
// open new book in display
|
||||
type EventLoadBook struct {
|
||||
tcell.EventTime
|
||||
ID int
|
||||
}
|
||||
|
||||
func NewEventLoadBook(id int) *EventLoadBook {
|
||||
e := &EventLoadBook{ID: id}
|
||||
e.SetEventNow()
|
||||
return e
|
||||
}
|
||||
|
||||
// open new book in display
|
||||
type EventEnterBook struct {
|
||||
tcell.EventTime
|
||||
}
|
||||
|
||||
func NewEventEnterBook() *EventEnterBook {
|
||||
e := &EventEnterBook{}
|
||||
e.SetEventNow()
|
||||
return e
|
||||
}
|
||||
|
||||
// switch back to menu control
|
||||
type EventExitBook struct {
|
||||
tcell.EventTime
|
||||
}
|
||||
|
||||
func NewEventExitBook() *EventExitBook {
|
||||
e := &EventExitBook{}
|
||||
e.SetEventNow()
|
||||
return e
|
||||
}
|
||||
|
||||
// open import window
|
||||
type EventOpenImport struct {
|
||||
tcell.EventTime
|
||||
}
|
||||
|
||||
func NewEventOpenImport() *EventOpenImport {
|
||||
e := &EventOpenImport{}
|
||||
e.SetEventNow()
|
||||
return e
|
||||
}
|
||||
|
||||
// attempt to import given filename.csv
|
||||
type EventAttemptImport struct {
|
||||
tcell.EventTime
|
||||
filename string
|
||||
}
|
||||
|
||||
func NewEventAttemptImport(f string) *EventAttemptImport {
|
||||
e := &EventAttemptImport{filename: f}
|
||||
e.SetEventNow()
|
||||
return e
|
||||
}
|
||||
|
||||
// close import window
|
||||
type EventCloseImport struct {
|
||||
tcell.EventTime
|
||||
}
|
||||
|
||||
func NewEventCloseImport() *EventCloseImport {
|
||||
e := &EventCloseImport{}
|
||||
e.SetEventNow()
|
||||
return e
|
||||
}
|
||||
|
||||
// quit
|
||||
type EventQuit struct {
|
||||
tcell.EventTime
|
||||
}
|
||||
|
||||
func NewEventQuit() *EventQuit {
|
||||
e := &EventQuit{}
|
||||
e.SetEventNow()
|
||||
return e
|
||||
}
|
336
cmd/manage/main.go
Normal file
336
cmd/manage/main.go
Normal file
@ -0,0 +1,336 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.yetaga.in/alazyreader/library/book"
|
||||
"git.yetaga.in/alazyreader/library/config"
|
||||
"git.yetaga.in/alazyreader/library/database"
|
||||
"git.yetaga.in/alazyreader/library/importer"
|
||||
"git.yetaga.in/alazyreader/library/ui"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
)
|
||||
|
||||
// State holds the UI state keys=>value map and manages access to the map with a mutex
|
||||
type State struct {
|
||||
m sync.Mutex
|
||||
stateMap map[string]interface{}
|
||||
}
|
||||
|
||||
// key, present
|
||||
func (s *State) Get(key string) interface{} {
|
||||
s.m.Lock()
|
||||
defer s.m.Unlock()
|
||||
if s.stateMap == nil {
|
||||
s.stateMap = make(map[string]interface{})
|
||||
}
|
||||
k, ok := s.stateMap[key]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return k
|
||||
}
|
||||
|
||||
// key, value
|
||||
func (s *State) Set(key string, value interface{}) {
|
||||
s.m.Lock()
|
||||
defer s.m.Unlock()
|
||||
if s.stateMap == nil {
|
||||
s.stateMap = make(map[string]interface{})
|
||||
}
|
||||
s.stateMap[key] = value
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// UI states
|
||||
const (
|
||||
IN_MENU = iota
|
||||
IN_BOOK
|
||||
IN_IMPORT
|
||||
)
|
||||
|
||||
func main() {
|
||||
var c config.Config
|
||||
err := envconfig.Process("library", &c)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
// create state
|
||||
state := State{}
|
||||
|
||||
// set up DB connection
|
||||
if c.DBUser == "" || c.DBPass == "" || c.DBHost == "" || c.DBPort == "" || c.DBName == "" {
|
||||
if c.DBPass != "" { // obscure password
|
||||
c.DBPass = c.DBPass[0:max(3, len(c.DBPass))] + strings.Repeat("*", max(0, len(c.DBPass)-3))
|
||||
}
|
||||
log.Fatalf("vars: %+v", c)
|
||||
}
|
||||
lib, err := database.NewMySQLConnection(c.DBUser, c.DBPass, c.DBHost, c.DBPort, c.DBName)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
err = lib.PrepareDatabase(context.Background())
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
_, _, err = lib.RunMigrations(context.Background())
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
books, err := lib.GetAllBooks(context.Background())
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
state.Set("library", books)
|
||||
|
||||
screen, err := tcell.NewScreen()
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
err = screen.Init()
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
// cleanup our screen and log if we panic and crash out somewhere
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if screen != nil {
|
||||
screen.Fini()
|
||||
}
|
||||
fmt.Println("fatal panic;", r)
|
||||
if c.Debug {
|
||||
fmt.Println("stacktrace: \n" + string(debug.Stack()))
|
||||
}
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// book list and options menu (left column)
|
||||
l := ui.NewList(Titles(state.Get("library").([]book.Book)), 0)
|
||||
menu := ui.NewBox(
|
||||
"library",
|
||||
[]string{"˄˅ select", "⏎ edit", "(n)ew", "(i)mport", "(q)uit"},
|
||||
ui.Contents{{
|
||||
Offsets: ui.Offsets{Top: 1, Left: 2, Bottom: -2, Right: -2},
|
||||
Container: l,
|
||||
}},
|
||||
ui.StyleActive,
|
||||
false,
|
||||
)
|
||||
activeBookDetails := ui.NewBookDetails(&book.Book{})
|
||||
|
||||
// book display (right column)
|
||||
activeBook := ui.NewBox(
|
||||
"book",
|
||||
[]string{"˄˅ select", "⏎ edit", "(esc) close"},
|
||||
ui.Contents{{
|
||||
Offsets: ui.Offsets{Top: 1, Left: 2, Bottom: 0, Right: 0},
|
||||
Container: activeBookDetails,
|
||||
}},
|
||||
ui.StyleInactive,
|
||||
false,
|
||||
)
|
||||
|
||||
// parent container
|
||||
container := ui.NewContainer(
|
||||
ui.Contents{
|
||||
{Container: menu, Offsets: ui.Offsets{Percent: 1}},
|
||||
{Container: activeBook, Offsets: ui.Offsets{Percent: 2}},
|
||||
},
|
||||
ui.LayoutHorizontalPercent,
|
||||
)
|
||||
|
||||
// import pop-up
|
||||
wd, _ := os.Getwd()
|
||||
fileSelector := ui.NewEditableTextLine(wd)
|
||||
fileSelector.ResetCursor(false)
|
||||
fileSelector.SetStyle(ui.StyleActive.Underline(true))
|
||||
popup := ui.NewBox(
|
||||
"import csv file",
|
||||
[]string{"⏎ submit", "(esc)close"},
|
||||
ui.Contents{
|
||||
{Container: fileSelector, Offsets: ui.Offsets{Top: 2, Left: 2}},
|
||||
},
|
||||
ui.StyleActive,
|
||||
false,
|
||||
)
|
||||
popup.SetVisible(false)
|
||||
|
||||
// error pop-up
|
||||
errorMessage := ui.NewEditableTextLine("")
|
||||
errorPopup := ui.NewBox(
|
||||
"error",
|
||||
[]string{"⏎ close"},
|
||||
ui.Contents{
|
||||
{Container: errorMessage, Offsets: ui.Offsets{Top: 1, Left: 1}},
|
||||
},
|
||||
ui.StyleActive.Bold(true).Foreground(tcell.ColorRed),
|
||||
false,
|
||||
)
|
||||
errorPopup.SetVisible(false)
|
||||
|
||||
// init
|
||||
screen.Clear()
|
||||
w, h := screen.Size()
|
||||
container.SetSize(0, 0, h, w)
|
||||
container.Draw(screen)
|
||||
screen.Sync()
|
||||
|
||||
// init UI state
|
||||
state.Set("ui_state", IN_MENU)
|
||||
screen.PostEvent(NewEventLoadBook(l.SelectedID()))
|
||||
|
||||
// UI loop
|
||||
for {
|
||||
e := screen.PollEvent()
|
||||
switch v := e.(type) {
|
||||
case *tcell.EventError:
|
||||
fmt.Fprintf(os.Stderr, "%v", v)
|
||||
screen.Beep()
|
||||
case *tcell.EventKey: // input handling
|
||||
curr := state.Get("ui_state").(int)
|
||||
if curr == IN_MENU {
|
||||
if v.Key() == tcell.KeyUp && l.Selected() > 0 {
|
||||
l.SetSelected(l.Selected() - 1)
|
||||
screen.PostEvent(NewEventLoadBook(l.SelectedID()))
|
||||
}
|
||||
if v.Key() == tcell.KeyDown && l.Selected() < len(l.ListMembers())-1 {
|
||||
l.SetSelected(l.Selected() + 1)
|
||||
screen.PostEvent(NewEventLoadBook(l.SelectedID()))
|
||||
}
|
||||
if v.Key() == tcell.KeyEnter {
|
||||
screen.PostEvent(NewEventEnterBook())
|
||||
}
|
||||
if v.Rune() == 'i' {
|
||||
screen.PostEvent(NewEventOpenImport())
|
||||
}
|
||||
if v.Rune() == 'q' {
|
||||
screen.PostEvent(NewEventQuit())
|
||||
}
|
||||
} else if curr == IN_BOOK {
|
||||
if v.Key() == tcell.KeyEsc {
|
||||
screen.PostEvent(NewEventExitBook())
|
||||
}
|
||||
} else if curr == IN_IMPORT {
|
||||
if v.Key() == tcell.KeyEsc {
|
||||
fileSelector.SetText(wd)
|
||||
fileSelector.ResetCursor(false)
|
||||
screen.PostEvent(NewEventCloseImport())
|
||||
}
|
||||
if v.Key() == tcell.KeyBackspace || v.Key() == tcell.KeyBackspace2 {
|
||||
fileSelector.DeleteAtCursor()
|
||||
} else if v.Key() == tcell.KeyRight {
|
||||
fileSelector.MoveCursor(1)
|
||||
|
||||
} else if v.Key() == tcell.KeyLeft {
|
||||
fileSelector.MoveCursor(-1)
|
||||
} else if v.Key() == tcell.KeyEnter {
|
||||
screen.PostEvent(NewEventAttemptImport(fileSelector.Text()))
|
||||
} else if v.Rune() != 0 {
|
||||
fileSelector.InsertAtCursor(v.Rune())
|
||||
}
|
||||
}
|
||||
case *tcell.EventResize: // screen redraw
|
||||
w, h := screen.Size()
|
||||
container.SetSize(0, 0, h, w)
|
||||
case *EventBookUpdate:
|
||||
// TK
|
||||
case *EventEnterBook:
|
||||
activeBook.SetStyle(ui.StyleActive)
|
||||
menu.SetStyle(ui.StyleInactive)
|
||||
state.Set("ui_state", IN_BOOK)
|
||||
case *EventExitBook:
|
||||
state.Set("ui_state", IN_MENU)
|
||||
activeBook.SetStyle(ui.StyleInactive)
|
||||
menu.SetStyle(ui.StyleActive)
|
||||
case *EventLoadBook:
|
||||
activeBookDetails.SetBook(GetBookByID(v.ID, books))
|
||||
case *EventOpenImport:
|
||||
state.Set("ui_state", IN_IMPORT)
|
||||
menu.SetStyle(ui.StyleInactive)
|
||||
popup.SetVisible(true)
|
||||
popup.SetSize(6, 3, 5, 80)
|
||||
case *EventAttemptImport:
|
||||
// this will block other events, but it shouldn't take too long...
|
||||
f, err := os.Open(v.filename)
|
||||
if err != nil {
|
||||
screen.PostEvent(NewEventError(err))
|
||||
continue
|
||||
}
|
||||
books, err := importer.CSVToBooks(f)
|
||||
if err != nil {
|
||||
screen.PostEvent(NewEventError(err))
|
||||
continue
|
||||
}
|
||||
for b := range books {
|
||||
err = lib.AddBook(context.Background(), &books[b])
|
||||
if err != nil {
|
||||
screen.PostEvent(NewEventError(err))
|
||||
}
|
||||
}
|
||||
screen.PostEvent(NewEventCloseImport())
|
||||
allbooks, err := lib.GetAllBooks(context.Background())
|
||||
if err != nil {
|
||||
screen.PostEvent(NewEventError(err))
|
||||
}
|
||||
state.Set("library", allbooks)
|
||||
state.Set("ui_state", IN_MENU)
|
||||
case *EventCloseImport:
|
||||
state.Set("ui_state", IN_MENU)
|
||||
screen.HideCursor()
|
||||
menu.SetStyle(ui.StyleActive)
|
||||
popup.SetVisible(false)
|
||||
case *EventError:
|
||||
errorMessage.SetText(v.err.Error())
|
||||
errorPopup.SetVisible(true)
|
||||
case *EventQuit:
|
||||
screen.Fini()
|
||||
fmt.Printf("Thank you for playing Wing Commander!\n\n")
|
||||
return
|
||||
case *tcell.EventInterrupt:
|
||||
case *tcell.EventMouse:
|
||||
case *tcell.EventTime:
|
||||
default:
|
||||
}
|
||||
// repaint
|
||||
l.SetMembers(Titles(state.Get("library").([]book.Book)))
|
||||
container.Draw(screen)
|
||||
popup.Draw(screen)
|
||||
errorPopup.Draw(screen)
|
||||
screen.Show()
|
||||
}
|
||||
}
|
||||
|
||||
func Titles(lb []book.Book) []ui.ListKeyValue {
|
||||
r := []ui.ListKeyValue{}
|
||||
for i := range lb {
|
||||
r = append(r, ui.ListKeyValue{
|
||||
Key: lb[i].ID,
|
||||
Value: lb[i].Title,
|
||||
})
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func GetBookByID(id int, lb []book.Book) *book.Book {
|
||||
for i := range lb {
|
||||
if lb[i].ID == id {
|
||||
return &lb[i]
|
||||
}
|
||||
}
|
||||
return &book.Book{}
|
||||
}
|
100
cmd/serve/main.go
Normal file
100
cmd/serve/main.go
Normal file
@ -0,0 +1,100 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.yetaga.in/alazyreader/library/book"
|
||||
"git.yetaga.in/alazyreader/library/config"
|
||||
"git.yetaga.in/alazyreader/library/database"
|
||||
"git.yetaga.in/alazyreader/library/frontend"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
)
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
type Library interface {
|
||||
GetAllBooks(context.Context) ([]book.Book, error)
|
||||
}
|
||||
|
||||
type Router struct {
|
||||
static fs.FS
|
||||
lib Library
|
||||
}
|
||||
|
||||
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
if req.URL.Path == "/api" {
|
||||
APIHandler(r.lib).ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
StaticHandler(r.static).ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
func APIHandler(l Library) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
books, err := l.GetAllBooks(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
b, err := json.Marshal(books)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(b)
|
||||
w.Write([]byte("\n"))
|
||||
})
|
||||
}
|
||||
|
||||
func StaticHandler(f fs.FS) http.Handler {
|
||||
return http.FileServer(http.FS(f))
|
||||
}
|
||||
|
||||
func main() {
|
||||
var c config.Config
|
||||
err := envconfig.Process("library", &c)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
f, err := frontend.Root()
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
if c.DBUser == "" || c.DBPass == "" || c.DBHost == "" || c.DBPort == "" || c.DBName == "" {
|
||||
if c.DBPass != "" { // obscure password
|
||||
c.DBPass = c.DBPass[0:max(3, len(c.DBPass))] + strings.Repeat("*", max(0, len(c.DBPass)-3))
|
||||
}
|
||||
log.Fatalf("vars: %+v", c)
|
||||
}
|
||||
lib, err := database.NewMySQLConnection(c.DBUser, c.DBPass, c.DBHost, c.DBPort, c.DBName)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
err = lib.PrepareDatabase(context.Background())
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
latest, run, err := lib.RunMigrations(context.Background())
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
log.Printf("latest migration: %d; migrations run: %d", latest, run)
|
||||
r := &Router{
|
||||
static: f,
|
||||
lib: lib,
|
||||
}
|
||||
log.Println("Listening on http://localhost:8080/")
|
||||
log.Fatalln(http.ListenAndServe(":8080", r))
|
||||
}
|
10
config/config.go
Normal file
10
config/config.go
Normal file
@ -0,0 +1,10 @@
|
||||
package config
|
||||
|
||||
type Config struct {
|
||||
DBUser string
|
||||
DBPass string
|
||||
DBHost string
|
||||
DBPort string
|
||||
DBName string
|
||||
Debug bool
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
/* http://meyerweb.com/eric/tools/css/reset/
|
||||
v2.0 | 20110126
|
||||
License: none (public domain)
|
||||
*/
|
||||
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||
small, strike, strong, sub, sup, tt, var,
|
||||
b, u, i, center,
|
||||
dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend,
|
||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
article, aside, canvas, details, embed,
|
||||
figure, figcaption, footer, header, hgroup,
|
||||
menu, nav, output, ruby, section, summary,
|
||||
time, mark, audio, video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
}
|
||||
body {
|
||||
line-height: 1;
|
||||
}
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
}
|
||||
blockquote, q {
|
||||
quotes: none;
|
||||
}
|
||||
blockquote:before, blockquote:after,
|
||||
q:before, q:after {
|
||||
content: '';
|
||||
content: none;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
79
database/memory.go
Normal file
79
database/memory.go
Normal file
@ -0,0 +1,79 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"git.yetaga.in/alazyreader/library/book"
|
||||
)
|
||||
|
||||
type Memory struct {
|
||||
lock sync.Mutex
|
||||
shelf []book.Book
|
||||
}
|
||||
|
||||
func (m *Memory) GetAllBooks(_ context.Context) ([]book.Book, error) {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
if m.shelf == nil {
|
||||
m.shelf = []book.Book{}
|
||||
}
|
||||
|
||||
return m.shelf, nil
|
||||
}
|
||||
|
||||
func (m *Memory) AddBook(_ context.Context, b *book.Book) error {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
if m.shelf == nil {
|
||||
m.shelf = []book.Book{}
|
||||
}
|
||||
|
||||
m.shelf = append(m.shelf, *b)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Memory) UpdateBook(_ context.Context, old, new *book.Book) error {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
if m.shelf == nil {
|
||||
m.shelf = []book.Book{}
|
||||
return fmt.Errorf("book does not exist")
|
||||
}
|
||||
|
||||
if old.ID != new.ID {
|
||||
return fmt.Errorf("cannot change book ID")
|
||||
}
|
||||
|
||||
for i := range m.shelf {
|
||||
if m.shelf[i].ID == old.ID {
|
||||
m.shelf[i] = *new
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("book does not exist")
|
||||
}
|
||||
|
||||
func (m *Memory) DeleteBook(_ context.Context, b *book.Book) error {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
if m.shelf == nil {
|
||||
m.shelf = []book.Book{}
|
||||
return fmt.Errorf("book does not exist")
|
||||
}
|
||||
|
||||
for i := range m.shelf {
|
||||
if m.shelf[i].ID == b.ID {
|
||||
// reorder slice to remove book quickly
|
||||
m.shelf[i] = m.shelf[len(m.shelf)-1]
|
||||
m.shelf = m.shelf[:len(m.shelf)-1]
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("book does not exist")
|
||||
}
|
20
database/migrations/mysql/01-init.sql
Normal file
20
database/migrations/mysql/01-init.sql
Normal file
@ -0,0 +1,20 @@
|
||||
CREATE TABLE IF NOT EXISTS books(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
title VARCHAR(1024),
|
||||
authors VARCHAR(1024),
|
||||
sortauthor VARCHAR(1024),
|
||||
isbn10 VARCHAR(10),
|
||||
isbn13 VARCHAR(13),
|
||||
format VARCHAR(255),
|
||||
genre VARCHAR(255),
|
||||
publisher VARCHAR(255),
|
||||
series VARCHAR(255),
|
||||
volume VARCHAR(255),
|
||||
year VARCHAR(10),
|
||||
signed BOOLEAN,
|
||||
description TEXT,
|
||||
notes TEXT,
|
||||
onloan VARCHAR(255),
|
||||
coverurl VARCHAR(1024),
|
||||
PRIMARY KEY (id)
|
||||
)
|
301
database/mysql.go
Normal file
301
database/mysql.go
Normal file
@ -0,0 +1,301 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.yetaga.in/alazyreader/library/book"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
//go:embed migrations/mysql
|
||||
var migrationsFS embed.FS
|
||||
|
||||
type migration struct {
|
||||
id int
|
||||
name string
|
||||
content []byte
|
||||
}
|
||||
|
||||
type MySQL struct {
|
||||
connection *sql.DB
|
||||
tableName string
|
||||
versionTable string
|
||||
migrationsDirectory string
|
||||
}
|
||||
|
||||
func NewMySQLConnection(user, pass, host, port, db string) (*MySQL, error) {
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", user, pass, host, port, db) // what a strange syntax
|
||||
connection, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &MySQL{
|
||||
connection: connection,
|
||||
tableName: "books",
|
||||
versionTable: "migrations",
|
||||
migrationsDirectory: "migrations/mysql",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MySQL) PrepareDatabase(ctx context.Context) error {
|
||||
if m.connection == nil || m.migrationsDirectory == "" || m.versionTable == "" {
|
||||
return fmt.Errorf("uninitialized mysql client")
|
||||
}
|
||||
|
||||
tablecheck := `SELECT count(*) AS count
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_NAME = '` + m.versionTable + `'
|
||||
AND TABLE_SCHEMA in (SELECT DATABASE());`
|
||||
tableschema := `CREATE TABLE ` + m.versionTable + `(
|
||||
id INT NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
datetime DATE,
|
||||
PRIMARY KEY (id))`
|
||||
|
||||
var versionTableExists int
|
||||
m.connection.QueryRowContext(ctx, tablecheck).Scan(&versionTableExists)
|
||||
if versionTableExists != 0 {
|
||||
return nil
|
||||
}
|
||||
_, err := m.connection.ExecContext(ctx, tableschema)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *MySQL) GetLatestMigration(ctx context.Context) (int, error) {
|
||||
if m.connection == nil || m.migrationsDirectory == "" || m.versionTable == "" {
|
||||
return 0, fmt.Errorf("uninitialized mysql client")
|
||||
}
|
||||
|
||||
var latestMigration int
|
||||
err := m.connection.QueryRowContext(ctx, "SELECT COALESCE(MAX(id), 0) FROM "+m.versionTable).Scan(&latestMigration)
|
||||
return latestMigration, err
|
||||
}
|
||||
|
||||
func (m *MySQL) RunMigrations(ctx context.Context) (int, int, error) {
|
||||
if m.connection == nil || m.migrationsDirectory == "" || m.versionTable == "" {
|
||||
return 0, 0, fmt.Errorf("uninitialized mysql client")
|
||||
}
|
||||
|
||||
migrations := map[int]migration{}
|
||||
dir, err := migrationsFS.ReadDir(m.migrationsDirectory)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
for f := range dir {
|
||||
if dir[f].Type().IsRegular() {
|
||||
mig := migration{}
|
||||
id, name, err := parseMigrationFileName(dir[f].Name())
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
mig.id, mig.name = id, name
|
||||
mig.content, err = fs.ReadFile(migrationsFS, m.migrationsDirectory+"/"+dir[f].Name())
|
||||
migrations[mig.id] = mig
|
||||
}
|
||||
}
|
||||
|
||||
latestMigrationRan, err := m.GetLatestMigration(ctx)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
// exit if nothing to do (that is, there's no greater migration ID)
|
||||
if _, ok := migrations[latestMigrationRan+1]; !ok {
|
||||
return latestMigrationRan, 0, nil
|
||||
}
|
||||
|
||||
// loop over and apply migrations if required
|
||||
tx, err := m.connection.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return latestMigrationRan, 0, err
|
||||
}
|
||||
migrationsRun := 0
|
||||
for migrationsToRun := true; migrationsToRun; _, migrationsToRun = migrations[latestMigrationRan+1] {
|
||||
mig := migrations[latestMigrationRan+1]
|
||||
_, err := tx.ExecContext(ctx, string(mig.content))
|
||||
if err != nil {
|
||||
nestederr := tx.Rollback()
|
||||
if nestederr != nil {
|
||||
return latestMigrationRan, migrationsRun, nestederr
|
||||
}
|
||||
return latestMigrationRan, migrationsRun, err
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, "INSERT INTO "+m.versionTable+" (id, name, datetime) VALUES (?, ?, ?)", mig.id, mig.name, time.Now())
|
||||
if err != nil {
|
||||
nestederr := tx.Rollback()
|
||||
if nestederr != nil {
|
||||
return latestMigrationRan, migrationsRun, nestederr
|
||||
}
|
||||
return latestMigrationRan, migrationsRun, err
|
||||
}
|
||||
latestMigrationRan = latestMigrationRan + 1
|
||||
migrationsRun = migrationsRun + 1
|
||||
}
|
||||
err = tx.Commit()
|
||||
return latestMigrationRan, migrationsRun, err
|
||||
}
|
||||
|
||||
func (m *MySQL) GetAllBooks(ctx context.Context) ([]book.Book, error) {
|
||||
if m.connection == nil {
|
||||
return nil, fmt.Errorf("uninitialized mysql client")
|
||||
}
|
||||
|
||||
books := []book.Book{}
|
||||
rows, err := m.connection.QueryContext(ctx, `
|
||||
SELECT id,
|
||||
title,
|
||||
authors,
|
||||
sortauthor,
|
||||
isbn10,
|
||||
isbn13,
|
||||
format,
|
||||
genre,
|
||||
publisher,
|
||||
series,
|
||||
volume,
|
||||
year,
|
||||
signed,
|
||||
description,
|
||||
notes,
|
||||
onloan,
|
||||
coverurl
|
||||
FROM `+m.tableName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
b := book.Book{}
|
||||
var authors string
|
||||
err := rows.Scan(
|
||||
&b.ID, &b.Title, &authors,
|
||||
&b.SortAuthor, &b.ISBN10, &b.ISBN13,
|
||||
&b.Format, &b.Genre, &b.Publisher,
|
||||
&b.Series, &b.Volume, &b.Year,
|
||||
&b.Signed, &b.Description, &b.Notes,
|
||||
&b.OnLoan, &b.CoverURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.Authors = strings.Split(authors, ";")
|
||||
books = append(books, b)
|
||||
}
|
||||
|
||||
return books, nil
|
||||
}
|
||||
|
||||
func (m *MySQL) AddBook(ctx context.Context, b *book.Book) error {
|
||||
if m.connection == nil {
|
||||
return fmt.Errorf("uninitialized mysql client")
|
||||
}
|
||||
|
||||
res, err := m.connection.ExecContext(ctx, `
|
||||
INSERT INTO `+m.tableName+`
|
||||
(title, authors, sortauthor, isbn10, isbn13, format, genre, publisher, series, volume, year, signed, description, notes, onloan, coverurl)
|
||||
VALUES
|
||||
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
b.Title,
|
||||
strings.Join(b.Authors, ";"),
|
||||
b.SortAuthor,
|
||||
b.ISBN10,
|
||||
b.ISBN13,
|
||||
b.Format,
|
||||
b.Genre,
|
||||
b.Publisher,
|
||||
b.Series,
|
||||
b.Volume,
|
||||
b.Year,
|
||||
b.Signed,
|
||||
b.Description,
|
||||
b.Notes,
|
||||
b.OnLoan,
|
||||
b.CoverURL,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if i != 1 {
|
||||
return fmt.Errorf("unexpectedly updated more than one row: %d", i)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MySQL) UpdateBook(ctx context.Context, old, new *book.Book) error {
|
||||
if m.connection == nil {
|
||||
return fmt.Errorf("uninitialized mysql client")
|
||||
}
|
||||
if old.ID != new.ID {
|
||||
return fmt.Errorf("cannot change book ID")
|
||||
}
|
||||
|
||||
res, err := m.connection.ExecContext(ctx, `
|
||||
UPDATE `+m.tableName+`
|
||||
SET id=?
|
||||
title=?
|
||||
authors=?
|
||||
sortauthor=?
|
||||
isbn10=?
|
||||
isbn13=?
|
||||
format=?
|
||||
genre=?
|
||||
publisher=?
|
||||
series=?
|
||||
volume=?
|
||||
year=?
|
||||
signed=?
|
||||
description=?
|
||||
notes=?
|
||||
onloan=?
|
||||
coverurl=?
|
||||
WHERE id=?`,
|
||||
new.Title,
|
||||
strings.Join(new.Authors, ";"),
|
||||
new.SortAuthor,
|
||||
new.ISBN10,
|
||||
new.ISBN13,
|
||||
new.Format,
|
||||
new.Genre,
|
||||
new.Publisher,
|
||||
new.Series,
|
||||
new.Volume,
|
||||
new.Year,
|
||||
new.Signed,
|
||||
new.Description,
|
||||
new.Notes,
|
||||
new.OnLoan,
|
||||
new.CoverURL,
|
||||
old.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if i != 1 {
|
||||
return fmt.Errorf("unexpectedly updated more than one row: %d", i)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseMigrationFileName(filename string) (int, string, error) {
|
||||
sp := strings.SplitN(filename, "-", 2)
|
||||
i, err := strconv.Atoi(sp[0])
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
tr := strings.TrimSuffix(sp[1], ".sql")
|
||||
return i, tr, nil
|
||||
}
|
8
docker-compose.yml
Normal file
8
docker-compose.yml
Normal file
@ -0,0 +1,8 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:5.7
|
||||
ports:
|
||||
- 3306:3306
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=KigYBNCT9IU5XyB3ehzMLFWyI # for dev testing only, obviously.
|
BIN
favicon.ico
BIN
favicon.ico
Binary file not shown.
Before Width: | Height: | Size: 286 KiB |
BIN
frontend/files/favicon.ico
Normal file
BIN
frontend/files/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.3 KiB |
BIN
frontend/files/favicon.png
Normal file
BIN
frontend/files/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 69 KiB |
243
frontend/files/index.html
Normal file
243
frontend/files/index.html
Normal file
@ -0,0 +1,243 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Library</title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"
|
||||
/>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<link rel="icon" href="favicon.ico" type="image/x-icon" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Libre+Baskerville:400,700&display=swap"
|
||||
as="style"
|
||||
rel="stylesheet preload prefetch"
|
||||
/>
|
||||
<script type="text/javascript">
|
||||
var sortState = {
|
||||
sortBy: "sortAuthor",
|
||||
sortOrder: "asc",
|
||||
};
|
||||
|
||||
function init() {
|
||||
fetch("/api")
|
||||
.then((response) => response.json())
|
||||
.then((books) => {
|
||||
// prepare response
|
||||
books.forEach((book) => {
|
||||
book.sortTitle = titleCleaner(book.title);
|
||||
if (!book["isbn-10"] && book["isbn-13"]) {
|
||||
book["isbn-10"] = ISBNfromEAN(book["isbn-13"]);
|
||||
}
|
||||
if (!book.coverurl && book["isbn-10"]) {
|
||||
book.coverurl =
|
||||
`https://images-na.ssl-images-amazon.com/images/P/` +
|
||||
book["isbn-10"] +
|
||||
`.01.LZZ.jpg`;
|
||||
}
|
||||
});
|
||||
return books;
|
||||
})
|
||||
.then((books) => {
|
||||
document.getElementById("search").addEventListener("input", (e) => {
|
||||
search(books, e.target.value);
|
||||
});
|
||||
return books;
|
||||
})
|
||||
.then(renderTable);
|
||||
}
|
||||
|
||||
function search(books, searchBy) {
|
||||
searchBy = searchCleaner(searchBy);
|
||||
if (searchBy !== "") {
|
||||
books = books.filter(
|
||||
({ title, authors, genre, publisher, series, year }) => {
|
||||
return Object.values({
|
||||
title,
|
||||
authors: authors.join(" "),
|
||||
genre,
|
||||
publisher,
|
||||
series,
|
||||
year,
|
||||
}).find((field) => searchCleaner(field).indexOf(searchBy) !== -1);
|
||||
}
|
||||
);
|
||||
}
|
||||
renderTable(books);
|
||||
}
|
||||
|
||||
function renderTable(books, sortField) {
|
||||
if (sortField) {
|
||||
if (sortState.sortBy === sortField && sortState.sortOrder === "asc") {
|
||||
sortState.sortOrder = "desc";
|
||||
} else {
|
||||
sortState.sortOrder = "asc";
|
||||
}
|
||||
sortState.sortBy = sortField;
|
||||
}
|
||||
books.sort((one, two) =>
|
||||
(one[sortState.sortBy] + one["sortTitle"]).localeCompare(
|
||||
two[sortState.sortBy] + two["sortTitle"]
|
||||
)
|
||||
);
|
||||
if (sortState.sortOrder === "desc") {
|
||||
books.reverse();
|
||||
}
|
||||
books.forEach((e, i) => (e.rowNumber = i)); // re-key
|
||||
|
||||
// rendering
|
||||
var bookElement = document.getElementById("books");
|
||||
bookElement.innerHTML = TableTemplate(books);
|
||||
|
||||
// add listeners for selecting book to view
|
||||
Array.from(bookElement.querySelectorAll("tbody tr"))
|
||||
.slice(1) // remove header from Array
|
||||
.forEach((row) => {
|
||||
row.addEventListener("click", (e) => {
|
||||
// add listener to swap current book
|
||||
document.getElementById("current").innerHTML = BookTemplate(
|
||||
books[e.currentTarget.id]
|
||||
);
|
||||
});
|
||||
});
|
||||
// add sorting callbacks
|
||||
Array.from(
|
||||
bookElement.querySelectorAll("tbody tr th[data-sort-by]")
|
||||
).forEach((row) => {
|
||||
row.addEventListener("click", function (e) {
|
||||
renderTable(books, e.target.dataset.sortBy); // only add callback when there's a sortBy attribute
|
||||
});
|
||||
});
|
||||
// mark currently active column
|
||||
bookElement
|
||||
.querySelector("tbody tr th[data-sort-by=" + sortState.sortBy + "]")
|
||||
.classList.add(sortState.sortOrder);
|
||||
}
|
||||
|
||||
function titleCleaner(title) {
|
||||
return title
|
||||
.replace('"', "")
|
||||
.replace(":", "")
|
||||
.replace(/^(An?|The)\s/i, "");
|
||||
}
|
||||
|
||||
function searchCleaner(str) {
|
||||
return str
|
||||
.toLowerCase()
|
||||
.replace('"', "")
|
||||
.replace(":", "")
|
||||
.replace("'", "")
|
||||
.replace(" ", "");
|
||||
}
|
||||
|
||||
function ISBNfromEAN(EAN) {
|
||||
ISBN = EAN.slice(3, 12);
|
||||
var checkdigit =
|
||||
(11 -
|
||||
(ISBN.split("").reduce((s, n, k) => s + n * (10 - k), 0) % 11)) %
|
||||
11;
|
||||
return ISBN + (checkdigit === 10 ? "X" : checkdigit);
|
||||
}
|
||||
|
||||
function BookTemplate({
|
||||
"isbn-13": isbn13,
|
||||
authors,
|
||||
coverurl,
|
||||
description,
|
||||
format,
|
||||
notes,
|
||||
onLoan,
|
||||
publisher,
|
||||
series,
|
||||
signed,
|
||||
title,
|
||||
volume,
|
||||
year,
|
||||
}) {
|
||||
return `${coverurl ? `<img src="${coverurl}"/>` : ""}
|
||||
<h1 ${onLoan ? "class='onLoan' " : ""}>${title}</h1>
|
||||
<h2>${authors}</h2>
|
||||
<span>${isbn13}</span><br/>
|
||||
<span>${publisher}, ${year}</span><br/>
|
||||
${
|
||||
series
|
||||
? `<span>${series}${volume ? `, Volume ${volume}` : ""}</span><br/>`
|
||||
: ""
|
||||
}
|
||||
${signed ? "<span>Signed by the author ✒</span><br/>" : ""}
|
||||
<span>${format}</span>
|
||||
${onLoan ? `<h2 class="onLoan">On loan to ${onLoan}</h2>` : ""}
|
||||
<div class="description">
|
||||
<p>${description}</p>
|
||||
${notes ? `<span>Notes:</span><p>${notes}</p>` : ""}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function TableRowTemplate({
|
||||
"isbn-13": isbn13,
|
||||
authors,
|
||||
onLoan,
|
||||
publisher,
|
||||
rowNumber,
|
||||
signed,
|
||||
title,
|
||||
year,
|
||||
}) {
|
||||
return `<tr class="tRow ${onLoan ? "onLoan" : ""}" id="${rowNumber}">
|
||||
<td class="title">
|
||||
${title} ${
|
||||
signed
|
||||
? '<span class="signed" title="Signed by the author" >✒</span>︎'
|
||||
: ""
|
||||
}
|
||||
</td>
|
||||
<td class="author">${authors}</td>
|
||||
<td class="publisher">${publisher}</td>
|
||||
<td class="year">${year}</td>
|
||||
<td class="isbn">${isbn13}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
function TableTemplate(books) {
|
||||
return `<table class="bookTable">
|
||||
<tr>
|
||||
<th data-sort-by="sortTitle" class="tHeader title">Title</th>
|
||||
<th data-sort-by="sortAuthor" class="tHeader author">Author</th>
|
||||
<th data-sort-by="publisher" class="tHeader publisher">Publisher</th>
|
||||
<th data-sort-by="year" class="tHeader year">Year</th>
|
||||
<th class="tHeader isbn">ISBN</th>
|
||||
</tr>${books.reduce((acc, book) => {
|
||||
return acc.concat(TableRowTemplate(book));
|
||||
}, "")} </table>`;
|
||||
}
|
||||
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
init();
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<div id="header">
|
||||
<h1>Library</h1>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://git.yetaga.in/alazyreader/library"
|
||||
>git</a
|
||||
>
|
||||
<div id="searchBox">
|
||||
<input
|
||||
id="search"
|
||||
type="text"
|
||||
name="search"
|
||||
placeholder="Search..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div id="current">No Book Selected</div>
|
||||
<div id="books"></div>
|
||||
<!-- Table goes here -->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,3 +1,134 @@
|
||||
/* http://meyerweb.com/eric/tools/css/reset/
|
||||
v2.0 | 20110126
|
||||
License: none (public domain)
|
||||
*/
|
||||
|
||||
html,
|
||||
body,
|
||||
div,
|
||||
span,
|
||||
applet,
|
||||
object,
|
||||
iframe,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p,
|
||||
blockquote,
|
||||
pre,
|
||||
a,
|
||||
abbr,
|
||||
acronym,
|
||||
address,
|
||||
big,
|
||||
cite,
|
||||
code,
|
||||
del,
|
||||
dfn,
|
||||
em,
|
||||
img,
|
||||
ins,
|
||||
kbd,
|
||||
q,
|
||||
s,
|
||||
samp,
|
||||
small,
|
||||
strike,
|
||||
strong,
|
||||
sub,
|
||||
sup,
|
||||
tt,
|
||||
var,
|
||||
b,
|
||||
u,
|
||||
i,
|
||||
center,
|
||||
dl,
|
||||
dt,
|
||||
dd,
|
||||
ol,
|
||||
ul,
|
||||
li,
|
||||
fieldset,
|
||||
form,
|
||||
label,
|
||||
legend,
|
||||
table,
|
||||
caption,
|
||||
tbody,
|
||||
tfoot,
|
||||
thead,
|
||||
tr,
|
||||
th,
|
||||
td,
|
||||
article,
|
||||
aside,
|
||||
canvas,
|
||||
details,
|
||||
embed,
|
||||
figure,
|
||||
figcaption,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
menu,
|
||||
nav,
|
||||
output,
|
||||
ruby,
|
||||
section,
|
||||
summary,
|
||||
time,
|
||||
mark,
|
||||
audio,
|
||||
video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
menu,
|
||||
nav,
|
||||
section {
|
||||
display: block;
|
||||
}
|
||||
body {
|
||||
line-height: 1;
|
||||
}
|
||||
ol,
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
blockquote,
|
||||
q {
|
||||
quotes: none;
|
||||
}
|
||||
blockquote:before,
|
||||
blockquote:after,
|
||||
q:before,
|
||||
q:after {
|
||||
content: "";
|
||||
content: none;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
/* site CSS starts here */
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
@ -8,10 +139,10 @@ body {
|
||||
padding: 4px 10px;
|
||||
background-color: #f7f3dc;
|
||||
border-bottom: 2px solid #d8d0a0;
|
||||
font-family: 'Libre Baskerville', sans-serif;
|
||||
font-family: "Libre Baskerville", sans-serif;
|
||||
}
|
||||
|
||||
#header h1{
|
||||
#header h1 {
|
||||
font-size: xx-large;
|
||||
display: inline;
|
||||
}
|
||||
@ -31,7 +162,7 @@ body {
|
||||
padding: 2px 5px;
|
||||
border: none;
|
||||
border-bottom: 2px solid #d8d0a0;
|
||||
font-family: 'Libre Baskerville', sans-serif;
|
||||
font-family: "Libre Baskerville", sans-serif;
|
||||
}
|
||||
|
||||
#searchBox input:focus {
|
||||
@ -39,7 +170,7 @@ body {
|
||||
}
|
||||
|
||||
#searchBox input::placeholder {
|
||||
font-family: 'Libre Baskerville', sans-serif;
|
||||
font-family: "Libre Baskerville", sans-serif;
|
||||
color: #d8d0a0;
|
||||
}
|
||||
|
||||
@ -63,7 +194,7 @@ body {
|
||||
.bookTable th {
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
font-family: 'Libre Baskerville', sans-serif;
|
||||
font-family: "Libre Baskerville", sans-serif;
|
||||
}
|
||||
|
||||
.bookTable th[data-sort-by] {
|
||||
@ -97,7 +228,8 @@ body {
|
||||
bottom: 2px;
|
||||
}
|
||||
|
||||
.bookTable td, .bookTable th {
|
||||
.bookTable td,
|
||||
.bookTable th {
|
||||
padding: 5px;
|
||||
min-width: 50px;
|
||||
}
|
13
frontend/frontend.go
Normal file
13
frontend/frontend.go
Normal file
@ -0,0 +1,13 @@
|
||||
package frontend
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
//go:embed files
|
||||
var static embed.FS
|
||||
|
||||
func Root() (fs.FS, error) {
|
||||
return fs.Sub(static, "files")
|
||||
}
|
9
go.mod
Normal file
9
go.mod
Normal file
@ -0,0 +1,9 @@
|
||||
module git.yetaga.in/alazyreader/library
|
||||
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/gdamore/tcell v1.4.0
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/kelseyhightower/envconfig v1.4.0
|
||||
)
|
16
go.sum
Normal file
16
go.sum
Normal file
@ -0,0 +1,16 @@
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU=
|
||||
github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0=
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
|
||||
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
65
importer/csv.go
Normal file
65
importer/csv.go
Normal file
@ -0,0 +1,65 @@
|
||||
package importer
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"git.yetaga.in/alazyreader/library/book"
|
||||
)
|
||||
|
||||
func CSVToBooks(r io.Reader) ([]book.Book, error) {
|
||||
reader := csv.NewReader(r)
|
||||
header, err := reader.Read()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hmap := parseHeader(header)
|
||||
books := []book.Book{}
|
||||
|
||||
for {
|
||||
row, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return books, err
|
||||
}
|
||||
b := book.Book{
|
||||
Title: row[hmap["title"]],
|
||||
Authors: parseAuthors(row[hmap["author"]]),
|
||||
SortAuthor: row[hmap["authorlast"]],
|
||||
ISBN10: row[hmap["isbn-10"]],
|
||||
ISBN13: row[hmap["isbn-13"]],
|
||||
Format: row[hmap["format"]],
|
||||
Genre: row[hmap["genre"]],
|
||||
Publisher: row[hmap["publisher"]],
|
||||
Series: row[hmap["series"]],
|
||||
Volume: row[hmap["volume"]],
|
||||
Year: row[hmap["year"]],
|
||||
Signed: row[hmap["signed"]] == "yes", // convert from known string to bool
|
||||
Description: row[hmap["description"]],
|
||||
Notes: row[hmap["notes"]],
|
||||
OnLoan: row[hmap["onloan"]],
|
||||
CoverURL: row[hmap["coverurl"]],
|
||||
}
|
||||
books = append(books, b)
|
||||
}
|
||||
return books, nil
|
||||
}
|
||||
|
||||
func parseHeader(header []string) map[string]int {
|
||||
m := make(map[string]int, len(header)-1)
|
||||
for i := range header {
|
||||
m[strings.TrimSpace(strings.ToLower(header[i]))] = i
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func parseAuthors(a string) []string {
|
||||
as := strings.Split(a, ";")
|
||||
for i := range as {
|
||||
as[i] = strings.TrimSpace(as[i])
|
||||
}
|
||||
return as
|
||||
}
|
188
index.html
188
index.html
@ -1,188 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Library</title>
|
||||
<script type="text/javascript" src='js/jquery.js'></script>
|
||||
<script type="text/javascript" src='js/mustache.js'></script>
|
||||
<script type="text/javascript" src='js/lodash.min.js'></script>
|
||||
<script src='https://cdnjs.cloudflare.com/ajax/libs/tabletop.js/1.5.1/tabletop.min.js'></script>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"></link>
|
||||
<link rel="stylesheet" href="css/reset.css"></link>
|
||||
<link rel="stylesheet" href="css/style.css"></link>
|
||||
<link rel="icon" href="favicon.ico" type="image/x-icon">
|
||||
<link href="https://fonts.googleapis.com/css?family=Libre+Baskerville:400,700" rel="stylesheet">
|
||||
<script type='text/javascript'>
|
||||
var publicSpreadsheetUrl = 'https://docs.google.com/spreadsheets/d/1w5Dc57wV0_rrKFsG7KM-qdPWEpqYk6lFu3JzAA0cSv0/pubhtml';
|
||||
var sortState = {
|
||||
sortBy: 'authorLast',
|
||||
sortOrder: 'asc'
|
||||
};
|
||||
|
||||
function init() {
|
||||
Tabletop.init({
|
||||
key: publicSpreadsheetUrl,
|
||||
callback: showInfo,
|
||||
simpleSheet: true
|
||||
});
|
||||
}
|
||||
|
||||
function showInfo(data, tabletop) {
|
||||
$("#reloadLink").unbind('click');
|
||||
$("#reloadLink").on("click", function() { init(); });
|
||||
|
||||
$("#search").unbind('input');
|
||||
$("#search").on("input", function(e) { search(data, e.target.value); });
|
||||
|
||||
$.each(data, function(key, value) {
|
||||
value.sortTitle = titleCleaner(value.title);
|
||||
if (!value['isbn-10'] && value['isbn-13']) {
|
||||
value['isbn-10'] = generateISBNfromEAN(value['isbn-13']);
|
||||
}
|
||||
if (!value.coverurl && value['isbn-10']) {
|
||||
value.coverurl = generateAmazonCoverUrl(value['isbn-10']);
|
||||
}
|
||||
});
|
||||
|
||||
renderTable(data);
|
||||
}
|
||||
|
||||
function search(data, searchString) {
|
||||
searchBy = searchString.toLowerCase().replace('"', '').replace(':', '').replace("'", '').replace(" ", '');
|
||||
relevantFields = ['title', 'author', 'genre', 'publisher', 'series', 'year'];
|
||||
|
||||
if (!searchString) {
|
||||
renderTable(data);
|
||||
return false;
|
||||
}
|
||||
|
||||
renderTable(_.filter(data, function(book) {
|
||||
return _.find(_.pick(book, relevantFields), function(field) {
|
||||
return field.toLowerCase().replace('"', '').replace(':', '').replace("'", '').replace(" ", '').indexOf(searchBy) !== -1;
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
function renderTable(data, sortField) {
|
||||
if (sortField) {
|
||||
if (sortState.sortBy === sortField) {
|
||||
sortState.sortOrder = (sortState.sortOrder === 'asc') ? 'desc' : 'asc'; // swap if we're looping
|
||||
} else {
|
||||
sortState.sortOrder = 'asc'; // reset if we've changed columns
|
||||
}
|
||||
sortState.sortBy = sortField;
|
||||
}
|
||||
data = _.orderBy(data, function(o) {
|
||||
return o[sortState.sortBy].toLowerCase() + o["sortTitle"].toLowerCase();
|
||||
}, sortState.sortOrder);
|
||||
|
||||
$.each(data, function(key, value) {
|
||||
value.rowNumber = key; // re-key for new sort
|
||||
});
|
||||
|
||||
var template = $('#Table').html();
|
||||
var rendered = Mustache.render(template, {books: data});
|
||||
$('#books').html(rendered);
|
||||
$("#books tbody tr").not(':first').on("click", function() {
|
||||
updateCurrentBook(data[$(this)[0].id]); // ignore the headers
|
||||
});
|
||||
$("#books tbody tr th[data-sort-by]").on("click", function() {
|
||||
renderTable(data, $(this).data('sortBy')); // only add callback when there's a sortBy attribute
|
||||
});
|
||||
$("#books tbody tr th[data-sort-by=" + sortState.sortBy + ']').addClass(sortState.sortOrder);
|
||||
}
|
||||
|
||||
function updateCurrentBook(book) {
|
||||
var template = $('#View').html();
|
||||
var rendered = Mustache.render(template, {book: book});
|
||||
$('#current').html(rendered);
|
||||
}
|
||||
|
||||
function titleCleaner(title) {
|
||||
return title
|
||||
.replace('"', '')
|
||||
.replace(':', '')
|
||||
.replace(/^(An?|The)\s/i, '');
|
||||
}
|
||||
|
||||
function generateAmazonCoverUrl(ISBN) {
|
||||
return "https://images-na.ssl-images-amazon.com/images/P/" + ISBN + ".01.LZZ.jpg"
|
||||
}
|
||||
|
||||
function generateISBNfromEAN(EAN) {
|
||||
ISBN = EAN.slice(3,12);
|
||||
var checkdigit = ((11 - (_.reduce(ISBN.split(''), function(sum, num, key) {
|
||||
return sum + (num * (10 - key));
|
||||
}, 0) % 11)) % 11);
|
||||
return ISBN + (checkdigit == 10 ? 'X' : checkdigit);
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', init);
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<div id="header">
|
||||
<h1>Library</h1>
|
||||
<a target="_blank" href="https://docs.google.com/spreadsheets/d/1w5Dc57wV0_rrKFsG7KM-qdPWEpqYk6lFu3JzAA0cSv0/edit">spreadsheet</a>
|
||||
<a target="_blank" href="https://git.yetaga.in/alazyreader/library">git</a>
|
||||
<a id="reloadLink" href="#">reload</a>
|
||||
<div id="searchBox">
|
||||
<input id="search" type="text" name="search" placeholder="Search..."/>
|
||||
</div>
|
||||
</div>
|
||||
<div id="current">No Book Selected</div>
|
||||
<div id="books"></div> <!-- Table goes here -->
|
||||
</div>
|
||||
|
||||
<script id="Table" type="text/html">
|
||||
<table class="bookTable">
|
||||
<tr>
|
||||
<th data-sort-by="sortTitle" class="tHeader title">Title</th>
|
||||
<th data-sort-by="authorLast" class="tHeader author">Author</th>
|
||||
<th data-sort-by="publisher" class="tHeader publisher">Publisher</th>
|
||||
<th data-sort-by="year" class="tHeader year">Year</th>
|
||||
<th class="tHeader isbn">ISBN</th>
|
||||
</tr>
|
||||
{{#books}}
|
||||
<tr class="tRow {{#onLoan}}onLoan{{/onLoan}}" id="{{rowNumber}}">
|
||||
<td class="title">{{title}}
|
||||
{{#signed}}<span class="signed" title="Signed by the author">✒</span>︎{{/signed}}</td>
|
||||
<td class="author">{{author}}</td>
|
||||
<td class="publisher">{{publisher}}</td>
|
||||
<td class="year">{{year}}</td>
|
||||
<td class="isbn">{{isbn-13}}</td>
|
||||
</tr>
|
||||
{{/books}}
|
||||
</table>
|
||||
</script>
|
||||
|
||||
<script id="View" type="text/html">
|
||||
{{#book}}
|
||||
{{#coverurl}}
|
||||
<img src="{{coverurl}}"/>
|
||||
{{/coverurl}}
|
||||
<h1 {{#onLoan}}class="onLoan" {{/onLoan}}>{{title}}</h1>
|
||||
<h2>{{author}}</h2>
|
||||
<span>{{isbn-13}}</span><br/>
|
||||
<span>{{publisher}}, {{year}}</span><br/>
|
||||
{{#series}}
|
||||
<span>{{series}}{{#volume}}, Volume {{volume}}</span>{{/volume}}<br/>
|
||||
{{/series}}
|
||||
{{#signed}}
|
||||
<span>Signed by the author ✒</span><br/>
|
||||
{{/signed}}
|
||||
<span>{{format}}</span>
|
||||
{{#onLoan}}
|
||||
<h2 class="onLoan">On loan to {{onLoan}}</h2>
|
||||
{{/onLoan}}
|
||||
<div class="description">
|
||||
<p>{{description}}</p>
|
||||
{{#notes}}
|
||||
<span>Notes:</span>
|
||||
<p>{{notes}}</p>
|
||||
{{/notes}}
|
||||
</div>
|
||||
{{/book}}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
174
index.js
174
index.js
@ -1,174 +0,0 @@
|
||||
var fs = require('fs');
|
||||
var readline = require('readline');
|
||||
var google = require('googleapis');
|
||||
var googleAuth = require('google-auth-library');
|
||||
var books = require('google-books-search');
|
||||
var _ = require('lodash');
|
||||
|
||||
var SCOPES = ['https://www.googleapis.com/auth/spreadsheets'];
|
||||
var TOKEN_DIR = '.credentials/';
|
||||
var TOKEN_PATH = TOKEN_DIR + 'sheets.googleapis.com-my-library.json';
|
||||
var SHEET_ID = '1w5Dc57wV0_rrKFsG7KM-qdPWEpqYk6lFu3JzAA0cSv0';
|
||||
var SHEET_NAME = 'Sheet1';
|
||||
|
||||
// Load client secrets from a local file.
|
||||
fs.readFile(TOKEN_DIR + 'client_secret.json', function processClientSecrets(err, content) {
|
||||
if (err) {
|
||||
console.log('Error loading client secret file: ' + err);
|
||||
return;
|
||||
}
|
||||
// Authorize a client with the loaded credentials, then call the
|
||||
// Google Sheets API.
|
||||
authorize(JSON.parse(content), inputLoop);
|
||||
});
|
||||
|
||||
/**
|
||||
* Create an OAuth2 client with the given credentials, and then execute the
|
||||
* given callback function.
|
||||
*
|
||||
* @param {Object} credentials The authorization client credentials.
|
||||
* @param {function} callback The callback to call with the authorized client.
|
||||
*/
|
||||
function authorize(credentials, callback) {
|
||||
var clientSecret = credentials.installed.client_secret;
|
||||
var clientId = credentials.installed.client_id;
|
||||
var redirectUrl = credentials.installed.redirect_uris[0];
|
||||
var auth = new googleAuth();
|
||||
var oauth2Client = new auth.OAuth2(clientId, clientSecret, redirectUrl);
|
||||
|
||||
// Check if we have previously stored a token.
|
||||
fs.readFile(TOKEN_PATH, function(err, token) {
|
||||
if (err) {
|
||||
getNewToken(oauth2Client, callback);
|
||||
} else {
|
||||
oauth2Client.credentials = JSON.parse(token);
|
||||
callback(oauth2Client);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get and store new token after prompting for user authorization, and then
|
||||
* execute the given callback with the authorized OAuth2 client.
|
||||
*
|
||||
* @param {google.auth.OAuth2} oauth2Client The OAuth2 client to get token for.
|
||||
* @param {getEventsCallback} callback The callback to call with the authorized
|
||||
* client.
|
||||
*/
|
||||
function getNewToken(oauth2Client, callback) {
|
||||
var authUrl = oauth2Client.generateAuthUrl({
|
||||
access_type: 'offline',
|
||||
scope: SCOPES
|
||||
});
|
||||
console.log('Authorize this app by visiting this url: ', authUrl);
|
||||
var rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
rl.question('Enter the code from that page here: ', function(code) {
|
||||
rl.close();
|
||||
oauth2Client.getToken(code, function(err, token) {
|
||||
if (err) {
|
||||
console.log('Error while trying to retrieve access token', err);
|
||||
return;
|
||||
}
|
||||
oauth2Client.credentials = token;
|
||||
storeToken(token);
|
||||
callback(oauth2Client);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Store token to disk be used in later program executions.
|
||||
*
|
||||
* @param {Object} token The token to store to disk.
|
||||
*/
|
||||
function storeToken(token) {
|
||||
try {
|
||||
fs.mkdirSync(TOKEN_DIR);
|
||||
} catch (err) {
|
||||
if (err.code != 'EEXIST') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
fs.writeFile(TOKEN_PATH, JSON.stringify(token));
|
||||
console.log('Token stored to ' + TOKEN_PATH);
|
||||
}
|
||||
|
||||
function inputLoop(auth) {
|
||||
insertRow(auth);
|
||||
}
|
||||
|
||||
function insertRow(auth) {
|
||||
var rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
rl.question('ISBN: ', function(isbn) {
|
||||
rl.close();
|
||||
books.search(isbn, {
|
||||
field: 'isbn',
|
||||
lang: 'en'
|
||||
}, function(error, results) {
|
||||
if ( ! error && results.length > 0 ) {
|
||||
console.log(results[0]);
|
||||
var book = normalizeGoogleData(results[0]);
|
||||
console.log(book);
|
||||
appendToSheet(auth, _.values(book));
|
||||
} else if ( results.length == 0 ) {
|
||||
console.log("no book found");
|
||||
inputLoop(auth);
|
||||
} else {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeGoogleData(book) {
|
||||
return {
|
||||
title: book.subtitle ? book.title + ': ' + book.subtitle : book.title,
|
||||
author: _.join(book.authors, ', '),
|
||||
authorLast: book.authors ? _.lowerCase(_.head(_.reverse(_.split(book.authors[0], ' ')))) : '',
|
||||
"isbn-10": _.find(book.industryIdentifiers, { type: "ISBN_10" })
|
||||
? _.find(book.industryIdentifiers, { type: "ISBN_10" }).identifier
|
||||
: '',
|
||||
"isbn-13": _.find(book.industryIdentifiers, { type: "ISBN_13" })
|
||||
? _.find(book.industryIdentifiers, { type: "ISBN_13" }).identifier
|
||||
: '',
|
||||
format: '',
|
||||
genre: book.categories ? book.categories[0] : '',
|
||||
publisher: _.trim(book.publisher, '"'),
|
||||
series: '',
|
||||
volume: '',
|
||||
publishedDate: book.publishedDate ? book.publishedDate.substring(0, 4) : '',
|
||||
coverurl: '',
|
||||
description: book.description,
|
||||
notes: '',
|
||||
signed: ''
|
||||
};
|
||||
}
|
||||
|
||||
function appendToSheet(auth, book) {
|
||||
var sheets = google.sheets('v4');
|
||||
sheets.spreadsheets.values.append({
|
||||
auth: auth,
|
||||
spreadsheetId: SHEET_ID,
|
||||
range: SHEET_NAME,
|
||||
valueInputOption: "RAW",
|
||||
insertDataOption: "INSERT_ROWS",
|
||||
resource: {
|
||||
"range": SHEET_NAME,
|
||||
"majorDimension": "ROWS",
|
||||
"values": [
|
||||
book
|
||||
]
|
||||
}
|
||||
}, function(err, response) {
|
||||
if (err) {
|
||||
console.log('The API returned an error: ' + err);
|
||||
}
|
||||
inputLoop(auth);
|
||||
});
|
||||
}
|
4
js/jquery.js
vendored
4
js/jquery.js
vendored
File diff suppressed because one or more lines are too long
136
js/lodash.min.js
vendored
136
js/lodash.min.js
vendored
@ -1,136 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Lodash lodash.com/license | Underscore.js 1.8.3 underscorejs.org/LICENSE
|
||||
*/
|
||||
;(function(){function n(n,t){return n.set(t[0],t[1]),n}function t(n,t){return n.add(t),n}function r(n,t,r){switch(r.length){case 0:return n.call(t);case 1:return n.call(t,r[0]);case 2:return n.call(t,r[0],r[1]);case 3:return n.call(t,r[0],r[1],r[2])}return n.apply(t,r)}function e(n,t,r,e){for(var u=-1,i=null==n?0:n.length;++u<i;){var o=n[u];t(e,o,r(o),n)}return e}function u(n,t){for(var r=-1,e=null==n?0:n.length;++r<e&&false!==t(n[r],r,n););return n}function i(n,t){for(var r=null==n?0:n.length;r--&&false!==t(n[r],r,n););
|
||||
return n}function o(n,t){for(var r=-1,e=null==n?0:n.length;++r<e;)if(!t(n[r],r,n))return false;return true}function f(n,t){for(var r=-1,e=null==n?0:n.length,u=0,i=[];++r<e;){var o=n[r];t(o,r,n)&&(i[u++]=o)}return i}function c(n,t){return!(null==n||!n.length)&&-1<d(n,t,0)}function a(n,t,r){for(var e=-1,u=null==n?0:n.length;++e<u;)if(r(t,n[e]))return true;return false}function l(n,t){for(var r=-1,e=null==n?0:n.length,u=Array(e);++r<e;)u[r]=t(n[r],r,n);return u}function s(n,t){for(var r=-1,e=t.length,u=n.length;++r<e;)n[u+r]=t[r];
|
||||
return n}function h(n,t,r,e){var u=-1,i=null==n?0:n.length;for(e&&i&&(r=n[++u]);++u<i;)r=t(r,n[u],u,n);return r}function p(n,t,r,e){var u=null==n?0:n.length;for(e&&u&&(r=n[--u]);u--;)r=t(r,n[u],u,n);return r}function _(n,t){for(var r=-1,e=null==n?0:n.length;++r<e;)if(t(n[r],r,n))return true;return false}function v(n,t,r){var e;return r(n,function(n,r,u){if(t(n,r,u))return e=r,false}),e}function g(n,t,r,e){var u=n.length;for(r+=e?1:-1;e?r--:++r<u;)if(t(n[r],r,n))return r;return-1}function d(n,t,r){if(t===t)n:{
|
||||
--r;for(var e=n.length;++r<e;)if(n[r]===t){n=r;break n}n=-1}else n=g(n,b,r);return n}function y(n,t,r,e){--r;for(var u=n.length;++r<u;)if(e(n[r],t))return r;return-1}function b(n){return n!==n}function x(n,t){var r=null==n?0:n.length;return r?k(n,t)/r:P}function j(n){return function(t){return null==t?F:t[n]}}function w(n){return function(t){return null==n?F:n[t]}}function m(n,t,r,e,u){return u(n,function(n,u,i){r=e?(e=false,n):t(r,n,u,i)}),r}function A(n,t){var r=n.length;for(n.sort(t);r--;)n[r]=n[r].c;
|
||||
return n}function k(n,t){for(var r,e=-1,u=n.length;++e<u;){var i=t(n[e]);i!==F&&(r=r===F?i:r+i)}return r}function E(n,t){for(var r=-1,e=Array(n);++r<n;)e[r]=t(r);return e}function O(n,t){return l(t,function(t){return[t,n[t]]})}function S(n){return function(t){return n(t)}}function I(n,t){return l(t,function(t){return n[t]})}function R(n,t){return n.has(t)}function z(n,t){for(var r=-1,e=n.length;++r<e&&-1<d(t,n[r],0););return r}function W(n,t){for(var r=n.length;r--&&-1<d(t,n[r],0););return r}function B(n){
|
||||
return"\\"+Tn[n]}function L(n){var t=-1,r=Array(n.size);return n.forEach(function(n,e){r[++t]=[e,n]}),r}function U(n,t){return function(r){return n(t(r))}}function C(n,t){for(var r=-1,e=n.length,u=0,i=[];++r<e;){var o=n[r];o!==t&&"__lodash_placeholder__"!==o||(n[r]="__lodash_placeholder__",i[u++]=r)}return i}function D(n){var t=-1,r=Array(n.size);return n.forEach(function(n){r[++t]=n}),r}function M(n){var t=-1,r=Array(n.size);return n.forEach(function(n){r[++t]=[n,n]}),r}function T(n){if(Bn.test(n)){
|
||||
for(var t=zn.lastIndex=0;zn.test(n);)++t;n=t}else n=tt(n);return n}function $(n){return Bn.test(n)?n.match(zn)||[]:n.split("")}var F,N=1/0,P=NaN,Z=[["ary",128],["bind",1],["bindKey",2],["curry",8],["curryRight",16],["flip",512],["partial",32],["partialRight",64],["rearg",256]],q=/\b__p\+='';/g,V=/\b(__p\+=)''\+/g,K=/(__e\(.*?\)|\b__t\))\+'';/g,G=/&(?:amp|lt|gt|quot|#39);/g,H=/[&<>"']/g,J=RegExp(G.source),Y=RegExp(H.source),Q=/<%-([\s\S]+?)%>/g,X=/<%([\s\S]+?)%>/g,nn=/<%=([\s\S]+?)%>/g,tn=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,rn=/^\w*$/,en=/^\./,un=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,on=/[\\^$.*+?()[\]{}|]/g,fn=RegExp(on.source),cn=/^\s+|\s+$/g,an=/^\s+/,ln=/\s+$/,sn=/\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/,hn=/\{\n\/\* \[wrapped with (.+)\] \*/,pn=/,? & /,_n=/[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g,vn=/\\(\\)?/g,gn=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,dn=/\w*$/,yn=/^[-+]0x[0-9a-f]+$/i,bn=/^0b[01]+$/i,xn=/^\[object .+?Constructor\]$/,jn=/^0o[0-7]+$/i,wn=/^(?:0|[1-9]\d*)$/,mn=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,An=/($^)/,kn=/['\n\r\u2028\u2029\\]/g,En="[\\ufe0e\\ufe0f]?(?:[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]|\\ud83c[\\udffb-\\udfff])?(?:\\u200d(?:[^\\ud800-\\udfff]|(?:\\ud83c[\\udde6-\\uddff]){2}|[\\ud800-\\udbff][\\udc00-\\udfff])[\\ufe0e\\ufe0f]?(?:[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]|\\ud83c[\\udffb-\\udfff])?)*",On="(?:[\\u2700-\\u27bf]|(?:\\ud83c[\\udde6-\\uddff]){2}|[\\ud800-\\udbff][\\udc00-\\udfff])"+En,Sn="(?:[^\\ud800-\\udfff][\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]?|[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]|(?:\\ud83c[\\udde6-\\uddff]){2}|[\\ud800-\\udbff][\\udc00-\\udfff]|[\\ud800-\\udfff])",In=RegExp("['\u2019]","g"),Rn=RegExp("[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]","g"),zn=RegExp("\\ud83c[\\udffb-\\udfff](?=\\ud83c[\\udffb-\\udfff])|"+Sn+En,"g"),Wn=RegExp(["[A-Z\\xc0-\\xd6\\xd8-\\xde]?[a-z\\xdf-\\xf6\\xf8-\\xff]+(?:['\u2019](?:d|ll|m|re|s|t|ve))?(?=[\\xac\\xb1\\xd7\\xf7\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf\\u2000-\\u206f \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000]|[A-Z\\xc0-\\xd6\\xd8-\\xde]|$)|(?:[A-Z\\xc0-\\xd6\\xd8-\\xde]|[^\\ud800-\\udfff\\xac\\xb1\\xd7\\xf7\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf\\u2000-\\u206f \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000\\d+\\u2700-\\u27bfa-z\\xdf-\\xf6\\xf8-\\xffA-Z\\xc0-\\xd6\\xd8-\\xde])+(?:['\u2019](?:D|LL|M|RE|S|T|VE))?(?=[\\xac\\xb1\\xd7\\xf7\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf\\u2000-\\u206f \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000]|[A-Z\\xc0-\\xd6\\xd8-\\xde](?:[a-z\\xdf-\\xf6\\xf8-\\xff]|[^\\ud800-\\udfff\\xac\\xb1\\xd7\\xf7\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf\\u2000-\\u206f \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000\\d+\\u2700-\\u27bfa-z\\xdf-\\xf6\\xf8-\\xffA-Z\\xc0-\\xd6\\xd8-\\xde])|$)|[A-Z\\xc0-\\xd6\\xd8-\\xde]?(?:[a-z\\xdf-\\xf6\\xf8-\\xff]|[^\\ud800-\\udfff\\xac\\xb1\\xd7\\xf7\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf\\u2000-\\u206f \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000\\d+\\u2700-\\u27bfa-z\\xdf-\\xf6\\xf8-\\xffA-Z\\xc0-\\xd6\\xd8-\\xde])+(?:['\u2019](?:d|ll|m|re|s|t|ve))?|[A-Z\\xc0-\\xd6\\xd8-\\xde]+(?:['\u2019](?:D|LL|M|RE|S|T|VE))?|\\d*(?:(?:1ST|2ND|3RD|(?![123])\\dTH)\\b)|\\d*(?:(?:1st|2nd|3rd|(?![123])\\dth)\\b)|\\d+",On].join("|"),"g"),Bn=RegExp("[\\u200d\\ud800-\\udfff\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff\\ufe0e\\ufe0f]"),Ln=/[a-z][A-Z]|[A-Z]{2,}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/,Un="Array Buffer DataView Date Error Float32Array Float64Array Function Int8Array Int16Array Int32Array Map Math Object Promise RegExp Set String Symbol TypeError Uint8Array Uint8ClampedArray Uint16Array Uint32Array WeakMap _ clearTimeout isFinite parseInt setTimeout".split(" "),Cn={};
|
||||
Cn["[object Float32Array]"]=Cn["[object Float64Array]"]=Cn["[object Int8Array]"]=Cn["[object Int16Array]"]=Cn["[object Int32Array]"]=Cn["[object Uint8Array]"]=Cn["[object Uint8ClampedArray]"]=Cn["[object Uint16Array]"]=Cn["[object Uint32Array]"]=true,Cn["[object Arguments]"]=Cn["[object Array]"]=Cn["[object ArrayBuffer]"]=Cn["[object Boolean]"]=Cn["[object DataView]"]=Cn["[object Date]"]=Cn["[object Error]"]=Cn["[object Function]"]=Cn["[object Map]"]=Cn["[object Number]"]=Cn["[object Object]"]=Cn["[object RegExp]"]=Cn["[object Set]"]=Cn["[object String]"]=Cn["[object WeakMap]"]=false;
|
||||
var Dn={};Dn["[object Arguments]"]=Dn["[object Array]"]=Dn["[object ArrayBuffer]"]=Dn["[object DataView]"]=Dn["[object Boolean]"]=Dn["[object Date]"]=Dn["[object Float32Array]"]=Dn["[object Float64Array]"]=Dn["[object Int8Array]"]=Dn["[object Int16Array]"]=Dn["[object Int32Array]"]=Dn["[object Map]"]=Dn["[object Number]"]=Dn["[object Object]"]=Dn["[object RegExp]"]=Dn["[object Set]"]=Dn["[object String]"]=Dn["[object Symbol]"]=Dn["[object Uint8Array]"]=Dn["[object Uint8ClampedArray]"]=Dn["[object Uint16Array]"]=Dn["[object Uint32Array]"]=true,
|
||||
Dn["[object Error]"]=Dn["[object Function]"]=Dn["[object WeakMap]"]=false;var Mn,Tn={"\\":"\\","'":"'","\n":"n","\r":"r","\u2028":"u2028","\u2029":"u2029"},$n=parseFloat,Fn=parseInt,Nn=typeof global=="object"&&global&&global.Object===Object&&global,Pn=typeof self=="object"&&self&&self.Object===Object&&self,Zn=Nn||Pn||Function("return this")(),qn=typeof exports=="object"&&exports&&!exports.nodeType&&exports,Vn=qn&&typeof module=="object"&&module&&!module.nodeType&&module,Kn=Vn&&Vn.exports===qn,Gn=Kn&&Nn.process;
|
||||
n:{try{Mn=Gn&&Gn.binding&&Gn.binding("util");break n}catch(n){}Mn=void 0}var Hn=Mn&&Mn.isArrayBuffer,Jn=Mn&&Mn.isDate,Yn=Mn&&Mn.isMap,Qn=Mn&&Mn.isRegExp,Xn=Mn&&Mn.isSet,nt=Mn&&Mn.isTypedArray,tt=j("length"),rt=w({"\xc0":"A","\xc1":"A","\xc2":"A","\xc3":"A","\xc4":"A","\xc5":"A","\xe0":"a","\xe1":"a","\xe2":"a","\xe3":"a","\xe4":"a","\xe5":"a","\xc7":"C","\xe7":"c","\xd0":"D","\xf0":"d","\xc8":"E","\xc9":"E","\xca":"E","\xcb":"E","\xe8":"e","\xe9":"e","\xea":"e","\xeb":"e","\xcc":"I","\xcd":"I","\xce":"I",
|
||||
"\xcf":"I","\xec":"i","\xed":"i","\xee":"i","\xef":"i","\xd1":"N","\xf1":"n","\xd2":"O","\xd3":"O","\xd4":"O","\xd5":"O","\xd6":"O","\xd8":"O","\xf2":"o","\xf3":"o","\xf4":"o","\xf5":"o","\xf6":"o","\xf8":"o","\xd9":"U","\xda":"U","\xdb":"U","\xdc":"U","\xf9":"u","\xfa":"u","\xfb":"u","\xfc":"u","\xdd":"Y","\xfd":"y","\xff":"y","\xc6":"Ae","\xe6":"ae","\xde":"Th","\xfe":"th","\xdf":"ss","\u0100":"A","\u0102":"A","\u0104":"A","\u0101":"a","\u0103":"a","\u0105":"a","\u0106":"C","\u0108":"C","\u010a":"C",
|
||||
"\u010c":"C","\u0107":"c","\u0109":"c","\u010b":"c","\u010d":"c","\u010e":"D","\u0110":"D","\u010f":"d","\u0111":"d","\u0112":"E","\u0114":"E","\u0116":"E","\u0118":"E","\u011a":"E","\u0113":"e","\u0115":"e","\u0117":"e","\u0119":"e","\u011b":"e","\u011c":"G","\u011e":"G","\u0120":"G","\u0122":"G","\u011d":"g","\u011f":"g","\u0121":"g","\u0123":"g","\u0124":"H","\u0126":"H","\u0125":"h","\u0127":"h","\u0128":"I","\u012a":"I","\u012c":"I","\u012e":"I","\u0130":"I","\u0129":"i","\u012b":"i","\u012d":"i",
|
||||
"\u012f":"i","\u0131":"i","\u0134":"J","\u0135":"j","\u0136":"K","\u0137":"k","\u0138":"k","\u0139":"L","\u013b":"L","\u013d":"L","\u013f":"L","\u0141":"L","\u013a":"l","\u013c":"l","\u013e":"l","\u0140":"l","\u0142":"l","\u0143":"N","\u0145":"N","\u0147":"N","\u014a":"N","\u0144":"n","\u0146":"n","\u0148":"n","\u014b":"n","\u014c":"O","\u014e":"O","\u0150":"O","\u014d":"o","\u014f":"o","\u0151":"o","\u0154":"R","\u0156":"R","\u0158":"R","\u0155":"r","\u0157":"r","\u0159":"r","\u015a":"S","\u015c":"S",
|
||||
"\u015e":"S","\u0160":"S","\u015b":"s","\u015d":"s","\u015f":"s","\u0161":"s","\u0162":"T","\u0164":"T","\u0166":"T","\u0163":"t","\u0165":"t","\u0167":"t","\u0168":"U","\u016a":"U","\u016c":"U","\u016e":"U","\u0170":"U","\u0172":"U","\u0169":"u","\u016b":"u","\u016d":"u","\u016f":"u","\u0171":"u","\u0173":"u","\u0174":"W","\u0175":"w","\u0176":"Y","\u0177":"y","\u0178":"Y","\u0179":"Z","\u017b":"Z","\u017d":"Z","\u017a":"z","\u017c":"z","\u017e":"z","\u0132":"IJ","\u0133":"ij","\u0152":"Oe","\u0153":"oe",
|
||||
"\u0149":"'n","\u017f":"s"}),et=w({"&":"&","<":"<",">":">",'"':""","'":"'"}),ut=w({"&":"&","<":"<",">":">",""":'"',"'":"'"}),it=function w(En){function On(n){if(xu(n)&&!af(n)&&!(n instanceof Mn)){if(n instanceof zn)return n;if(ci.call(n,"__wrapped__"))return Pe(n)}return new zn(n)}function Sn(){}function zn(n,t){this.__wrapped__=n,this.__actions__=[],this.__chain__=!!t,this.__index__=0,this.__values__=F}function Mn(n){this.__wrapped__=n,this.__actions__=[],this.__dir__=1,
|
||||
this.__filtered__=false,this.__iteratees__=[],this.__takeCount__=4294967295,this.__views__=[]}function Tn(n){var t=-1,r=null==n?0:n.length;for(this.clear();++t<r;){var e=n[t];this.set(e[0],e[1])}}function Nn(n){var t=-1,r=null==n?0:n.length;for(this.clear();++t<r;){var e=n[t];this.set(e[0],e[1])}}function Pn(n){var t=-1,r=null==n?0:n.length;for(this.clear();++t<r;){var e=n[t];this.set(e[0],e[1])}}function qn(n){var t=-1,r=null==n?0:n.length;for(this.__data__=new Pn;++t<r;)this.add(n[t])}function Vn(n){
|
||||
this.size=(this.__data__=new Nn(n)).size}function Gn(n,t){var r,e=af(n),u=!e&&cf(n),i=!e&&!u&&sf(n),o=!e&&!u&&!i&&gf(n),u=(e=e||u||i||o)?E(n.length,ri):[],f=u.length;for(r in n)!t&&!ci.call(n,r)||e&&("length"==r||i&&("offset"==r||"parent"==r)||o&&("buffer"==r||"byteLength"==r||"byteOffset"==r)||Re(r,f))||u.push(r);return u}function tt(n){var t=n.length;return t?n[cr(0,t-1)]:F}function ot(n,t){return Te(Mr(n),gt(t,0,n.length))}function ft(n){return Te(Mr(n))}function ct(n,t,r){(r===F||hu(n[t],r))&&(r!==F||t in n)||_t(n,t,r);
|
||||
}function at(n,t,r){var e=n[t];ci.call(n,t)&&hu(e,r)&&(r!==F||t in n)||_t(n,t,r)}function lt(n,t){for(var r=n.length;r--;)if(hu(n[r][0],t))return r;return-1}function st(n,t,r,e){return oo(n,function(n,u,i){t(e,n,r(n),i)}),e}function ht(n,t){return n&&Tr(t,Lu(t),n)}function pt(n,t){return n&&Tr(t,Uu(t),n)}function _t(n,t,r){"__proto__"==t&&Ei?Ei(n,t,{configurable:true,enumerable:true,value:r,writable:true}):n[t]=r}function vt(n,t){for(var r=-1,e=t.length,u=Hu(e),i=null==n;++r<e;)u[r]=i?F:Wu(n,t[r]);return u;
|
||||
}function gt(n,t,r){return n===n&&(r!==F&&(n=n<=r?n:r),t!==F&&(n=n>=t?n:t)),n}function dt(n,t,r,e,i,o){var f,c=1&t,a=2&t,l=4&t;if(r&&(f=i?r(n,e,i,o):r(n)),f!==F)return f;if(!bu(n))return n;if(e=af(n)){if(f=Ee(n),!c)return Mr(n,f)}else{var s=yo(n),h="[object Function]"==s||"[object GeneratorFunction]"==s;if(sf(n))return Wr(n,c);if("[object Object]"==s||"[object Arguments]"==s||h&&!i){if(f=a||h?{}:Oe(n),!c)return a?Fr(n,pt(f,n)):$r(n,ht(f,n))}else{if(!Dn[s])return i?n:{};f=Se(n,s,dt,c)}}if(o||(o=new Vn),
|
||||
i=o.get(n))return i;o.set(n,f);var a=l?a?ye:de:a?Uu:Lu,p=e?F:a(n);return u(p||n,function(e,u){p&&(u=e,e=n[u]),at(f,u,dt(e,t,r,u,n,o))}),f}function yt(n){var t=Lu(n);return function(r){return bt(r,n,t)}}function bt(n,t,r){var e=r.length;if(null==n)return!e;for(n=ni(n);e--;){var u=r[e],i=t[u],o=n[u];if(o===F&&!(u in n)||!i(o))return false}return true}function xt(n,t,r){if(typeof n!="function")throw new ei("Expected a function");return jo(function(){n.apply(F,r)},t)}function jt(n,t,r,e){var u=-1,i=c,o=true,f=n.length,s=[],h=t.length;
|
||||
if(!f)return s;r&&(t=l(t,S(r))),e?(i=a,o=false):200<=t.length&&(i=R,o=false,t=new qn(t));n:for(;++u<f;){var p=n[u],_=null==r?p:r(p),p=e||0!==p?p:0;if(o&&_===_){for(var v=h;v--;)if(t[v]===_)continue n;s.push(p)}else i(t,_,e)||s.push(p)}return s}function wt(n,t){var r=true;return oo(n,function(n,e,u){return r=!!t(n,e,u)}),r}function mt(n,t,r){for(var e=-1,u=n.length;++e<u;){var i=n[e],o=t(i);if(null!=o&&(f===F?o===o&&!Au(o):r(o,f)))var f=o,c=i}return c}function At(n,t){var r=[];return oo(n,function(n,e,u){
|
||||
t(n,e,u)&&r.push(n)}),r}function kt(n,t,r,e,u){var i=-1,o=n.length;for(r||(r=Ie),u||(u=[]);++i<o;){var f=n[i];0<t&&r(f)?1<t?kt(f,t-1,r,e,u):s(u,f):e||(u[u.length]=f)}return u}function Et(n,t){return n&&co(n,t,Lu)}function Ot(n,t){return n&&ao(n,t,Lu)}function St(n,t){return f(t,function(t){return gu(n[t])})}function It(n,t){t=Rr(t,n);for(var r=0,e=t.length;null!=n&&r<e;)n=n[$e(t[r++])];return r&&r==e?n:F}function Rt(n,t,r){return t=t(n),af(n)?t:s(t,r(n))}function zt(n){if(null==n)n=n===F?"[object Undefined]":"[object Null]";else if(ki&&ki in ni(n)){
|
||||
var t=ci.call(n,ki),r=n[ki];try{n[ki]=F;var e=true}catch(n){}var u=si.call(n);e&&(t?n[ki]=r:delete n[ki]),n=u}else n=si.call(n);return n}function Wt(n,t){return n>t}function Bt(n,t){return null!=n&&ci.call(n,t)}function Lt(n,t){return null!=n&&t in ni(n)}function Ut(n,t,r){for(var e=r?a:c,u=n[0].length,i=n.length,o=i,f=Hu(i),s=1/0,h=[];o--;){var p=n[o];o&&t&&(p=l(p,S(t))),s=Mi(p.length,s),f[o]=!r&&(t||120<=u&&120<=p.length)?new qn(o&&p):F}var p=n[0],_=-1,v=f[0];n:for(;++_<u&&h.length<s;){var g=p[_],d=t?t(g):g,g=r||0!==g?g:0;
|
||||
if(v?!R(v,d):!e(h,d,r)){for(o=i;--o;){var y=f[o];if(y?!R(y,d):!e(n[o],d,r))continue n}v&&v.push(d),h.push(g)}}return h}function Ct(n,t,r){var e={};return Et(n,function(n,u,i){t(e,r(n),u,i)}),e}function Dt(n,t,e){return t=Rr(t,n),n=2>t.length?n:It(n,vr(t,0,-1)),t=null==n?n:n[$e(Ge(t))],null==t?F:r(t,n,e)}function Mt(n){return xu(n)&&"[object Arguments]"==zt(n)}function Tt(n){return xu(n)&&"[object ArrayBuffer]"==zt(n)}function $t(n){return xu(n)&&"[object Date]"==zt(n)}function Ft(n,t,r,e,u){if(n===t)t=true;else if(null==n||null==t||!xu(n)&&!xu(t))t=n!==n&&t!==t;else n:{
|
||||
var i=af(n),o=af(t),f=i?"[object Array]":yo(n),c=o?"[object Array]":yo(t),f="[object Arguments]"==f?"[object Object]":f,c="[object Arguments]"==c?"[object Object]":c,a="[object Object]"==f,o="[object Object]"==c;if((c=f==c)&&sf(n)){if(!sf(t)){t=false;break n}i=true,a=false}if(c&&!a)u||(u=new Vn),t=i||gf(n)?_e(n,t,r,e,Ft,u):ve(n,t,f,r,e,Ft,u);else{if(!(1&r)&&(i=a&&ci.call(n,"__wrapped__"),f=o&&ci.call(t,"__wrapped__"),i||f)){n=i?n.value():n,t=f?t.value():t,u||(u=new Vn),t=Ft(n,t,r,e,u);break n}if(c)t:if(u||(u=new Vn),
|
||||
i=1&r,f=de(n),o=f.length,c=de(t).length,o==c||i){for(a=o;a--;){var l=f[a];if(!(i?l in t:ci.call(t,l))){t=false;break t}}if((c=u.get(n))&&u.get(t))t=c==t;else{c=true,u.set(n,t),u.set(t,n);for(var s=i;++a<o;){var l=f[a],h=n[l],p=t[l];if(e)var _=i?e(p,h,l,t,n,u):e(h,p,l,n,t,u);if(_===F?h!==p&&!Ft(h,p,r,e,u):!_){c=false;break}s||(s="constructor"==l)}c&&!s&&(r=n.constructor,e=t.constructor,r!=e&&"constructor"in n&&"constructor"in t&&!(typeof r=="function"&&r instanceof r&&typeof e=="function"&&e instanceof e)&&(c=false)),
|
||||
u.delete(n),u.delete(t),t=c}}else t=false;else t=false}}return t}function Nt(n){return xu(n)&&"[object Map]"==yo(n)}function Pt(n,t,r,e){var u=r.length,i=u,o=!e;if(null==n)return!i;for(n=ni(n);u--;){var f=r[u];if(o&&f[2]?f[1]!==n[f[0]]:!(f[0]in n))return false}for(;++u<i;){var f=r[u],c=f[0],a=n[c],l=f[1];if(o&&f[2]){if(a===F&&!(c in n))return false}else{if(f=new Vn,e)var s=e(a,l,c,n,t,f);if(s===F?!Ft(l,a,3,e,f):!s)return false}}return true}function Zt(n){return!(!bu(n)||li&&li in n)&&(gu(n)?_i:xn).test(Fe(n))}function qt(n){
|
||||
return xu(n)&&"[object RegExp]"==zt(n)}function Vt(n){return xu(n)&&"[object Set]"==yo(n)}function Kt(n){return xu(n)&&yu(n.length)&&!!Cn[zt(n)]}function Gt(n){return typeof n=="function"?n:null==n?Nu:typeof n=="object"?af(n)?Xt(n[0],n[1]):Qt(n):Vu(n)}function Ht(n){if(!Le(n))return Ci(n);var t,r=[];for(t in ni(n))ci.call(n,t)&&"constructor"!=t&&r.push(t);return r}function Jt(n,t){return n<t}function Yt(n,t){var r=-1,e=pu(n)?Hu(n.length):[];return oo(n,function(n,u,i){e[++r]=t(n,u,i)}),e}function Qt(n){
|
||||
var t=me(n);return 1==t.length&&t[0][2]?Ue(t[0][0],t[0][1]):function(r){return r===n||Pt(r,n,t)}}function Xt(n,t){return We(n)&&t===t&&!bu(t)?Ue($e(n),t):function(r){var e=Wu(r,n);return e===F&&e===t?Bu(r,n):Ft(t,e,3)}}function nr(n,t,r,e,u){n!==t&&co(t,function(i,o){if(bu(i)){u||(u=new Vn);var f=u,c=n[o],a=t[o],l=f.get(a);if(l)ct(n,o,l);else{var l=e?e(c,a,o+"",n,t,f):F,s=l===F;if(s){var h=af(a),p=!h&&sf(a),_=!h&&!p&&gf(a),l=a;h||p||_?af(c)?l=c:_u(c)?l=Mr(c):p?(s=false,l=Wr(a,true)):_?(s=false,l=Lr(a,true)):l=[]:wu(a)||cf(a)?(l=c,
|
||||
cf(c)?l=Ru(c):(!bu(c)||r&&gu(c))&&(l=Oe(a))):s=false}s&&(f.set(a,l),nr(l,a,r,e,f),f.delete(a)),ct(n,o,l)}}else f=e?e(n[o],i,o+"",n,t,u):F,f===F&&(f=i),ct(n,o,f)},Uu)}function tr(n,t){var r=n.length;if(r)return t+=0>t?r:0,Re(t,r)?n[t]:F}function rr(n,t,r){var e=-1;return t=l(t.length?t:[Nu],S(je())),n=Yt(n,function(n){return{a:l(t,function(t){return t(n)}),b:++e,c:n}}),A(n,function(n,t){var e;n:{e=-1;for(var u=n.a,i=t.a,o=u.length,f=r.length;++e<o;){var c=Ur(u[e],i[e]);if(c){e=e>=f?c:c*("desc"==r[e]?-1:1);
|
||||
break n}}e=n.b-t.b}return e})}function er(n,t){return ur(n,t,function(t,r){return Bu(n,r)})}function ur(n,t,r){for(var e=-1,u=t.length,i={};++e<u;){var o=t[e],f=It(n,o);r(f,o)&&pr(i,Rr(o,n),f)}return i}function ir(n){return function(t){return It(t,n)}}function or(n,t,r,e){var u=e?y:d,i=-1,o=t.length,f=n;for(n===t&&(t=Mr(t)),r&&(f=l(n,S(r)));++i<o;)for(var c=0,a=t[i],a=r?r(a):a;-1<(c=u(f,a,c,e));)f!==n&&wi.call(f,c,1),wi.call(n,c,1);return n}function fr(n,t){for(var r=n?t.length:0,e=r-1;r--;){var u=t[r];
|
||||
if(r==e||u!==i){var i=u;Re(u)?wi.call(n,u,1):mr(n,u)}}}function cr(n,t){return n+zi(Fi()*(t-n+1))}function ar(n,t){var r="";if(!n||1>t||9007199254740991<t)return r;do t%2&&(r+=n),(t=zi(t/2))&&(n+=n);while(t);return r}function lr(n,t){return wo(Ce(n,t,Nu),n+"")}function sr(n){return tt(Du(n))}function hr(n,t){var r=Du(n);return Te(r,gt(t,0,r.length))}function pr(n,t,r,e){if(!bu(n))return n;t=Rr(t,n);for(var u=-1,i=t.length,o=i-1,f=n;null!=f&&++u<i;){var c=$e(t[u]),a=r;if(u!=o){var l=f[c],a=e?e(l,c,f):F;
|
||||
a===F&&(a=bu(l)?l:Re(t[u+1])?[]:{})}at(f,c,a),f=f[c]}return n}function _r(n){return Te(Du(n))}function vr(n,t,r){var e=-1,u=n.length;for(0>t&&(t=-t>u?0:u+t),r=r>u?u:r,0>r&&(r+=u),u=t>r?0:r-t>>>0,t>>>=0,r=Hu(u);++e<u;)r[e]=n[e+t];return r}function gr(n,t){var r;return oo(n,function(n,e,u){return r=t(n,e,u),!r}),!!r}function dr(n,t,r){var e=0,u=null==n?e:n.length;if(typeof t=="number"&&t===t&&2147483647>=u){for(;e<u;){var i=e+u>>>1,o=n[i];null!==o&&!Au(o)&&(r?o<=t:o<t)?e=i+1:u=i}return u}return yr(n,t,Nu,r);
|
||||
}function yr(n,t,r,e){t=r(t);for(var u=0,i=null==n?0:n.length,o=t!==t,f=null===t,c=Au(t),a=t===F;u<i;){var l=zi((u+i)/2),s=r(n[l]),h=s!==F,p=null===s,_=s===s,v=Au(s);(o?e||_:a?_&&(e||h):f?_&&h&&(e||!p):c?_&&h&&!p&&(e||!v):p||v?0:e?s<=t:s<t)?u=l+1:i=l}return Mi(i,4294967294)}function br(n,t){for(var r=-1,e=n.length,u=0,i=[];++r<e;){var o=n[r],f=t?t(o):o;if(!r||!hu(f,c)){var c=f;i[u++]=0===o?0:o}}return i}function xr(n){return typeof n=="number"?n:Au(n)?P:+n}function jr(n){if(typeof n=="string")return n;
|
||||
if(af(n))return l(n,jr)+"";if(Au(n))return uo?uo.call(n):"";var t=n+"";return"0"==t&&1/n==-N?"-0":t}function wr(n,t,r){var e=-1,u=c,i=n.length,o=true,f=[],l=f;if(r)o=false,u=a;else if(200<=i){if(u=t?null:po(n))return D(u);o=false,u=R,l=new qn}else l=t?[]:f;n:for(;++e<i;){var s=n[e],h=t?t(s):s,s=r||0!==s?s:0;if(o&&h===h){for(var p=l.length;p--;)if(l[p]===h)continue n;t&&l.push(h),f.push(s)}else u(l,h,r)||(l!==f&&l.push(h),f.push(s))}return f}function mr(n,t){return t=Rr(t,n),n=2>t.length?n:It(n,vr(t,0,-1)),
|
||||
null==n||delete n[$e(Ge(t))]}function Ar(n,t,r,e){for(var u=n.length,i=e?u:-1;(e?i--:++i<u)&&t(n[i],i,n););return r?vr(n,e?0:i,e?i+1:u):vr(n,e?i+1:0,e?u:i)}function kr(n,t){var r=n;return r instanceof Mn&&(r=r.value()),h(t,function(n,t){return t.func.apply(t.thisArg,s([n],t.args))},r)}function Er(n,t,r){var e=n.length;if(2>e)return e?wr(n[0]):[];for(var u=-1,i=Hu(e);++u<e;)for(var o=n[u],f=-1;++f<e;)f!=u&&(i[u]=jt(i[u]||o,n[f],t,r));return wr(kt(i,1),t,r)}function Or(n,t,r){for(var e=-1,u=n.length,i=t.length,o={};++e<u;)r(o,n[e],e<i?t[e]:F);
|
||||
return o}function Sr(n){return _u(n)?n:[]}function Ir(n){return typeof n=="function"?n:Nu}function Rr(n,t){return af(n)?n:We(n,t)?[n]:mo(zu(n))}function zr(n,t,r){var e=n.length;return r=r===F?e:r,!t&&r>=e?n:vr(n,t,r)}function Wr(n,t){if(t)return n.slice();var r=n.length,r=yi?yi(r):new n.constructor(r);return n.copy(r),r}function Br(n){var t=new n.constructor(n.byteLength);return new di(t).set(new di(n)),t}function Lr(n,t){return new n.constructor(t?Br(n.buffer):n.buffer,n.byteOffset,n.length)}function Ur(n,t){
|
||||
if(n!==t){var r=n!==F,e=null===n,u=n===n,i=Au(n),o=t!==F,f=null===t,c=t===t,a=Au(t);if(!f&&!a&&!i&&n>t||i&&o&&c&&!f&&!a||e&&o&&c||!r&&c||!u)return 1;if(!e&&!i&&!a&&n<t||a&&r&&u&&!e&&!i||f&&r&&u||!o&&u||!c)return-1}return 0}function Cr(n,t,r,e){var u=-1,i=n.length,o=r.length,f=-1,c=t.length,a=Di(i-o,0),l=Hu(c+a);for(e=!e;++f<c;)l[f]=t[f];for(;++u<o;)(e||u<i)&&(l[r[u]]=n[u]);for(;a--;)l[f++]=n[u++];return l}function Dr(n,t,r,e){var u=-1,i=n.length,o=-1,f=r.length,c=-1,a=t.length,l=Di(i-f,0),s=Hu(l+a);
|
||||
for(e=!e;++u<l;)s[u]=n[u];for(l=u;++c<a;)s[l+c]=t[c];for(;++o<f;)(e||u<i)&&(s[l+r[o]]=n[u++]);return s}function Mr(n,t){var r=-1,e=n.length;for(t||(t=Hu(e));++r<e;)t[r]=n[r];return t}function Tr(n,t,r,e){var u=!r;r||(r={});for(var i=-1,o=t.length;++i<o;){var f=t[i],c=e?e(r[f],n[f],f,r,n):F;c===F&&(c=n[f]),u?_t(r,f,c):at(r,f,c)}return r}function $r(n,t){return Tr(n,vo(n),t)}function Fr(n,t){return Tr(n,go(n),t)}function Nr(n,t){return function(r,u){var i=af(r)?e:st,o=t?t():{};return i(r,n,je(u,2),o);
|
||||
}}function Pr(n){return lr(function(t,r){var e=-1,u=r.length,i=1<u?r[u-1]:F,o=2<u?r[2]:F,i=3<n.length&&typeof i=="function"?(u--,i):F;for(o&&ze(r[0],r[1],o)&&(i=3>u?F:i,u=1),t=ni(t);++e<u;)(o=r[e])&&n(t,o,e,i);return t})}function Zr(n,t){return function(r,e){if(null==r)return r;if(!pu(r))return n(r,e);for(var u=r.length,i=t?u:-1,o=ni(r);(t?i--:++i<u)&&false!==e(o[i],i,o););return r}}function qr(n){return function(t,r,e){var u=-1,i=ni(t);e=e(t);for(var o=e.length;o--;){var f=e[n?o:++u];if(false===r(i[f],f,i))break;
|
||||
}return t}}function Vr(n,t,r){function e(){return(this&&this!==Zn&&this instanceof e?i:n).apply(u?r:this,arguments)}var u=1&t,i=Hr(n);return e}function Kr(n){return function(t){t=zu(t);var r=Bn.test(t)?$(t):F,e=r?r[0]:t.charAt(0);return t=r?zr(r,1).join(""):t.slice(1),e[n]()+t}}function Gr(n){return function(t){return h($u(Tu(t).replace(In,"")),n,"")}}function Hr(n){return function(){var t=arguments;switch(t.length){case 0:return new n;case 1:return new n(t[0]);case 2:return new n(t[0],t[1]);case 3:
|
||||
return new n(t[0],t[1],t[2]);case 4:return new n(t[0],t[1],t[2],t[3]);case 5:return new n(t[0],t[1],t[2],t[3],t[4]);case 6:return new n(t[0],t[1],t[2],t[3],t[4],t[5]);case 7:return new n(t[0],t[1],t[2],t[3],t[4],t[5],t[6])}var r=io(n.prototype),t=n.apply(r,t);return bu(t)?t:r}}function Jr(n,t,e){function u(){for(var o=arguments.length,f=Hu(o),c=o,a=xe(u);c--;)f[c]=arguments[c];return c=3>o&&f[0]!==a&&f[o-1]!==a?[]:C(f,a),o-=c.length,o<e?fe(n,t,Xr,u.placeholder,F,f,c,F,F,e-o):r(this&&this!==Zn&&this instanceof u?i:n,this,f);
|
||||
}var i=Hr(n);return u}function Yr(n){return function(t,r,e){var u=ni(t);if(!pu(t)){var i=je(r,3);t=Lu(t),r=function(n){return i(u[n],n,u)}}return r=n(t,r,e),-1<r?u[i?t[r]:r]:F}}function Qr(n){return ge(function(t){var r=t.length,e=r,u=zn.prototype.thru;for(n&&t.reverse();e--;){var i=t[e];if(typeof i!="function")throw new ei("Expected a function");if(u&&!o&&"wrapper"==be(i))var o=new zn([],true)}for(e=o?e:r;++e<r;)var i=t[e],u=be(i),f="wrapper"==u?_o(i):F,o=f&&Be(f[0])&&424==f[1]&&!f[4].length&&1==f[9]?o[be(f[0])].apply(o,f[3]):1==i.length&&Be(i)?o[u]():o.thru(i);
|
||||
return function(){var n=arguments,e=n[0];if(o&&1==n.length&&af(e))return o.plant(e).value();for(var u=0,n=r?t[u].apply(this,n):e;++u<r;)n=t[u].call(this,n);return n}})}function Xr(n,t,r,e,u,i,o,f,c,a){function l(){for(var d=arguments.length,y=Hu(d),b=d;b--;)y[b]=arguments[b];if(_){var x,j=xe(l),b=y.length;for(x=0;b--;)y[b]===j&&++x}if(e&&(y=Cr(y,e,u,_)),i&&(y=Dr(y,i,o,_)),d-=x,_&&d<a)return j=C(y,j),fe(n,t,Xr,l.placeholder,r,y,j,f,c,a-d);if(j=h?r:this,b=p?j[n]:n,d=y.length,f){x=y.length;for(var w=Mi(f.length,x),m=Mr(y);w--;){
|
||||
var A=f[w];y[w]=Re(A,x)?m[A]:F}}else v&&1<d&&y.reverse();return s&&c<d&&(y.length=c),this&&this!==Zn&&this instanceof l&&(b=g||Hr(b)),b.apply(j,y)}var s=128&t,h=1&t,p=2&t,_=24&t,v=512&t,g=p?F:Hr(n);return l}function ne(n,t){return function(r,e){return Ct(r,n,t(e))}}function te(n,t){return function(r,e){var u;if(r===F&&e===F)return t;if(r!==F&&(u=r),e!==F){if(u===F)return e;typeof r=="string"||typeof e=="string"?(r=jr(r),e=jr(e)):(r=xr(r),e=xr(e)),u=n(r,e)}return u}}function re(n){return ge(function(t){
|
||||
return t=l(t,S(je())),lr(function(e){var u=this;return n(t,function(n){return r(n,u,e)})})})}function ee(n,t){t=t===F?" ":jr(t);var r=t.length;return 2>r?r?ar(t,n):t:(r=ar(t,Ri(n/T(t))),Bn.test(t)?zr($(r),0,n).join(""):r.slice(0,n))}function ue(n,t,e,u){function i(){for(var t=-1,c=arguments.length,a=-1,l=u.length,s=Hu(l+c),h=this&&this!==Zn&&this instanceof i?f:n;++a<l;)s[a]=u[a];for(;c--;)s[a++]=arguments[++t];return r(h,o?e:this,s)}var o=1&t,f=Hr(n);return i}function ie(n){return function(t,r,e){
|
||||
e&&typeof e!="number"&&ze(t,r,e)&&(r=e=F),t=Eu(t),r===F?(r=t,t=0):r=Eu(r),e=e===F?t<r?1:-1:Eu(e);var u=-1;r=Di(Ri((r-t)/(e||1)),0);for(var i=Hu(r);r--;)i[n?r:++u]=t,t+=e;return i}}function oe(n){return function(t,r){return typeof t=="string"&&typeof r=="string"||(t=Iu(t),r=Iu(r)),n(t,r)}}function fe(n,t,r,e,u,i,o,f,c,a){var l=8&t,s=l?o:F;o=l?F:o;var h=l?i:F;return i=l?F:i,t=(t|(l?32:64))&~(l?64:32),4&t||(t&=-4),u=[n,t,u,h,s,i,o,f,c,a],r=r.apply(F,u),Be(n)&&xo(r,u),r.placeholder=e,De(r,n,t)}function ce(n){
|
||||
var t=Xu[n];return function(n,r){if(n=Iu(n),r=null==r?0:Mi(Ou(r),292)){var e=(zu(n)+"e").split("e"),e=t(e[0]+"e"+(+e[1]+r)),e=(zu(e)+"e").split("e");return+(e[0]+"e"+(+e[1]-r))}return t(n)}}function ae(n){return function(t){var r=yo(t);return"[object Map]"==r?L(t):"[object Set]"==r?M(t):O(t,n(t))}}function le(n,t,r,e,u,i,o,f){var c=2&t;if(!c&&typeof n!="function")throw new ei("Expected a function");var a=e?e.length:0;if(a||(t&=-97,e=u=F),o=o===F?o:Di(Ou(o),0),f=f===F?f:Ou(f),a-=u?u.length:0,64&t){
|
||||
var l=e,s=u;e=u=F}var h=c?F:_o(n);return i=[n,t,r,e,u,l,s,i,o,f],h&&(r=i[1],n=h[1],t=r|n,e=128==n&&8==r||128==n&&256==r&&i[7].length<=h[8]||384==n&&h[7].length<=h[8]&&8==r,131>t||e)&&(1&n&&(i[2]=h[2],t|=1&r?0:4),(r=h[3])&&(e=i[3],i[3]=e?Cr(e,r,h[4]):r,i[4]=e?C(i[3],"__lodash_placeholder__"):h[4]),(r=h[5])&&(e=i[5],i[5]=e?Dr(e,r,h[6]):r,i[6]=e?C(i[5],"__lodash_placeholder__"):h[6]),(r=h[7])&&(i[7]=r),128&n&&(i[8]=null==i[8]?h[8]:Mi(i[8],h[8])),null==i[9]&&(i[9]=h[9]),i[0]=h[0],i[1]=t),n=i[0],t=i[1],
|
||||
r=i[2],e=i[3],u=i[4],f=i[9]=i[9]===F?c?0:n.length:Di(i[9]-a,0),!f&&24&t&&(t&=-25),De((h?lo:xo)(t&&1!=t?8==t||16==t?Jr(n,t,f):32!=t&&33!=t||u.length?Xr.apply(F,i):ue(n,t,r,e):Vr(n,t,r),i),n,t)}function se(n,t,r,e){return n===F||hu(n,ii[r])&&!ci.call(e,r)?t:n}function he(n,t,r,e,u,i){return bu(n)&&bu(t)&&(i.set(t,n),nr(n,t,F,he,i),i.delete(t)),n}function pe(n){return wu(n)?F:n}function _e(n,t,r,e,u,i){var o=1&r,f=n.length,c=t.length;if(f!=c&&!(o&&c>f))return false;if((c=i.get(n))&&i.get(t))return c==t;var c=-1,a=true,l=2&r?new qn:F;
|
||||
for(i.set(n,t),i.set(t,n);++c<f;){var s=n[c],h=t[c];if(e)var p=o?e(h,s,c,t,n,i):e(s,h,c,n,t,i);if(p!==F){if(p)continue;a=false;break}if(l){if(!_(t,function(n,t){if(!R(l,t)&&(s===n||u(s,n,r,e,i)))return l.push(t)})){a=false;break}}else if(s!==h&&!u(s,h,r,e,i)){a=false;break}}return i.delete(n),i.delete(t),a}function ve(n,t,r,e,u,i,o){switch(r){case"[object DataView]":if(n.byteLength!=t.byteLength||n.byteOffset!=t.byteOffset)break;n=n.buffer,t=t.buffer;case"[object ArrayBuffer]":if(n.byteLength!=t.byteLength||!i(new di(n),new di(t)))break;
|
||||
return true;case"[object Boolean]":case"[object Date]":case"[object Number]":return hu(+n,+t);case"[object Error]":return n.name==t.name&&n.message==t.message;case"[object RegExp]":case"[object String]":return n==t+"";case"[object Map]":var f=L;case"[object Set]":if(f||(f=D),n.size!=t.size&&!(1&e))break;return(r=o.get(n))?r==t:(e|=2,o.set(n,t),t=_e(f(n),f(t),e,u,i,o),o.delete(n),t);case"[object Symbol]":if(eo)return eo.call(n)==eo.call(t)}return false}function ge(n){return wo(Ce(n,F,Ve),n+"")}function de(n){
|
||||
return Rt(n,Lu,vo)}function ye(n){return Rt(n,Uu,go)}function be(n){for(var t=n.name+"",r=Ji[t],e=ci.call(Ji,t)?r.length:0;e--;){var u=r[e],i=u.func;if(null==i||i==n)return u.name}return t}function xe(n){return(ci.call(On,"placeholder")?On:n).placeholder}function je(){var n=On.iteratee||Pu,n=n===Pu?Gt:n;return arguments.length?n(arguments[0],arguments[1]):n}function we(n,t){var r=n.__data__,e=typeof t;return("string"==e||"number"==e||"symbol"==e||"boolean"==e?"__proto__"!==t:null===t)?r[typeof t=="string"?"string":"hash"]:r.map;
|
||||
}function me(n){for(var t=Lu(n),r=t.length;r--;){var e=t[r],u=n[e];t[r]=[e,u,u===u&&!bu(u)]}return t}function Ae(n,t){var r=null==n?F:n[t];return Zt(r)?r:F}function ke(n,t,r){t=Rr(t,n);for(var e=-1,u=t.length,i=false;++e<u;){var o=$e(t[e]);if(!(i=null!=n&&r(n,o)))break;n=n[o]}return i||++e!=u?i:(u=null==n?0:n.length,!!u&&yu(u)&&Re(o,u)&&(af(n)||cf(n)))}function Ee(n){var t=n.length,r=n.constructor(t);return t&&"string"==typeof n[0]&&ci.call(n,"index")&&(r.index=n.index,r.input=n.input),r}function Oe(n){
|
||||
return typeof n.constructor!="function"||Le(n)?{}:io(bi(n))}function Se(r,e,u,i){var o=r.constructor;switch(e){case"[object ArrayBuffer]":return Br(r);case"[object Boolean]":case"[object Date]":return new o(+r);case"[object DataView]":return e=i?Br(r.buffer):r.buffer,new r.constructor(e,r.byteOffset,r.byteLength);case"[object Float32Array]":case"[object Float64Array]":case"[object Int8Array]":case"[object Int16Array]":case"[object Int32Array]":case"[object Uint8Array]":case"[object Uint8ClampedArray]":
|
||||
case"[object Uint16Array]":case"[object Uint32Array]":return Lr(r,i);case"[object Map]":return e=i?u(L(r),1):L(r),h(e,n,new r.constructor);case"[object Number]":case"[object String]":return new o(r);case"[object RegExp]":return e=new r.constructor(r.source,dn.exec(r)),e.lastIndex=r.lastIndex,e;case"[object Set]":return e=i?u(D(r),1):D(r),h(e,t,new r.constructor);case"[object Symbol]":return eo?ni(eo.call(r)):{}}}function Ie(n){return af(n)||cf(n)||!!(mi&&n&&n[mi])}function Re(n,t){return t=null==t?9007199254740991:t,
|
||||
!!t&&(typeof n=="number"||wn.test(n))&&-1<n&&0==n%1&&n<t}function ze(n,t,r){if(!bu(r))return false;var e=typeof t;return!!("number"==e?pu(r)&&Re(t,r.length):"string"==e&&t in r)&&hu(r[t],n)}function We(n,t){if(af(n))return false;var r=typeof n;return!("number"!=r&&"symbol"!=r&&"boolean"!=r&&null!=n&&!Au(n))||(rn.test(n)||!tn.test(n)||null!=t&&n in ni(t))}function Be(n){var t=be(n),r=On[t];return typeof r=="function"&&t in Mn.prototype&&(n===r||(t=_o(r),!!t&&n===t[0]))}function Le(n){var t=n&&n.constructor;
|
||||
return n===(typeof t=="function"&&t.prototype||ii)}function Ue(n,t){return function(r){return null!=r&&(r[n]===t&&(t!==F||n in ni(r)))}}function Ce(n,t,e){return t=Di(t===F?n.length-1:t,0),function(){for(var u=arguments,i=-1,o=Di(u.length-t,0),f=Hu(o);++i<o;)f[i]=u[t+i];for(i=-1,o=Hu(t+1);++i<t;)o[i]=u[i];return o[t]=e(f),r(n,this,o)}}function De(n,t,r){var e=t+"";t=wo;var u,i=Ne;return u=(u=e.match(hn))?u[1].split(pn):[],r=i(u,r),(i=r.length)&&(u=i-1,r[u]=(1<i?"& ":"")+r[u],r=r.join(2<i?", ":" "),
|
||||
e=e.replace(sn,"{\n/* [wrapped with "+r+"] */\n")),t(n,e)}function Me(n){var t=0,r=0;return function(){var e=Ti(),u=16-(e-r);if(r=e,0<u){if(800<=++t)return arguments[0]}else t=0;return n.apply(F,arguments)}}function Te(n,t){var r=-1,e=n.length,u=e-1;for(t=t===F?e:t;++r<t;){var e=cr(r,u),i=n[e];n[e]=n[r],n[r]=i}return n.length=t,n}function $e(n){if(typeof n=="string"||Au(n))return n;var t=n+"";return"0"==t&&1/n==-N?"-0":t}function Fe(n){if(null!=n){try{return fi.call(n)}catch(n){}return n+""}return"";
|
||||
}function Ne(n,t){return u(Z,function(r){var e="_."+r[0];t&r[1]&&!c(n,e)&&n.push(e)}),n.sort()}function Pe(n){if(n instanceof Mn)return n.clone();var t=new zn(n.__wrapped__,n.__chain__);return t.__actions__=Mr(n.__actions__),t.__index__=n.__index__,t.__values__=n.__values__,t}function Ze(n,t,r){var e=null==n?0:n.length;return e?(r=null==r?0:Ou(r),0>r&&(r=Di(e+r,0)),g(n,je(t,3),r)):-1}function qe(n,t,r){var e=null==n?0:n.length;if(!e)return-1;var u=e-1;return r!==F&&(u=Ou(r),u=0>r?Di(e+u,0):Mi(u,e-1)),
|
||||
g(n,je(t,3),u,true)}function Ve(n){return(null==n?0:n.length)?kt(n,1):[]}function Ke(n){return n&&n.length?n[0]:F}function Ge(n){var t=null==n?0:n.length;return t?n[t-1]:F}function He(n,t){return n&&n.length&&t&&t.length?or(n,t):n}function Je(n){return null==n?n:Ni.call(n)}function Ye(n){if(!n||!n.length)return[];var t=0;return n=f(n,function(n){if(_u(n))return t=Di(n.length,t),true}),E(t,function(t){return l(n,j(t))})}function Qe(n,t){if(!n||!n.length)return[];var e=Ye(n);return null==t?e:l(e,function(n){
|
||||
return r(t,F,n)})}function Xe(n){return n=On(n),n.__chain__=true,n}function nu(n,t){return t(n)}function tu(){return this}function ru(n,t){return(af(n)?u:oo)(n,je(t,3))}function eu(n,t){return(af(n)?i:fo)(n,je(t,3))}function uu(n,t){return(af(n)?l:Yt)(n,je(t,3))}function iu(n,t,r){return t=r?F:t,t=n&&null==t?n.length:t,le(n,128,F,F,F,F,t)}function ou(n,t){var r;if(typeof t!="function")throw new ei("Expected a function");return n=Ou(n),function(){return 0<--n&&(r=t.apply(this,arguments)),1>=n&&(t=F),
|
||||
r}}function fu(n,t,r){return t=r?F:t,n=le(n,8,F,F,F,F,F,t),n.placeholder=fu.placeholder,n}function cu(n,t,r){return t=r?F:t,n=le(n,16,F,F,F,F,F,t),n.placeholder=cu.placeholder,n}function au(n,t,r){function e(t){var r=c,e=a;return c=a=F,_=t,s=n.apply(e,r)}function u(n){var r=n-p;return n-=_,p===F||r>=t||0>r||g&&n>=l}function i(){var n=Jo();if(u(n))return o(n);var r,e=jo;r=n-_,n=t-(n-p),r=g?Mi(n,l-r):n,h=e(i,r)}function o(n){return h=F,d&&c?e(n):(c=a=F,s)}function f(){var n=Jo(),r=u(n);if(c=arguments,
|
||||
a=this,p=n,r){if(h===F)return _=n=p,h=jo(i,t),v?e(n):s;if(g)return h=jo(i,t),e(p)}return h===F&&(h=jo(i,t)),s}var c,a,l,s,h,p,_=0,v=false,g=false,d=true;if(typeof n!="function")throw new ei("Expected a function");return t=Iu(t)||0,bu(r)&&(v=!!r.leading,l=(g="maxWait"in r)?Di(Iu(r.maxWait)||0,t):l,d="trailing"in r?!!r.trailing:d),f.cancel=function(){h!==F&&ho(h),_=0,c=p=a=h=F},f.flush=function(){return h===F?s:o(Jo())},f}function lu(n,t){function r(){var e=arguments,u=t?t.apply(this,e):e[0],i=r.cache;return i.has(u)?i.get(u):(e=n.apply(this,e),
|
||||
r.cache=i.set(u,e)||i,e)}if(typeof n!="function"||null!=t&&typeof t!="function")throw new ei("Expected a function");return r.cache=new(lu.Cache||Pn),r}function su(n){if(typeof n!="function")throw new ei("Expected a function");return function(){var t=arguments;switch(t.length){case 0:return!n.call(this);case 1:return!n.call(this,t[0]);case 2:return!n.call(this,t[0],t[1]);case 3:return!n.call(this,t[0],t[1],t[2])}return!n.apply(this,t)}}function hu(n,t){return n===t||n!==n&&t!==t}function pu(n){return null!=n&&yu(n.length)&&!gu(n);
|
||||
}function _u(n){return xu(n)&&pu(n)}function vu(n){if(!xu(n))return false;var t=zt(n);return"[object Error]"==t||"[object DOMException]"==t||typeof n.message=="string"&&typeof n.name=="string"&&!wu(n)}function gu(n){return!!bu(n)&&(n=zt(n),"[object Function]"==n||"[object GeneratorFunction]"==n||"[object AsyncFunction]"==n||"[object Proxy]"==n)}function du(n){return typeof n=="number"&&n==Ou(n)}function yu(n){return typeof n=="number"&&-1<n&&0==n%1&&9007199254740991>=n}function bu(n){var t=typeof n;return null!=n&&("object"==t||"function"==t);
|
||||
}function xu(n){return null!=n&&typeof n=="object"}function ju(n){return typeof n=="number"||xu(n)&&"[object Number]"==zt(n)}function wu(n){return!(!xu(n)||"[object Object]"!=zt(n))&&(n=bi(n),null===n||(n=ci.call(n,"constructor")&&n.constructor,typeof n=="function"&&n instanceof n&&fi.call(n)==hi))}function mu(n){return typeof n=="string"||!af(n)&&xu(n)&&"[object String]"==zt(n)}function Au(n){return typeof n=="symbol"||xu(n)&&"[object Symbol]"==zt(n)}function ku(n){if(!n)return[];if(pu(n))return mu(n)?$(n):Mr(n);
|
||||
if(Ai&&n[Ai]){n=n[Ai]();for(var t,r=[];!(t=n.next()).done;)r.push(t.value);return r}return t=yo(n),("[object Map]"==t?L:"[object Set]"==t?D:Du)(n)}function Eu(n){return n?(n=Iu(n),n===N||n===-N?1.7976931348623157e308*(0>n?-1:1):n===n?n:0):0===n?n:0}function Ou(n){n=Eu(n);var t=n%1;return n===n?t?n-t:n:0}function Su(n){return n?gt(Ou(n),0,4294967295):0}function Iu(n){if(typeof n=="number")return n;if(Au(n))return P;if(bu(n)&&(n=typeof n.valueOf=="function"?n.valueOf():n,n=bu(n)?n+"":n),typeof n!="string")return 0===n?n:+n;
|
||||
n=n.replace(cn,"");var t=bn.test(n);return t||jn.test(n)?Fn(n.slice(2),t?2:8):yn.test(n)?P:+n}function Ru(n){return Tr(n,Uu(n))}function zu(n){return null==n?"":jr(n)}function Wu(n,t,r){return n=null==n?F:It(n,t),n===F?r:n}function Bu(n,t){return null!=n&&ke(n,t,Lt)}function Lu(n){return pu(n)?Gn(n):Ht(n)}function Uu(n){if(pu(n))n=Gn(n,true);else if(bu(n)){var t,r=Le(n),e=[];for(t in n)("constructor"!=t||!r&&ci.call(n,t))&&e.push(t);n=e}else{if(t=[],null!=n)for(r in ni(n))t.push(r);n=t}return n}function Cu(n,t){
|
||||
if(null==n)return{};var r=l(ye(n),function(n){return[n]});return t=je(t),ur(n,r,function(n,r){return t(n,r[0])})}function Du(n){return null==n?[]:I(n,Lu(n))}function Mu(n){return Nf(zu(n).toLowerCase())}function Tu(n){return(n=zu(n))&&n.replace(mn,rt).replace(Rn,"")}function $u(n,t,r){return n=zu(n),t=r?F:t,t===F?Ln.test(n)?n.match(Wn)||[]:n.match(_n)||[]:n.match(t)||[]}function Fu(n){return function(){return n}}function Nu(n){return n}function Pu(n){return Gt(typeof n=="function"?n:dt(n,1))}function Zu(n,t,r){
|
||||
var e=Lu(t),i=St(t,e);null!=r||bu(t)&&(i.length||!e.length)||(r=t,t=n,n=this,i=St(t,Lu(t)));var o=!(bu(r)&&"chain"in r&&!r.chain),f=gu(n);return u(i,function(r){var e=t[r];n[r]=e,f&&(n.prototype[r]=function(){var t=this.__chain__;if(o||t){var r=n(this.__wrapped__);return(r.__actions__=Mr(this.__actions__)).push({func:e,args:arguments,thisArg:n}),r.__chain__=t,r}return e.apply(n,s([this.value()],arguments))})}),n}function qu(){}function Vu(n){return We(n)?j($e(n)):ir(n)}function Ku(){return[]}function Gu(){
|
||||
return false}En=null==En?Zn:it.defaults(Zn.Object(),En,it.pick(Zn,Un));var Hu=En.Array,Ju=En.Date,Yu=En.Error,Qu=En.Function,Xu=En.Math,ni=En.Object,ti=En.RegExp,ri=En.String,ei=En.TypeError,ui=Hu.prototype,ii=ni.prototype,oi=En["__core-js_shared__"],fi=Qu.prototype.toString,ci=ii.hasOwnProperty,ai=0,li=function(){var n=/[^.]+$/.exec(oi&&oi.keys&&oi.keys.IE_PROTO||"");return n?"Symbol(src)_1."+n:""}(),si=ii.toString,hi=fi.call(ni),pi=Zn._,_i=ti("^"+fi.call(ci).replace(on,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$"),vi=Kn?En.Buffer:F,gi=En.Symbol,di=En.Uint8Array,yi=vi?vi.f:F,bi=U(ni.getPrototypeOf,ni),xi=ni.create,ji=ii.propertyIsEnumerable,wi=ui.splice,mi=gi?gi.isConcatSpreadable:F,Ai=gi?gi.iterator:F,ki=gi?gi.toStringTag:F,Ei=function(){
|
||||
try{var n=Ae(ni,"defineProperty");return n({},"",{}),n}catch(n){}}(),Oi=En.clearTimeout!==Zn.clearTimeout&&En.clearTimeout,Si=Ju&&Ju.now!==Zn.Date.now&&Ju.now,Ii=En.setTimeout!==Zn.setTimeout&&En.setTimeout,Ri=Xu.ceil,zi=Xu.floor,Wi=ni.getOwnPropertySymbols,Bi=vi?vi.isBuffer:F,Li=En.isFinite,Ui=ui.join,Ci=U(ni.keys,ni),Di=Xu.max,Mi=Xu.min,Ti=Ju.now,$i=En.parseInt,Fi=Xu.random,Ni=ui.reverse,Pi=Ae(En,"DataView"),Zi=Ae(En,"Map"),qi=Ae(En,"Promise"),Vi=Ae(En,"Set"),Ki=Ae(En,"WeakMap"),Gi=Ae(ni,"create"),Hi=Ki&&new Ki,Ji={},Yi=Fe(Pi),Qi=Fe(Zi),Xi=Fe(qi),no=Fe(Vi),to=Fe(Ki),ro=gi?gi.prototype:F,eo=ro?ro.valueOf:F,uo=ro?ro.toString:F,io=function(){
|
||||
function n(){}return function(t){return bu(t)?xi?xi(t):(n.prototype=t,t=new n,n.prototype=F,t):{}}}();On.templateSettings={escape:Q,evaluate:X,interpolate:nn,variable:"",imports:{_:On}},On.prototype=Sn.prototype,On.prototype.constructor=On,zn.prototype=io(Sn.prototype),zn.prototype.constructor=zn,Mn.prototype=io(Sn.prototype),Mn.prototype.constructor=Mn,Tn.prototype.clear=function(){this.__data__=Gi?Gi(null):{},this.size=0},Tn.prototype.delete=function(n){return n=this.has(n)&&delete this.__data__[n],
|
||||
this.size-=n?1:0,n},Tn.prototype.get=function(n){var t=this.__data__;return Gi?(n=t[n],"__lodash_hash_undefined__"===n?F:n):ci.call(t,n)?t[n]:F},Tn.prototype.has=function(n){var t=this.__data__;return Gi?t[n]!==F:ci.call(t,n)},Tn.prototype.set=function(n,t){var r=this.__data__;return this.size+=this.has(n)?0:1,r[n]=Gi&&t===F?"__lodash_hash_undefined__":t,this},Nn.prototype.clear=function(){this.__data__=[],this.size=0},Nn.prototype.delete=function(n){var t=this.__data__;return n=lt(t,n),!(0>n)&&(n==t.length-1?t.pop():wi.call(t,n,1),
|
||||
--this.size,true)},Nn.prototype.get=function(n){var t=this.__data__;return n=lt(t,n),0>n?F:t[n][1]},Nn.prototype.has=function(n){return-1<lt(this.__data__,n)},Nn.prototype.set=function(n,t){var r=this.__data__,e=lt(r,n);return 0>e?(++this.size,r.push([n,t])):r[e][1]=t,this},Pn.prototype.clear=function(){this.size=0,this.__data__={hash:new Tn,map:new(Zi||Nn),string:new Tn}},Pn.prototype.delete=function(n){return n=we(this,n).delete(n),this.size-=n?1:0,n},Pn.prototype.get=function(n){return we(this,n).get(n);
|
||||
},Pn.prototype.has=function(n){return we(this,n).has(n)},Pn.prototype.set=function(n,t){var r=we(this,n),e=r.size;return r.set(n,t),this.size+=r.size==e?0:1,this},qn.prototype.add=qn.prototype.push=function(n){return this.__data__.set(n,"__lodash_hash_undefined__"),this},qn.prototype.has=function(n){return this.__data__.has(n)},Vn.prototype.clear=function(){this.__data__=new Nn,this.size=0},Vn.prototype.delete=function(n){var t=this.__data__;return n=t.delete(n),this.size=t.size,n},Vn.prototype.get=function(n){
|
||||
return this.__data__.get(n)},Vn.prototype.has=function(n){return this.__data__.has(n)},Vn.prototype.set=function(n,t){var r=this.__data__;if(r instanceof Nn){var e=r.__data__;if(!Zi||199>e.length)return e.push([n,t]),this.size=++r.size,this;r=this.__data__=new Pn(e)}return r.set(n,t),this.size=r.size,this};var oo=Zr(Et),fo=Zr(Ot,true),co=qr(),ao=qr(true),lo=Hi?function(n,t){return Hi.set(n,t),n}:Nu,so=Ei?function(n,t){return Ei(n,"toString",{configurable:true,enumerable:false,value:Fu(t),writable:true})}:Nu,ho=Oi||function(n){
|
||||
return Zn.clearTimeout(n)},po=Vi&&1/D(new Vi([,-0]))[1]==N?function(n){return new Vi(n)}:qu,_o=Hi?function(n){return Hi.get(n)}:qu,vo=Wi?function(n){return null==n?[]:(n=ni(n),f(Wi(n),function(t){return ji.call(n,t)}))}:Ku,go=Wi?function(n){for(var t=[];n;)s(t,vo(n)),n=bi(n);return t}:Ku,yo=zt;(Pi&&"[object DataView]"!=yo(new Pi(new ArrayBuffer(1)))||Zi&&"[object Map]"!=yo(new Zi)||qi&&"[object Promise]"!=yo(qi.resolve())||Vi&&"[object Set]"!=yo(new Vi)||Ki&&"[object WeakMap]"!=yo(new Ki))&&(yo=function(n){
|
||||
var t=zt(n);if(n=(n="[object Object]"==t?n.constructor:F)?Fe(n):"")switch(n){case Yi:return"[object DataView]";case Qi:return"[object Map]";case Xi:return"[object Promise]";case no:return"[object Set]";case to:return"[object WeakMap]"}return t});var bo=oi?gu:Gu,xo=Me(lo),jo=Ii||function(n,t){return Zn.setTimeout(n,t)},wo=Me(so),mo=function(n){n=lu(n,function(n){return 500===t.size&&t.clear(),n});var t=n.cache;return n}(function(n){var t=[];return en.test(n)&&t.push(""),n.replace(un,function(n,r,e,u){
|
||||
t.push(e?u.replace(vn,"$1"):r||n)}),t}),Ao=lr(function(n,t){return _u(n)?jt(n,kt(t,1,_u,true)):[]}),ko=lr(function(n,t){var r=Ge(t);return _u(r)&&(r=F),_u(n)?jt(n,kt(t,1,_u,true),je(r,2)):[]}),Eo=lr(function(n,t){var r=Ge(t);return _u(r)&&(r=F),_u(n)?jt(n,kt(t,1,_u,true),F,r):[]}),Oo=lr(function(n){var t=l(n,Sr);return t.length&&t[0]===n[0]?Ut(t):[]}),So=lr(function(n){var t=Ge(n),r=l(n,Sr);return t===Ge(r)?t=F:r.pop(),r.length&&r[0]===n[0]?Ut(r,je(t,2)):[]}),Io=lr(function(n){var t=Ge(n),r=l(n,Sr);return(t=typeof t=="function"?t:F)&&r.pop(),
|
||||
r.length&&r[0]===n[0]?Ut(r,F,t):[]}),Ro=lr(He),zo=ge(function(n,t){var r=null==n?0:n.length,e=vt(n,t);return fr(n,l(t,function(n){return Re(n,r)?+n:n}).sort(Ur)),e}),Wo=lr(function(n){return wr(kt(n,1,_u,true))}),Bo=lr(function(n){var t=Ge(n);return _u(t)&&(t=F),wr(kt(n,1,_u,true),je(t,2))}),Lo=lr(function(n){var t=Ge(n),t=typeof t=="function"?t:F;return wr(kt(n,1,_u,true),F,t)}),Uo=lr(function(n,t){return _u(n)?jt(n,t):[]}),Co=lr(function(n){return Er(f(n,_u))}),Do=lr(function(n){var t=Ge(n);return _u(t)&&(t=F),
|
||||
Er(f(n,_u),je(t,2))}),Mo=lr(function(n){var t=Ge(n),t=typeof t=="function"?t:F;return Er(f(n,_u),F,t)}),To=lr(Ye),$o=lr(function(n){var t=n.length,t=1<t?n[t-1]:F,t=typeof t=="function"?(n.pop(),t):F;return Qe(n,t)}),Fo=ge(function(n){function t(t){return vt(t,n)}var r=n.length,e=r?n[0]:0,u=this.__wrapped__;return!(1<r||this.__actions__.length)&&u instanceof Mn&&Re(e)?(u=u.slice(e,+e+(r?1:0)),u.__actions__.push({func:nu,args:[t],thisArg:F}),new zn(u,this.__chain__).thru(function(n){return r&&!n.length&&n.push(F),
|
||||
n})):this.thru(t)}),No=Nr(function(n,t,r){ci.call(n,r)?++n[r]:_t(n,r,1)}),Po=Yr(Ze),Zo=Yr(qe),qo=Nr(function(n,t,r){ci.call(n,r)?n[r].push(t):_t(n,r,[t])}),Vo=lr(function(n,t,e){var u=-1,i=typeof t=="function",o=pu(n)?Hu(n.length):[];return oo(n,function(n){o[++u]=i?r(t,n,e):Dt(n,t,e)}),o}),Ko=Nr(function(n,t,r){_t(n,r,t)}),Go=Nr(function(n,t,r){n[r?0:1].push(t)},function(){return[[],[]]}),Ho=lr(function(n,t){if(null==n)return[];var r=t.length;return 1<r&&ze(n,t[0],t[1])?t=[]:2<r&&ze(t[0],t[1],t[2])&&(t=[t[0]]),
|
||||
rr(n,kt(t,1),[])}),Jo=Si||function(){return Zn.Date.now()},Yo=lr(function(n,t,r){var e=1;if(r.length)var u=C(r,xe(Yo)),e=32|e;return le(n,e,t,r,u)}),Qo=lr(function(n,t,r){var e=3;if(r.length)var u=C(r,xe(Qo)),e=32|e;return le(t,e,n,r,u)}),Xo=lr(function(n,t){return xt(n,1,t)}),nf=lr(function(n,t,r){return xt(n,Iu(t)||0,r)});lu.Cache=Pn;var tf=lr(function(n,t){t=1==t.length&&af(t[0])?l(t[0],S(je())):l(kt(t,1),S(je()));var e=t.length;return lr(function(u){for(var i=-1,o=Mi(u.length,e);++i<o;)u[i]=t[i].call(this,u[i]);
|
||||
return r(n,this,u)})}),rf=lr(function(n,t){return le(n,32,F,t,C(t,xe(rf)))}),ef=lr(function(n,t){return le(n,64,F,t,C(t,xe(ef)))}),uf=ge(function(n,t){return le(n,256,F,F,F,t)}),of=oe(Wt),ff=oe(function(n,t){return n>=t}),cf=Mt(function(){return arguments}())?Mt:function(n){return xu(n)&&ci.call(n,"callee")&&!ji.call(n,"callee")},af=Hu.isArray,lf=Hn?S(Hn):Tt,sf=Bi||Gu,hf=Jn?S(Jn):$t,pf=Yn?S(Yn):Nt,_f=Qn?S(Qn):qt,vf=Xn?S(Xn):Vt,gf=nt?S(nt):Kt,df=oe(Jt),yf=oe(function(n,t){return n<=t}),bf=Pr(function(n,t){
|
||||
if(Le(t)||pu(t))Tr(t,Lu(t),n);else for(var r in t)ci.call(t,r)&&at(n,r,t[r])}),xf=Pr(function(n,t){Tr(t,Uu(t),n)}),jf=Pr(function(n,t,r,e){Tr(t,Uu(t),n,e)}),wf=Pr(function(n,t,r,e){Tr(t,Lu(t),n,e)}),mf=ge(vt),Af=lr(function(n){return n.push(F,se),r(jf,F,n)}),kf=lr(function(n){return n.push(F,he),r(Rf,F,n)}),Ef=ne(function(n,t,r){n[t]=r},Fu(Nu)),Of=ne(function(n,t,r){ci.call(n,t)?n[t].push(r):n[t]=[r]},je),Sf=lr(Dt),If=Pr(function(n,t,r){nr(n,t,r)}),Rf=Pr(function(n,t,r,e){nr(n,t,r,e)}),zf=ge(function(n,t){
|
||||
var r={};if(null==n)return r;var e=false;t=l(t,function(t){return t=Rr(t,n),e||(e=1<t.length),t}),Tr(n,ye(n),r),e&&(r=dt(r,7,pe));for(var u=t.length;u--;)mr(r,t[u]);return r}),Wf=ge(function(n,t){return null==n?{}:er(n,t)}),Bf=ae(Lu),Lf=ae(Uu),Uf=Gr(function(n,t,r){return t=t.toLowerCase(),n+(r?Mu(t):t)}),Cf=Gr(function(n,t,r){return n+(r?"-":"")+t.toLowerCase()}),Df=Gr(function(n,t,r){return n+(r?" ":"")+t.toLowerCase()}),Mf=Kr("toLowerCase"),Tf=Gr(function(n,t,r){return n+(r?"_":"")+t.toLowerCase();
|
||||
}),$f=Gr(function(n,t,r){return n+(r?" ":"")+Nf(t)}),Ff=Gr(function(n,t,r){return n+(r?" ":"")+t.toUpperCase()}),Nf=Kr("toUpperCase"),Pf=lr(function(n,t){try{return r(n,F,t)}catch(n){return vu(n)?n:new Yu(n)}}),Zf=ge(function(n,t){return u(t,function(t){t=$e(t),_t(n,t,Yo(n[t],n))}),n}),qf=Qr(),Vf=Qr(true),Kf=lr(function(n,t){return function(r){return Dt(r,n,t)}}),Gf=lr(function(n,t){return function(r){return Dt(n,r,t)}}),Hf=re(l),Jf=re(o),Yf=re(_),Qf=ie(),Xf=ie(true),nc=te(function(n,t){return n+t},0),tc=ce("ceil"),rc=te(function(n,t){
|
||||
return n/t},1),ec=ce("floor"),uc=te(function(n,t){return n*t},1),ic=ce("round"),oc=te(function(n,t){return n-t},0);return On.after=function(n,t){if(typeof t!="function")throw new ei("Expected a function");return n=Ou(n),function(){if(1>--n)return t.apply(this,arguments)}},On.ary=iu,On.assign=bf,On.assignIn=xf,On.assignInWith=jf,On.assignWith=wf,On.at=mf,On.before=ou,On.bind=Yo,On.bindAll=Zf,On.bindKey=Qo,On.castArray=function(){if(!arguments.length)return[];var n=arguments[0];return af(n)?n:[n]},
|
||||
On.chain=Xe,On.chunk=function(n,t,r){if(t=(r?ze(n,t,r):t===F)?1:Di(Ou(t),0),r=null==n?0:n.length,!r||1>t)return[];for(var e=0,u=0,i=Hu(Ri(r/t));e<r;)i[u++]=vr(n,e,e+=t);return i},On.compact=function(n){for(var t=-1,r=null==n?0:n.length,e=0,u=[];++t<r;){var i=n[t];i&&(u[e++]=i)}return u},On.concat=function(){var n=arguments.length;if(!n)return[];for(var t=Hu(n-1),r=arguments[0];n--;)t[n-1]=arguments[n];return s(af(r)?Mr(r):[r],kt(t,1))},On.cond=function(n){var t=null==n?0:n.length,e=je();return n=t?l(n,function(n){
|
||||
if("function"!=typeof n[1])throw new ei("Expected a function");return[e(n[0]),n[1]]}):[],lr(function(e){for(var u=-1;++u<t;){var i=n[u];if(r(i[0],this,e))return r(i[1],this,e)}})},On.conforms=function(n){return yt(dt(n,1))},On.constant=Fu,On.countBy=No,On.create=function(n,t){var r=io(n);return null==t?r:ht(r,t)},On.curry=fu,On.curryRight=cu,On.debounce=au,On.defaults=Af,On.defaultsDeep=kf,On.defer=Xo,On.delay=nf,On.difference=Ao,On.differenceBy=ko,On.differenceWith=Eo,On.drop=function(n,t,r){var e=null==n?0:n.length;
|
||||
return e?(t=r||t===F?1:Ou(t),vr(n,0>t?0:t,e)):[]},On.dropRight=function(n,t,r){var e=null==n?0:n.length;return e?(t=r||t===F?1:Ou(t),t=e-t,vr(n,0,0>t?0:t)):[]},On.dropRightWhile=function(n,t){return n&&n.length?Ar(n,je(t,3),true,true):[]},On.dropWhile=function(n,t){return n&&n.length?Ar(n,je(t,3),true):[]},On.fill=function(n,t,r,e){var u=null==n?0:n.length;if(!u)return[];for(r&&typeof r!="number"&&ze(n,t,r)&&(r=0,e=u),u=n.length,r=Ou(r),0>r&&(r=-r>u?0:u+r),e=e===F||e>u?u:Ou(e),0>e&&(e+=u),e=r>e?0:Su(e);r<e;)n[r++]=t;
|
||||
return n},On.filter=function(n,t){return(af(n)?f:At)(n,je(t,3))},On.flatMap=function(n,t){return kt(uu(n,t),1)},On.flatMapDeep=function(n,t){return kt(uu(n,t),N)},On.flatMapDepth=function(n,t,r){return r=r===F?1:Ou(r),kt(uu(n,t),r)},On.flatten=Ve,On.flattenDeep=function(n){return(null==n?0:n.length)?kt(n,N):[]},On.flattenDepth=function(n,t){return null!=n&&n.length?(t=t===F?1:Ou(t),kt(n,t)):[]},On.flip=function(n){return le(n,512)},On.flow=qf,On.flowRight=Vf,On.fromPairs=function(n){for(var t=-1,r=null==n?0:n.length,e={};++t<r;){
|
||||
var u=n[t];e[u[0]]=u[1]}return e},On.functions=function(n){return null==n?[]:St(n,Lu(n))},On.functionsIn=function(n){return null==n?[]:St(n,Uu(n))},On.groupBy=qo,On.initial=function(n){return(null==n?0:n.length)?vr(n,0,-1):[]},On.intersection=Oo,On.intersectionBy=So,On.intersectionWith=Io,On.invert=Ef,On.invertBy=Of,On.invokeMap=Vo,On.iteratee=Pu,On.keyBy=Ko,On.keys=Lu,On.keysIn=Uu,On.map=uu,On.mapKeys=function(n,t){var r={};return t=je(t,3),Et(n,function(n,e,u){_t(r,t(n,e,u),n)}),r},On.mapValues=function(n,t){
|
||||
var r={};return t=je(t,3),Et(n,function(n,e,u){_t(r,e,t(n,e,u))}),r},On.matches=function(n){return Qt(dt(n,1))},On.matchesProperty=function(n,t){return Xt(n,dt(t,1))},On.memoize=lu,On.merge=If,On.mergeWith=Rf,On.method=Kf,On.methodOf=Gf,On.mixin=Zu,On.negate=su,On.nthArg=function(n){return n=Ou(n),lr(function(t){return tr(t,n)})},On.omit=zf,On.omitBy=function(n,t){return Cu(n,su(je(t)))},On.once=function(n){return ou(2,n)},On.orderBy=function(n,t,r,e){return null==n?[]:(af(t)||(t=null==t?[]:[t]),
|
||||
r=e?F:r,af(r)||(r=null==r?[]:[r]),rr(n,t,r))},On.over=Hf,On.overArgs=tf,On.overEvery=Jf,On.overSome=Yf,On.partial=rf,On.partialRight=ef,On.partition=Go,On.pick=Wf,On.pickBy=Cu,On.property=Vu,On.propertyOf=function(n){return function(t){return null==n?F:It(n,t)}},On.pull=Ro,On.pullAll=He,On.pullAllBy=function(n,t,r){return n&&n.length&&t&&t.length?or(n,t,je(r,2)):n},On.pullAllWith=function(n,t,r){return n&&n.length&&t&&t.length?or(n,t,F,r):n},On.pullAt=zo,On.range=Qf,On.rangeRight=Xf,On.rearg=uf,On.reject=function(n,t){
|
||||
return(af(n)?f:At)(n,su(je(t,3)))},On.remove=function(n,t){var r=[];if(!n||!n.length)return r;var e=-1,u=[],i=n.length;for(t=je(t,3);++e<i;){var o=n[e];t(o,e,n)&&(r.push(o),u.push(e))}return fr(n,u),r},On.rest=function(n,t){if(typeof n!="function")throw new ei("Expected a function");return t=t===F?t:Ou(t),lr(n,t)},On.reverse=Je,On.sampleSize=function(n,t,r){return t=(r?ze(n,t,r):t===F)?1:Ou(t),(af(n)?ot:hr)(n,t)},On.set=function(n,t,r){return null==n?n:pr(n,t,r)},On.setWith=function(n,t,r,e){return e=typeof e=="function"?e:F,
|
||||
null==n?n:pr(n,t,r,e)},On.shuffle=function(n){return(af(n)?ft:_r)(n)},On.slice=function(n,t,r){var e=null==n?0:n.length;return e?(r&&typeof r!="number"&&ze(n,t,r)?(t=0,r=e):(t=null==t?0:Ou(t),r=r===F?e:Ou(r)),vr(n,t,r)):[]},On.sortBy=Ho,On.sortedUniq=function(n){return n&&n.length?br(n):[]},On.sortedUniqBy=function(n,t){return n&&n.length?br(n,je(t,2)):[]},On.split=function(n,t,r){return r&&typeof r!="number"&&ze(n,t,r)&&(t=r=F),r=r===F?4294967295:r>>>0,r?(n=zu(n))&&(typeof t=="string"||null!=t&&!_f(t))&&(t=jr(t),
|
||||
!t&&Bn.test(n))?zr($(n),0,r):n.split(t,r):[]},On.spread=function(n,t){if(typeof n!="function")throw new ei("Expected a function");return t=null==t?0:Di(Ou(t),0),lr(function(e){var u=e[t];return e=zr(e,0,t),u&&s(e,u),r(n,this,e)})},On.tail=function(n){var t=null==n?0:n.length;return t?vr(n,1,t):[]},On.take=function(n,t,r){return n&&n.length?(t=r||t===F?1:Ou(t),vr(n,0,0>t?0:t)):[]},On.takeRight=function(n,t,r){var e=null==n?0:n.length;return e?(t=r||t===F?1:Ou(t),t=e-t,vr(n,0>t?0:t,e)):[]},On.takeRightWhile=function(n,t){
|
||||
return n&&n.length?Ar(n,je(t,3),false,true):[]},On.takeWhile=function(n,t){return n&&n.length?Ar(n,je(t,3)):[]},On.tap=function(n,t){return t(n),n},On.throttle=function(n,t,r){var e=true,u=true;if(typeof n!="function")throw new ei("Expected a function");return bu(r)&&(e="leading"in r?!!r.leading:e,u="trailing"in r?!!r.trailing:u),au(n,t,{leading:e,maxWait:t,trailing:u})},On.thru=nu,On.toArray=ku,On.toPairs=Bf,On.toPairsIn=Lf,On.toPath=function(n){return af(n)?l(n,$e):Au(n)?[n]:Mr(mo(zu(n)))},On.toPlainObject=Ru,
|
||||
On.transform=function(n,t,r){var e=af(n),i=e||sf(n)||gf(n);if(t=je(t,4),null==r){var o=n&&n.constructor;r=i?e?new o:[]:bu(n)&&gu(o)?io(bi(n)):{}}return(i?u:Et)(n,function(n,e,u){return t(r,n,e,u)}),r},On.unary=function(n){return iu(n,1)},On.union=Wo,On.unionBy=Bo,On.unionWith=Lo,On.uniq=function(n){return n&&n.length?wr(n):[]},On.uniqBy=function(n,t){return n&&n.length?wr(n,je(t,2)):[]},On.uniqWith=function(n,t){return t=typeof t=="function"?t:F,n&&n.length?wr(n,F,t):[]},On.unset=function(n,t){return null==n||mr(n,t);
|
||||
},On.unzip=Ye,On.unzipWith=Qe,On.update=function(n,t,r){return null==n?n:pr(n,t,Ir(r)(It(n,t)),void 0)},On.updateWith=function(n,t,r,e){return e=typeof e=="function"?e:F,null!=n&&(n=pr(n,t,Ir(r)(It(n,t)),e)),n},On.values=Du,On.valuesIn=function(n){return null==n?[]:I(n,Uu(n))},On.without=Uo,On.words=$u,On.wrap=function(n,t){return rf(Ir(t),n)},On.xor=Co,On.xorBy=Do,On.xorWith=Mo,On.zip=To,On.zipObject=function(n,t){return Or(n||[],t||[],at)},On.zipObjectDeep=function(n,t){return Or(n||[],t||[],pr);
|
||||
},On.zipWith=$o,On.entries=Bf,On.entriesIn=Lf,On.extend=xf,On.extendWith=jf,Zu(On,On),On.add=nc,On.attempt=Pf,On.camelCase=Uf,On.capitalize=Mu,On.ceil=tc,On.clamp=function(n,t,r){return r===F&&(r=t,t=F),r!==F&&(r=Iu(r),r=r===r?r:0),t!==F&&(t=Iu(t),t=t===t?t:0),gt(Iu(n),t,r)},On.clone=function(n){return dt(n,4)},On.cloneDeep=function(n){return dt(n,5)},On.cloneDeepWith=function(n,t){return t=typeof t=="function"?t:F,dt(n,5,t)},On.cloneWith=function(n,t){return t=typeof t=="function"?t:F,dt(n,4,t)},
|
||||
On.conformsTo=function(n,t){return null==t||bt(n,t,Lu(t))},On.deburr=Tu,On.defaultTo=function(n,t){return null==n||n!==n?t:n},On.divide=rc,On.endsWith=function(n,t,r){n=zu(n),t=jr(t);var e=n.length,e=r=r===F?e:gt(Ou(r),0,e);return r-=t.length,0<=r&&n.slice(r,e)==t},On.eq=hu,On.escape=function(n){return(n=zu(n))&&Y.test(n)?n.replace(H,et):n},On.escapeRegExp=function(n){return(n=zu(n))&&fn.test(n)?n.replace(on,"\\$&"):n},On.every=function(n,t,r){var e=af(n)?o:wt;return r&&ze(n,t,r)&&(t=F),e(n,je(t,3));
|
||||
},On.find=Po,On.findIndex=Ze,On.findKey=function(n,t){return v(n,je(t,3),Et)},On.findLast=Zo,On.findLastIndex=qe,On.findLastKey=function(n,t){return v(n,je(t,3),Ot)},On.floor=ec,On.forEach=ru,On.forEachRight=eu,On.forIn=function(n,t){return null==n?n:co(n,je(t,3),Uu)},On.forInRight=function(n,t){return null==n?n:ao(n,je(t,3),Uu)},On.forOwn=function(n,t){return n&&Et(n,je(t,3))},On.forOwnRight=function(n,t){return n&&Ot(n,je(t,3))},On.get=Wu,On.gt=of,On.gte=ff,On.has=function(n,t){return null!=n&&ke(n,t,Bt);
|
||||
},On.hasIn=Bu,On.head=Ke,On.identity=Nu,On.includes=function(n,t,r,e){return n=pu(n)?n:Du(n),r=r&&!e?Ou(r):0,e=n.length,0>r&&(r=Di(e+r,0)),mu(n)?r<=e&&-1<n.indexOf(t,r):!!e&&-1<d(n,t,r)},On.indexOf=function(n,t,r){var e=null==n?0:n.length;return e?(r=null==r?0:Ou(r),0>r&&(r=Di(e+r,0)),d(n,t,r)):-1},On.inRange=function(n,t,r){return t=Eu(t),r===F?(r=t,t=0):r=Eu(r),n=Iu(n),n>=Mi(t,r)&&n<Di(t,r)},On.invoke=Sf,On.isArguments=cf,On.isArray=af,On.isArrayBuffer=lf,On.isArrayLike=pu,On.isArrayLikeObject=_u,
|
||||
On.isBoolean=function(n){return true===n||false===n||xu(n)&&"[object Boolean]"==zt(n)},On.isBuffer=sf,On.isDate=hf,On.isElement=function(n){return xu(n)&&1===n.nodeType&&!wu(n)},On.isEmpty=function(n){if(null==n)return true;if(pu(n)&&(af(n)||typeof n=="string"||typeof n.splice=="function"||sf(n)||gf(n)||cf(n)))return!n.length;var t=yo(n);if("[object Map]"==t||"[object Set]"==t)return!n.size;if(Le(n))return!Ht(n).length;for(var r in n)if(ci.call(n,r))return false;return true},On.isEqual=function(n,t){return Ft(n,t);
|
||||
},On.isEqualWith=function(n,t,r){var e=(r=typeof r=="function"?r:F)?r(n,t):F;return e===F?Ft(n,t,F,r):!!e},On.isError=vu,On.isFinite=function(n){return typeof n=="number"&&Li(n)},On.isFunction=gu,On.isInteger=du,On.isLength=yu,On.isMap=pf,On.isMatch=function(n,t){return n===t||Pt(n,t,me(t))},On.isMatchWith=function(n,t,r){return r=typeof r=="function"?r:F,Pt(n,t,me(t),r)},On.isNaN=function(n){return ju(n)&&n!=+n},On.isNative=function(n){if(bo(n))throw new Yu("Unsupported core-js use. Try https://npms.io/search?q=ponyfill.");
|
||||
return Zt(n)},On.isNil=function(n){return null==n},On.isNull=function(n){return null===n},On.isNumber=ju,On.isObject=bu,On.isObjectLike=xu,On.isPlainObject=wu,On.isRegExp=_f,On.isSafeInteger=function(n){return du(n)&&-9007199254740991<=n&&9007199254740991>=n},On.isSet=vf,On.isString=mu,On.isSymbol=Au,On.isTypedArray=gf,On.isUndefined=function(n){return n===F},On.isWeakMap=function(n){return xu(n)&&"[object WeakMap]"==yo(n)},On.isWeakSet=function(n){return xu(n)&&"[object WeakSet]"==zt(n)},On.join=function(n,t){
|
||||
return null==n?"":Ui.call(n,t)},On.kebabCase=Cf,On.last=Ge,On.lastIndexOf=function(n,t,r){var e=null==n?0:n.length;if(!e)return-1;var u=e;if(r!==F&&(u=Ou(r),u=0>u?Di(e+u,0):Mi(u,e-1)),t===t){for(r=u+1;r--&&n[r]!==t;);n=r}else n=g(n,b,u,true);return n},On.lowerCase=Df,On.lowerFirst=Mf,On.lt=df,On.lte=yf,On.max=function(n){return n&&n.length?mt(n,Nu,Wt):F},On.maxBy=function(n,t){return n&&n.length?mt(n,je(t,2),Wt):F},On.mean=function(n){return x(n,Nu)},On.meanBy=function(n,t){return x(n,je(t,2))},On.min=function(n){
|
||||
return n&&n.length?mt(n,Nu,Jt):F},On.minBy=function(n,t){return n&&n.length?mt(n,je(t,2),Jt):F},On.stubArray=Ku,On.stubFalse=Gu,On.stubObject=function(){return{}},On.stubString=function(){return""},On.stubTrue=function(){return true},On.multiply=uc,On.nth=function(n,t){return n&&n.length?tr(n,Ou(t)):F},On.noConflict=function(){return Zn._===this&&(Zn._=pi),this},On.noop=qu,On.now=Jo,On.pad=function(n,t,r){n=zu(n);var e=(t=Ou(t))?T(n):0;return!t||e>=t?n:(t=(t-e)/2,ee(zi(t),r)+n+ee(Ri(t),r))},On.padEnd=function(n,t,r){
|
||||
n=zu(n);var e=(t=Ou(t))?T(n):0;return t&&e<t?n+ee(t-e,r):n},On.padStart=function(n,t,r){n=zu(n);var e=(t=Ou(t))?T(n):0;return t&&e<t?ee(t-e,r)+n:n},On.parseInt=function(n,t,r){return r||null==t?t=0:t&&(t=+t),$i(zu(n).replace(an,""),t||0)},On.random=function(n,t,r){if(r&&typeof r!="boolean"&&ze(n,t,r)&&(t=r=F),r===F&&(typeof t=="boolean"?(r=t,t=F):typeof n=="boolean"&&(r=n,n=F)),n===F&&t===F?(n=0,t=1):(n=Eu(n),t===F?(t=n,n=0):t=Eu(t)),n>t){var e=n;n=t,t=e}return r||n%1||t%1?(r=Fi(),Mi(n+r*(t-n+$n("1e-"+((r+"").length-1))),t)):cr(n,t);
|
||||
},On.reduce=function(n,t,r){var e=af(n)?h:m,u=3>arguments.length;return e(n,je(t,4),r,u,oo)},On.reduceRight=function(n,t,r){var e=af(n)?p:m,u=3>arguments.length;return e(n,je(t,4),r,u,fo)},On.repeat=function(n,t,r){return t=(r?ze(n,t,r):t===F)?1:Ou(t),ar(zu(n),t)},On.replace=function(){var n=arguments,t=zu(n[0]);return 3>n.length?t:t.replace(n[1],n[2])},On.result=function(n,t,r){t=Rr(t,n);var e=-1,u=t.length;for(u||(u=1,n=F);++e<u;){var i=null==n?F:n[$e(t[e])];i===F&&(e=u,i=r),n=gu(i)?i.call(n):i;
|
||||
}return n},On.round=ic,On.runInContext=w,On.sample=function(n){return(af(n)?tt:sr)(n)},On.size=function(n){if(null==n)return 0;if(pu(n))return mu(n)?T(n):n.length;var t=yo(n);return"[object Map]"==t||"[object Set]"==t?n.size:Ht(n).length},On.snakeCase=Tf,On.some=function(n,t,r){var e=af(n)?_:gr;return r&&ze(n,t,r)&&(t=F),e(n,je(t,3))},On.sortedIndex=function(n,t){return dr(n,t)},On.sortedIndexBy=function(n,t,r){return yr(n,t,je(r,2))},On.sortedIndexOf=function(n,t){var r=null==n?0:n.length;if(r){
|
||||
var e=dr(n,t);if(e<r&&hu(n[e],t))return e}return-1},On.sortedLastIndex=function(n,t){return dr(n,t,true)},On.sortedLastIndexBy=function(n,t,r){return yr(n,t,je(r,2),true)},On.sortedLastIndexOf=function(n,t){if(null==n?0:n.length){var r=dr(n,t,true)-1;if(hu(n[r],t))return r}return-1},On.startCase=$f,On.startsWith=function(n,t,r){return n=zu(n),r=null==r?0:gt(Ou(r),0,n.length),t=jr(t),n.slice(r,r+t.length)==t},On.subtract=oc,On.sum=function(n){return n&&n.length?k(n,Nu):0},On.sumBy=function(n,t){return n&&n.length?k(n,je(t,2)):0;
|
||||
},On.template=function(n,t,r){var e=On.templateSettings;r&&ze(n,t,r)&&(t=F),n=zu(n),t=jf({},t,e,se),r=jf({},t.imports,e.imports,se);var u,i,o=Lu(r),f=I(r,o),c=0;r=t.interpolate||An;var a="__p+='";r=ti((t.escape||An).source+"|"+r.source+"|"+(r===nn?gn:An).source+"|"+(t.evaluate||An).source+"|$","g");var l="sourceURL"in t?"//# sourceURL="+t.sourceURL+"\n":"";if(n.replace(r,function(t,r,e,o,f,l){return e||(e=o),a+=n.slice(c,l).replace(kn,B),r&&(u=true,a+="'+__e("+r+")+'"),f&&(i=true,a+="';"+f+";\n__p+='"),
|
||||
e&&(a+="'+((__t=("+e+"))==null?'':__t)+'"),c=l+t.length,t}),a+="';",(t=t.variable)||(a="with(obj){"+a+"}"),a=(i?a.replace(q,""):a).replace(V,"$1").replace(K,"$1;"),a="function("+(t||"obj")+"){"+(t?"":"obj||(obj={});")+"var __t,__p=''"+(u?",__e=_.escape":"")+(i?",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,'')}":";")+a+"return __p}",t=Pf(function(){return Qu(o,l+"return "+a).apply(F,f)}),t.source=a,vu(t))throw t;return t},On.times=function(n,t){if(n=Ou(n),1>n||9007199254740991<n)return[];
|
||||
var r=4294967295,e=Mi(n,4294967295);for(t=je(t),n-=4294967295,e=E(e,t);++r<n;)t(r);return e},On.toFinite=Eu,On.toInteger=Ou,On.toLength=Su,On.toLower=function(n){return zu(n).toLowerCase()},On.toNumber=Iu,On.toSafeInteger=function(n){return n?gt(Ou(n),-9007199254740991,9007199254740991):0===n?n:0},On.toString=zu,On.toUpper=function(n){return zu(n).toUpperCase()},On.trim=function(n,t,r){return(n=zu(n))&&(r||t===F)?n.replace(cn,""):n&&(t=jr(t))?(n=$(n),r=$(t),t=z(n,r),r=W(n,r)+1,zr(n,t,r).join("")):n;
|
||||
},On.trimEnd=function(n,t,r){return(n=zu(n))&&(r||t===F)?n.replace(ln,""):n&&(t=jr(t))?(n=$(n),t=W(n,$(t))+1,zr(n,0,t).join("")):n},On.trimStart=function(n,t,r){return(n=zu(n))&&(r||t===F)?n.replace(an,""):n&&(t=jr(t))?(n=$(n),t=z(n,$(t)),zr(n,t).join("")):n},On.truncate=function(n,t){var r=30,e="...";if(bu(t))var u="separator"in t?t.separator:u,r="length"in t?Ou(t.length):r,e="omission"in t?jr(t.omission):e;n=zu(n);var i=n.length;if(Bn.test(n))var o=$(n),i=o.length;if(r>=i)return n;if(i=r-T(e),1>i)return e;
|
||||
if(r=o?zr(o,0,i).join(""):n.slice(0,i),u===F)return r+e;if(o&&(i+=r.length-i),_f(u)){if(n.slice(i).search(u)){var f=r;for(u.global||(u=ti(u.source,zu(dn.exec(u))+"g")),u.lastIndex=0;o=u.exec(f);)var c=o.index;r=r.slice(0,c===F?i:c)}}else n.indexOf(jr(u),i)!=i&&(u=r.lastIndexOf(u),-1<u&&(r=r.slice(0,u)));return r+e},On.unescape=function(n){return(n=zu(n))&&J.test(n)?n.replace(G,ut):n},On.uniqueId=function(n){var t=++ai;return zu(n)+t},On.upperCase=Ff,On.upperFirst=Nf,On.each=ru,On.eachRight=eu,On.first=Ke,
|
||||
Zu(On,function(){var n={};return Et(On,function(t,r){ci.call(On.prototype,r)||(n[r]=t)}),n}(),{chain:false}),On.VERSION="4.17.4",u("bind bindKey curry curryRight partial partialRight".split(" "),function(n){On[n].placeholder=On}),u(["drop","take"],function(n,t){Mn.prototype[n]=function(r){r=r===F?1:Di(Ou(r),0);var e=this.__filtered__&&!t?new Mn(this):this.clone();return e.__filtered__?e.__takeCount__=Mi(r,e.__takeCount__):e.__views__.push({size:Mi(r,4294967295),type:n+(0>e.__dir__?"Right":"")}),e},Mn.prototype[n+"Right"]=function(t){
|
||||
return this.reverse()[n](t).reverse()}}),u(["filter","map","takeWhile"],function(n,t){var r=t+1,e=1==r||3==r;Mn.prototype[n]=function(n){var t=this.clone();return t.__iteratees__.push({iteratee:je(n,3),type:r}),t.__filtered__=t.__filtered__||e,t}}),u(["head","last"],function(n,t){var r="take"+(t?"Right":"");Mn.prototype[n]=function(){return this[r](1).value()[0]}}),u(["initial","tail"],function(n,t){var r="drop"+(t?"":"Right");Mn.prototype[n]=function(){return this.__filtered__?new Mn(this):this[r](1);
|
||||
}}),Mn.prototype.compact=function(){return this.filter(Nu)},Mn.prototype.find=function(n){return this.filter(n).head()},Mn.prototype.findLast=function(n){return this.reverse().find(n)},Mn.prototype.invokeMap=lr(function(n,t){return typeof n=="function"?new Mn(this):this.map(function(r){return Dt(r,n,t)})}),Mn.prototype.reject=function(n){return this.filter(su(je(n)))},Mn.prototype.slice=function(n,t){n=Ou(n);var r=this;return r.__filtered__&&(0<n||0>t)?new Mn(r):(0>n?r=r.takeRight(-n):n&&(r=r.drop(n)),
|
||||
t!==F&&(t=Ou(t),r=0>t?r.dropRight(-t):r.take(t-n)),r)},Mn.prototype.takeRightWhile=function(n){return this.reverse().takeWhile(n).reverse()},Mn.prototype.toArray=function(){return this.take(4294967295)},Et(Mn.prototype,function(n,t){var r=/^(?:filter|find|map|reject)|While$/.test(t),e=/^(?:head|last)$/.test(t),u=On[e?"take"+("last"==t?"Right":""):t],i=e||/^find/.test(t);u&&(On.prototype[t]=function(){function t(n){return n=u.apply(On,s([n],f)),e&&h?n[0]:n}var o=this.__wrapped__,f=e?[1]:arguments,c=o instanceof Mn,a=f[0],l=c||af(o);
|
||||
l&&r&&typeof a=="function"&&1!=a.length&&(c=l=false);var h=this.__chain__,p=!!this.__actions__.length,a=i&&!h,c=c&&!p;return!i&&l?(o=c?o:new Mn(this),o=n.apply(o,f),o.__actions__.push({func:nu,args:[t],thisArg:F}),new zn(o,h)):a&&c?n.apply(this,f):(o=this.thru(t),a?e?o.value()[0]:o.value():o)})}),u("pop push shift sort splice unshift".split(" "),function(n){var t=ui[n],r=/^(?:push|sort|unshift)$/.test(n)?"tap":"thru",e=/^(?:pop|shift)$/.test(n);On.prototype[n]=function(){var n=arguments;if(e&&!this.__chain__){
|
||||
var u=this.value();return t.apply(af(u)?u:[],n)}return this[r](function(r){return t.apply(af(r)?r:[],n)})}}),Et(Mn.prototype,function(n,t){var r=On[t];if(r){var e=r.name+"";(Ji[e]||(Ji[e]=[])).push({name:t,func:r})}}),Ji[Xr(F,2).name]=[{name:"wrapper",func:F}],Mn.prototype.clone=function(){var n=new Mn(this.__wrapped__);return n.__actions__=Mr(this.__actions__),n.__dir__=this.__dir__,n.__filtered__=this.__filtered__,n.__iteratees__=Mr(this.__iteratees__),n.__takeCount__=this.__takeCount__,n.__views__=Mr(this.__views__),
|
||||
n},Mn.prototype.reverse=function(){if(this.__filtered__){var n=new Mn(this);n.__dir__=-1,n.__filtered__=true}else n=this.clone(),n.__dir__*=-1;return n},Mn.prototype.value=function(){var n,t=this.__wrapped__.value(),r=this.__dir__,e=af(t),u=0>r,i=e?t.length:0;n=i;for(var o=this.__views__,f=0,c=-1,a=o.length;++c<a;){var l=o[c],s=l.size;switch(l.type){case"drop":f+=s;break;case"dropRight":n-=s;break;case"take":n=Mi(n,f+s);break;case"takeRight":f=Di(f,n-s)}}if(n={start:f,end:n},o=n.start,f=n.end,n=f-o,
|
||||
o=u?f:o-1,f=this.__iteratees__,c=f.length,a=0,l=Mi(n,this.__takeCount__),!e||!u&&i==n&&l==n)return kr(t,this.__actions__);e=[];n:for(;n--&&a<l;){for(o+=r,u=-1,i=t[o];++u<c;){var h=f[u],s=h.type,h=(0,h.iteratee)(i);if(2==s)i=h;else if(!h){if(1==s)continue n;break n}}e[a++]=i}return e},On.prototype.at=Fo,On.prototype.chain=function(){return Xe(this)},On.prototype.commit=function(){return new zn(this.value(),this.__chain__)},On.prototype.next=function(){this.__values__===F&&(this.__values__=ku(this.value()));
|
||||
var n=this.__index__>=this.__values__.length;return{done:n,value:n?F:this.__values__[this.__index__++]}},On.prototype.plant=function(n){for(var t,r=this;r instanceof Sn;){var e=Pe(r);e.__index__=0,e.__values__=F,t?u.__wrapped__=e:t=e;var u=e,r=r.__wrapped__}return u.__wrapped__=n,t},On.prototype.reverse=function(){var n=this.__wrapped__;return n instanceof Mn?(this.__actions__.length&&(n=new Mn(this)),n=n.reverse(),n.__actions__.push({func:nu,args:[Je],thisArg:F}),new zn(n,this.__chain__)):this.thru(Je);
|
||||
},On.prototype.toJSON=On.prototype.valueOf=On.prototype.value=function(){return kr(this.__wrapped__,this.__actions__)},On.prototype.first=On.prototype.head,Ai&&(On.prototype[Ai]=tu),On}();typeof define=="function"&&typeof define.amd=="object"&&define.amd?(Zn._=it, define(function(){return it})):Vn?((Vn.exports=it)._=it,qn._=it):Zn._=it}).call(this);
|
File diff suppressed because one or more lines are too long
24
package.json
24
package.json
@ -1,24 +0,0 @@
|
||||
{
|
||||
"name": "library",
|
||||
"version": "1.0.0",
|
||||
"description": "Managing my personal library",
|
||||
"scripts": {
|
||||
"start": "node ./index.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/deltamualpha/my-library.git"
|
||||
},
|
||||
"author": "https://github.com/deltamualpha/",
|
||||
"license": "BSD-3-Clause",
|
||||
"bugs": {
|
||||
"url": "https://github.com/deltamualpha/my-library/issues"
|
||||
},
|
||||
"homepage": "https://github.com/deltamualpha/my-library",
|
||||
"dependencies": {
|
||||
"google-auth-library": "^0.10.0",
|
||||
"google-books-search": "^0.3.1",
|
||||
"googleapis": "^19.0.0",
|
||||
"lodash": "^4.17.4"
|
||||
}
|
||||
}
|
30
readme.md
30
readme.md
@ -1,30 +0,0 @@
|
||||
# library
|
||||
|
||||
set `publicSpreadsheetUrl` in `index.html` to set a sheet
|
||||
|
||||
expects the following columns, but easily updated:
|
||||
|
||||
* title
|
||||
* author
|
||||
* authorLast (used for sorting)
|
||||
* isbn-10 (not displayed)
|
||||
* isbn-13
|
||||
* format
|
||||
* genre
|
||||
* publisher
|
||||
* series
|
||||
* volume
|
||||
* year
|
||||
* coverurl
|
||||
* description
|
||||
* notes
|
||||
* signed (`yes` or blank)
|
||||
|
||||
## adding books using the google books API
|
||||
|
||||
1. update `SHEET_ID` and `SHEET_NAME` in `index.js`.
|
||||
2. follow the instructions at <https://developers.google.com/sheets/api/quickstart/nodejs#step_1_turn_on_the_api_name>, and save the resulting json file to `.credentials/client_secret.json`.
|
||||
3. run `npm install`.
|
||||
4. run `npm start`.
|
||||
|
||||
the google books API is somewhat spotty; searching for a title at https://books.google.com/ will sometimes pull up a book and provide the ISBN they know it by. It also doesn't have comprehensive coverage of various editions of titles, and doesn't return image URLs or format information.
|
116
ui/mock.go
Normal file
116
ui/mock.go
Normal file
@ -0,0 +1,116 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
type coord struct {
|
||||
x, y int
|
||||
}
|
||||
|
||||
type MockScreen struct {
|
||||
x, y, h, w int
|
||||
content map[coord]rune
|
||||
}
|
||||
|
||||
func (m *MockScreen) Init() error {
|
||||
m.content = map[coord]rune{}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockScreen) Fini() {}
|
||||
|
||||
func (m *MockScreen) Clear() {
|
||||
m.content = map[coord]rune{}
|
||||
}
|
||||
|
||||
func (m *MockScreen) Fill(rune, tcell.Style) {}
|
||||
|
||||
func (m *MockScreen) SetCell(x int, y int, style tcell.Style, ch ...rune) {}
|
||||
|
||||
func (m *MockScreen) GetContent(x, y int) (mainc rune, combc []rune, style tcell.Style, width int) {
|
||||
return m.content[coord{x, y}], nil, tcell.StyleDefault, 1
|
||||
}
|
||||
|
||||
func (m *MockScreen) SetContent(x int, y int, mainc rune, combc []rune, style tcell.Style) {
|
||||
m.content[coord{x, y}] = mainc
|
||||
}
|
||||
|
||||
func (m *MockScreen) SetStyle(style tcell.Style) {}
|
||||
|
||||
func (m *MockScreen) ShowCursor(x int, y int) {}
|
||||
|
||||
func (m *MockScreen) HideCursor() {}
|
||||
|
||||
func (m *MockScreen) Size() (int, int) {
|
||||
return m.h, m.w
|
||||
}
|
||||
|
||||
func (m *MockScreen) PollEvent() tcell.Event {
|
||||
return tcell.NewEventError(fmt.Errorf("mock error"))
|
||||
}
|
||||
|
||||
func (m *MockScreen) PostEvent(ev tcell.Event) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockScreen) PostEventWait(ev tcell.Event) {}
|
||||
|
||||
func (m *MockScreen) EnableMouse() {}
|
||||
|
||||
func (m *MockScreen) DisableMouse() {}
|
||||
|
||||
func (m *MockScreen) HasMouse() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *MockScreen) Colors() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *MockScreen) Show() {}
|
||||
|
||||
func (m *MockScreen) Sync() {}
|
||||
|
||||
func (m *MockScreen) CharacterSet() string {
|
||||
return "UTF-8"
|
||||
}
|
||||
|
||||
func (m *MockScreen) RegisterRuneFallback(r rune, subst string) {}
|
||||
|
||||
func (m *MockScreen) UnregisterRuneFallback(r rune) {}
|
||||
|
||||
func (m *MockScreen) CanDisplay(r rune, checkFallbacks bool) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *MockScreen) Resize(x, y, h, w int) {
|
||||
m.x, m.y, m.h, m.w = x, y, h, w
|
||||
}
|
||||
|
||||
func (m *MockScreen) HasKey(tcell.Key) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *MockScreen) Beep() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockScreen) DumpContents() string {
|
||||
var res string
|
||||
for i := m.y; i < m.h; i++ {
|
||||
str := []rune{}
|
||||
for j := m.x; j < m.w; j++ {
|
||||
r, ok := m.content[coord{x: j, y: i}]
|
||||
if ok {
|
||||
str = append(str, r)
|
||||
} else {
|
||||
str = append(str, ' ')
|
||||
}
|
||||
}
|
||||
res = res + string(str) + "\n"
|
||||
}
|
||||
return res
|
||||
}
|
617
ui/ui.go
Normal file
617
ui/ui.go
Normal file
@ -0,0 +1,617 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.yetaga.in/alazyreader/library/book"
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
type Drawable interface {
|
||||
Draw(tcell.Screen)
|
||||
SetSize(x, y, h, w int)
|
||||
SetStyle(tcell.Style)
|
||||
SetVisible(bool)
|
||||
}
|
||||
|
||||
type Offsets struct {
|
||||
Top int
|
||||
Bottom int
|
||||
Left int
|
||||
Right int
|
||||
Percent int
|
||||
}
|
||||
|
||||
type Contents []struct {
|
||||
Offsets Offsets
|
||||
Container Drawable
|
||||
}
|
||||
|
||||
const (
|
||||
LayoutUnmanaged = iota
|
||||
LayoutHorizontalEven
|
||||
LayoutVerticalEven
|
||||
LayoutHorizontalPercent
|
||||
LayoutVerticalPercent
|
||||
)
|
||||
|
||||
var (
|
||||
StyleActive = tcell.Style(0).Foreground(tcell.ColorWhite).Background(tcell.ColorBlack)
|
||||
StyleInactive = tcell.Style(0).Foreground(tcell.ColorGray).Background(tcell.ColorBlack)
|
||||
)
|
||||
|
||||
// A Container has no visible UI of its own, but arranges sub-components on the screen.
|
||||
// layoutMethod manages how subcomponents are organized. If `LayoutUnmanaged`, it just uses the offsets
|
||||
// in contents to paint on the screen. Otherwise, `LayoutHorizontalEven` and `LayoutVerticalEven` will
|
||||
// have it compute even distributions of space for all components either horizontally or vertically,
|
||||
// filling the container.
|
||||
type Container struct {
|
||||
x, y int
|
||||
h, w int
|
||||
layoutMethod int
|
||||
contents Contents
|
||||
visible bool
|
||||
}
|
||||
|
||||
func NewContainer(contents Contents, layoutMethod int) *Container {
|
||||
return &Container{
|
||||
layoutMethod: layoutMethod,
|
||||
contents: contents,
|
||||
visible: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Container) Draw(s tcell.Screen) {
|
||||
if !c.visible {
|
||||
return
|
||||
}
|
||||
for i := range c.contents {
|
||||
c.contents[i].Container.Draw(s)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Container) SetSize(x, y, h, w int) {
|
||||
c.x, c.y, c.h, c.w = x, y, h, w
|
||||
carry := 0
|
||||
if c.layoutMethod == LayoutVerticalEven {
|
||||
num := len(c.contents)
|
||||
extra := c.h % num
|
||||
for r := range c.contents {
|
||||
w := c.w
|
||||
h := c.h / num
|
||||
x := c.x
|
||||
y := c.y + (h * r) + carry
|
||||
if extra > 0 { // distribute "extra" space to containers as we have some left
|
||||
h++
|
||||
extra--
|
||||
carry++
|
||||
}
|
||||
c.contents[r].Container.SetSize(x, y, h, w)
|
||||
}
|
||||
} else if c.layoutMethod == LayoutHorizontalEven {
|
||||
num := len(c.contents)
|
||||
extra := c.w % num
|
||||
for r := range c.contents {
|
||||
w := c.w / num
|
||||
h := c.h
|
||||
x := c.x + (w * r) + carry
|
||||
y := c.y
|
||||
if extra > 0 { // distribute "extra" space to containers as we have some left
|
||||
w++
|
||||
extra--
|
||||
carry++
|
||||
}
|
||||
c.contents[r].Container.SetSize(x, y, h, w)
|
||||
}
|
||||
} else if c.layoutMethod == LayoutHorizontalPercent {
|
||||
// first, work out overall distribution
|
||||
total := 0
|
||||
for r := range c.contents {
|
||||
// `0` or negatives are set as minimum
|
||||
if c.contents[r].Offsets.Percent < 1 {
|
||||
total += 1
|
||||
} else {
|
||||
total += c.contents[r].Offsets.Percent
|
||||
}
|
||||
}
|
||||
carry := 0
|
||||
// push around containers
|
||||
for r := range c.contents {
|
||||
ratio := (float64(c.contents[r].Offsets.Percent) / float64(total))
|
||||
w := int(float64(c.w) * ratio)
|
||||
h := c.h
|
||||
x := c.x + carry
|
||||
y := c.y
|
||||
carry += w
|
||||
// and add any remaining space to the last container
|
||||
if r == len(c.contents)-1 {
|
||||
w += (c.w - carry)
|
||||
}
|
||||
c.contents[r].Container.SetSize(x, y, h, w)
|
||||
}
|
||||
} else if c.layoutMethod == LayoutVerticalPercent {
|
||||
// first, work out overall distribution
|
||||
total := 0
|
||||
for r := range c.contents {
|
||||
// `0` or negatives are set as minimum
|
||||
if c.contents[r].Offsets.Percent < 1 {
|
||||
total += 1
|
||||
} else {
|
||||
total += c.contents[r].Offsets.Percent
|
||||
}
|
||||
}
|
||||
carry := 0
|
||||
// push around containers
|
||||
for r := range c.contents {
|
||||
ratio := (float64(c.contents[r].Offsets.Percent) / float64(total))
|
||||
w := c.w
|
||||
h := int(float64(c.h) * ratio)
|
||||
x := c.x
|
||||
y := c.y + carry
|
||||
carry += h
|
||||
// and add any remaining space to the last container
|
||||
if r == len(c.contents)-1 {
|
||||
h += (c.h - carry)
|
||||
}
|
||||
c.contents[r].Container.SetSize(x, y, h, w)
|
||||
}
|
||||
} else {
|
||||
for r := range c.contents {
|
||||
x := c.x + c.contents[r].Offsets.Left
|
||||
y := c.y + c.contents[r].Offsets.Top
|
||||
h := c.h - c.contents[r].Offsets.Bottom
|
||||
w := c.w - c.contents[r].Offsets.Right
|
||||
c.contents[r].Container.SetSize(x, y, h, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Container) SetStyle(s tcell.Style) {
|
||||
// containers have no visible elements to style
|
||||
}
|
||||
|
||||
func (c *Container) SetVisible(b bool) {
|
||||
c.visible = b
|
||||
}
|
||||
|
||||
func (c *Container) Contents() Contents {
|
||||
return c.contents
|
||||
}
|
||||
|
||||
func (c *Container) SetContents(con Contents) {
|
||||
c.contents = con
|
||||
}
|
||||
|
||||
// A Box draws a ASCII box around its contents, with an optional title and footer.
|
||||
type Box struct {
|
||||
x, y int
|
||||
h, w int
|
||||
title Drawable
|
||||
menuItems Drawable
|
||||
contents Contents
|
||||
style tcell.Style
|
||||
cascade bool
|
||||
visible bool
|
||||
transparent bool
|
||||
}
|
||||
|
||||
func NewBox(title string, menuItems []string, contents Contents, initialStyle tcell.Style, cascade bool) *Box {
|
||||
return &Box{
|
||||
title: NewPaddedText(title),
|
||||
menuItems: NewPaddedText(strings.Join(menuItems, " ")),
|
||||
contents: contents,
|
||||
style: initialStyle,
|
||||
cascade: cascade,
|
||||
visible: true,
|
||||
transparent: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Box) SetSize(x, y, h, w int) {
|
||||
b.x, b.y, b.h, b.w = x, y, h, w
|
||||
b.title.SetSize(b.x+2, b.y, 0, 0)
|
||||
b.menuItems.SetSize(b.x+2, b.y+b.h-1, 0, 0)
|
||||
for c := range b.contents {
|
||||
x := b.x + b.contents[c].Offsets.Left
|
||||
y := b.y + b.contents[c].Offsets.Top
|
||||
h := b.h - b.contents[c].Offsets.Bottom
|
||||
w := b.w - b.contents[c].Offsets.Right
|
||||
b.contents[c].Container.SetSize(x, y, h, w)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Box) Draw(s tcell.Screen) {
|
||||
if !b.visible {
|
||||
return
|
||||
}
|
||||
// blank out inner area
|
||||
if !b.transparent {
|
||||
for m := b.x + 1; m < b.x+b.w-1; m++ {
|
||||
for n := b.y + 1; n < b.y+b.h-1; n++ {
|
||||
s.SetContent(m, n, ' ', nil, b.style)
|
||||
}
|
||||
}
|
||||
}
|
||||
// draw outside bars
|
||||
for m := b.x + 1; m < b.x+b.w-1; m++ {
|
||||
s.SetContent(m, b.y, tcell.RuneHLine, nil, b.style)
|
||||
s.SetContent(m, b.y+b.h-1, tcell.RuneHLine, nil, b.style)
|
||||
}
|
||||
for m := b.y + 1; m < b.y+b.h-1; m++ {
|
||||
s.SetContent(b.x, m, tcell.RuneVLine, nil, b.style)
|
||||
s.SetContent(b.x+b.w-1, m, tcell.RuneVLine, nil, b.style)
|
||||
}
|
||||
s.SetContent(b.x, b.y, tcell.RuneULCorner, nil, b.style)
|
||||
s.SetContent(b.x+b.w-1, b.y, tcell.RuneURCorner, nil, b.style)
|
||||
s.SetContent(b.x, b.y+b.h-1, tcell.RuneLLCorner, nil, b.style)
|
||||
s.SetContent(b.x+b.w-1, b.y+b.h-1, tcell.RuneLRCorner, nil, b.style)
|
||||
|
||||
if b.title != nil {
|
||||
b.title.Draw(s)
|
||||
}
|
||||
if b.menuItems != nil {
|
||||
b.menuItems.Draw(s)
|
||||
}
|
||||
for c := range b.contents {
|
||||
b.contents[c].Container.Draw(s)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Box) SetStyle(s tcell.Style) {
|
||||
b.style = s
|
||||
b.title.SetStyle(s)
|
||||
b.menuItems.SetStyle(s)
|
||||
if b.cascade {
|
||||
for c := range b.contents {
|
||||
b.contents[c].Container.SetStyle(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Box) SetVisible(v bool) {
|
||||
b.visible = v
|
||||
}
|
||||
|
||||
func (b *Box) SetTransparent(v bool) {
|
||||
b.transparent = v
|
||||
}
|
||||
|
||||
func (b *Box) Contents() Contents {
|
||||
return b.contents
|
||||
}
|
||||
|
||||
func (b *Box) SetContents(c Contents) {
|
||||
b.contents = c
|
||||
}
|
||||
|
||||
// A List is a scrollable, pageable list with a selector token.
|
||||
type List struct {
|
||||
x, y int
|
||||
h, w int
|
||||
selected int
|
||||
listItems []ListKeyValue
|
||||
style tcell.Style
|
||||
visible bool
|
||||
}
|
||||
|
||||
type ListKeyValue struct {
|
||||
Key int
|
||||
Value string
|
||||
}
|
||||
|
||||
func NewList(listItems []ListKeyValue, initialSelected int) *List {
|
||||
return &List{
|
||||
listItems: listItems,
|
||||
selected: initialSelected,
|
||||
visible: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *List) SetSize(x, y, h, w int) {
|
||||
l.x, l.y, l.h, l.w = x, y, h, w
|
||||
}
|
||||
|
||||
func (l *List) Draw(s tcell.Screen) {
|
||||
if !l.visible {
|
||||
return
|
||||
}
|
||||
for i := range l.listItems {
|
||||
for j, r := range l.listItems[i].Value {
|
||||
s.SetContent(l.x+j, l.y+i, r, nil, l.style)
|
||||
}
|
||||
if i == l.selected {
|
||||
s.SetContent(l.x+len(l.listItems[i].Value)+1, l.y+i, '<', nil, l.style)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *List) SetVisible(b bool) {
|
||||
l.visible = b
|
||||
}
|
||||
|
||||
func (l *List) SetStyle(s tcell.Style) {
|
||||
l.style = s
|
||||
}
|
||||
|
||||
func (l *List) Selected() int {
|
||||
return l.selected
|
||||
}
|
||||
|
||||
func (l *List) SelectedID() int {
|
||||
if l.listItems == nil || len(l.listItems) == 0 {
|
||||
return 0
|
||||
}
|
||||
return l.listItems[l.selected].Key
|
||||
}
|
||||
|
||||
func (l *List) SetSelected(i int) {
|
||||
l.selected = i
|
||||
}
|
||||
|
||||
func (l *List) ListMembers() []ListKeyValue {
|
||||
return l.listItems
|
||||
}
|
||||
|
||||
func (l *List) SetMembers(lkv []ListKeyValue) {
|
||||
l.listItems = lkv
|
||||
}
|
||||
|
||||
// BookDetails displays an editable list of book details
|
||||
type BookDetails struct {
|
||||
x, y int
|
||||
h, w int
|
||||
book *book.Book
|
||||
style tcell.Style
|
||||
visible bool
|
||||
}
|
||||
|
||||
func NewBookDetails(b *book.Book) *BookDetails {
|
||||
return &BookDetails{
|
||||
book: b,
|
||||
visible: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *BookDetails) SetBook(b *book.Book) {
|
||||
l.book = b
|
||||
}
|
||||
|
||||
func (l *BookDetails) SetSize(x, y, h, w int) {
|
||||
l.x, l.y, l.h, l.w = x, y, h, w
|
||||
}
|
||||
|
||||
func (l *BookDetails) Draw(s tcell.Screen) {
|
||||
if l.book == nil {
|
||||
return
|
||||
}
|
||||
if !l.visible {
|
||||
return
|
||||
}
|
||||
items := []struct {
|
||||
label string
|
||||
value string
|
||||
}{
|
||||
{"Title", l.book.Title},
|
||||
{"Authors", strings.Join(l.book.Authors, ", ")},
|
||||
{"Sort Author", l.book.SortAuthor},
|
||||
{"ISBN-10", l.book.ISBN10},
|
||||
{"ISBN-13", l.book.ISBN13},
|
||||
{"Format", l.book.Format},
|
||||
{"Genre", l.book.Genre},
|
||||
{"Publisher", l.book.Publisher},
|
||||
{"Series", l.book.Series},
|
||||
{"Volume", l.book.Volume},
|
||||
{"Year", l.book.Year},
|
||||
{"Signed", strconv.FormatBool(l.book.Signed)},
|
||||
{"On Loan", l.book.OnLoan},
|
||||
{"Cover URL", l.book.CoverURL},
|
||||
{"Notes", l.book.Notes},
|
||||
{"Description", l.book.Description},
|
||||
}
|
||||
for i := range items {
|
||||
if i < l.h-2 {
|
||||
kv := NewKeyValue(items[i].label, ": ", items[i].value)
|
||||
kv.SetSize(l.x, l.y+i, 0, 0)
|
||||
kv.SetStyle(l.style)
|
||||
kv.Draw(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *BookDetails) SetVisible(b bool) {
|
||||
l.visible = b
|
||||
}
|
||||
|
||||
func (l *BookDetails) SetStyle(s tcell.Style) {
|
||||
l.style = s
|
||||
}
|
||||
|
||||
// PaddedText outputs strings with a space on both sides.
|
||||
// Useful for generating headings, footers, etc. Used by Box.
|
||||
type PaddedText struct {
|
||||
x, y int
|
||||
h, w int
|
||||
text string
|
||||
style tcell.Style
|
||||
visible bool
|
||||
}
|
||||
|
||||
func NewPaddedText(text string) *PaddedText {
|
||||
return &PaddedText{text: text, visible: true}
|
||||
}
|
||||
|
||||
func (p *PaddedText) SetSize(x, y, _, _ int) {
|
||||
p.x, p.y, p.h, p.w = x, y, 1, len(p.text)+2
|
||||
}
|
||||
|
||||
func (p *PaddedText) SetStyle(s tcell.Style) {
|
||||
p.style = s
|
||||
}
|
||||
|
||||
func (p *PaddedText) Draw(s tcell.Screen) {
|
||||
if p.text == "" {
|
||||
return
|
||||
}
|
||||
if !p.visible {
|
||||
return
|
||||
}
|
||||
t := p.x
|
||||
s.SetContent(t, p.y, ' ', nil, p.style)
|
||||
t++
|
||||
for _, r := range p.text {
|
||||
s.SetContent(t, p.y, r, nil, p.style)
|
||||
t++
|
||||
}
|
||||
s.SetContent(t, p.y, ' ', nil, p.style)
|
||||
}
|
||||
|
||||
func (p *PaddedText) SetVisible(b bool) {
|
||||
p.visible = b
|
||||
}
|
||||
|
||||
type KeyValue struct {
|
||||
x, y int
|
||||
h, w int
|
||||
key string
|
||||
value string
|
||||
separator string
|
||||
style tcell.Style
|
||||
visible bool
|
||||
}
|
||||
|
||||
func NewKeyValue(key, separator, value string) *KeyValue {
|
||||
return &KeyValue{
|
||||
key: key,
|
||||
separator: separator,
|
||||
value: value,
|
||||
visible: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *KeyValue) SetSize(x, y, _, _ int) {
|
||||
p.x, p.y, p.h, p.w = x, y, 1, len(p.key)+len(p.separator)+len(p.value)
|
||||
}
|
||||
|
||||
func (p *KeyValue) SetStyle(s tcell.Style) {
|
||||
p.style = s
|
||||
}
|
||||
|
||||
func (p *KeyValue) Draw(s tcell.Screen) {
|
||||
if !p.visible {
|
||||
return
|
||||
}
|
||||
for j, r := range p.key {
|
||||
s.SetContent(p.x+j, p.y, r, nil, p.style)
|
||||
}
|
||||
for j, r := range p.separator {
|
||||
s.SetContent(p.x+len(p.key)+j, p.y, r, nil, p.style)
|
||||
}
|
||||
for j, r := range p.value {
|
||||
s.SetContent(p.x+len(p.key)+len(p.separator)+j, p.y, r, nil, p.style)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *KeyValue) SetVisible(b bool) {
|
||||
p.visible = b
|
||||
}
|
||||
|
||||
func (p *KeyValue) GetValue() string {
|
||||
return p.value
|
||||
}
|
||||
|
||||
type EditableTextLine struct {
|
||||
x, y int
|
||||
h, w int
|
||||
text string
|
||||
style tcell.Style
|
||||
visible bool
|
||||
cursorPos int
|
||||
showCursor bool
|
||||
}
|
||||
|
||||
func NewEditableTextLine(initialText string) *EditableTextLine {
|
||||
return &EditableTextLine{
|
||||
text: initialText,
|
||||
visible: true,
|
||||
showCursor: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *EditableTextLine) SetSize(x, y, _, _ int) {
|
||||
p.x, p.y, p.h, p.w = x, y, 1, len(p.text)
|
||||
}
|
||||
|
||||
func (p *EditableTextLine) SetStyle(s tcell.Style) {
|
||||
p.style = s
|
||||
}
|
||||
|
||||
func (p *EditableTextLine) Draw(s tcell.Screen) {
|
||||
if !p.visible {
|
||||
return
|
||||
}
|
||||
for j, r := range p.text {
|
||||
s.SetContent(p.x+j, p.y, r, nil, p.style)
|
||||
}
|
||||
s.ShowCursor(p.x+p.cursorPos, p.y)
|
||||
}
|
||||
|
||||
func (p *EditableTextLine) SetVisible(b bool) {
|
||||
p.visible = b
|
||||
}
|
||||
|
||||
func (p *EditableTextLine) SetCursorVisible(b bool) {
|
||||
p.showCursor = b
|
||||
}
|
||||
|
||||
func (p *EditableTextLine) SetText(t string) {
|
||||
p.text = t
|
||||
if len(p.text) == 0 {
|
||||
p.ResetCursor(true)
|
||||
return
|
||||
}
|
||||
p.ResetCursor(false)
|
||||
}
|
||||
|
||||
func (p *EditableTextLine) Text() string {
|
||||
return p.text
|
||||
}
|
||||
|
||||
func (p *EditableTextLine) ResetCursor(beginning bool) {
|
||||
if beginning {
|
||||
p.cursorPos = 0
|
||||
} else {
|
||||
p.cursorPos = len(p.text)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *EditableTextLine) InsertAtCursor(r rune) {
|
||||
if len(p.text) == 0 {
|
||||
p.text = string(r)
|
||||
p.cursorPos = 1
|
||||
return
|
||||
}
|
||||
p.text = p.text[0:p.cursorPos] + string(r) + p.text[p.cursorPos:len(p.text)]
|
||||
p.cursorPos = p.cursorPos + 1
|
||||
}
|
||||
|
||||
func (p *EditableTextLine) MoveCursor(i int) {
|
||||
if p.cursorPos+i < 0 {
|
||||
p.cursorPos = 0
|
||||
return
|
||||
}
|
||||
if p.cursorPos+i > len(p.text) {
|
||||
p.cursorPos = len(p.text)
|
||||
return
|
||||
}
|
||||
p.cursorPos = p.cursorPos + i
|
||||
}
|
||||
|
||||
func (p *EditableTextLine) DeleteAtCursor() {
|
||||
if len(p.text) == 0 {
|
||||
p.cursorPos = 0
|
||||
return
|
||||
}
|
||||
p.text = p.text[0:p.cursorPos-1] + p.text[p.cursorPos:len(p.text)]
|
||||
p.cursorPos = p.cursorPos - 1
|
||||
}
|
239
ui/ui_test.go
Normal file
239
ui/ui_test.go
Normal file
@ -0,0 +1,239 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestContainerOneBox(t *testing.T) {
|
||||
expect := `┌─ box one ────────┐
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────┘
|
||||
`
|
||||
m := &MockScreen{}
|
||||
one := NewBox("box one", nil, Contents{}, 0, false)
|
||||
container := NewContainer(
|
||||
Contents{{Container: one}},
|
||||
LayoutHorizontalEven,
|
||||
)
|
||||
m.Init()
|
||||
m.Resize(0, 0, 5, 20)
|
||||
container.SetSize(0, 0, 5, 20)
|
||||
container.Draw(m)
|
||||
result := m.DumpContents()
|
||||
if result != expect {
|
||||
fmt.Printf("expected:\n%+v", expect)
|
||||
fmt.Printf("actual:\n%+v", result)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainerTwoBoxesHStack(t *testing.T) {
|
||||
expect := `┌─ one ──┐┌─ two ──┐
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
└────────┘└────────┘
|
||||
`
|
||||
m := &MockScreen{}
|
||||
one := NewBox("one", nil, Contents{}, 0, false)
|
||||
two := NewBox("two", nil, Contents{}, 0, false)
|
||||
container := NewContainer(
|
||||
Contents{{Container: one}, {Container: two}},
|
||||
LayoutHorizontalEven,
|
||||
)
|
||||
m.Init()
|
||||
m.Resize(0, 0, 5, 20)
|
||||
container.SetSize(0, 0, 5, 20)
|
||||
container.Draw(m)
|
||||
result := m.DumpContents()
|
||||
if result != expect {
|
||||
fmt.Printf("expected:\n%+v", expect)
|
||||
fmt.Printf("actual:\n%+v", result)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainerThreeBoxesUnevenHStack(t *testing.T) {
|
||||
expect := `┌─ one ──┐┌─ two ──┐┌─ three
|
||||
│ ││ ││ │
|
||||
│ ││ ││ │
|
||||
│ ││ ││ │
|
||||
└────────┘└────────┘└───────┘
|
||||
`
|
||||
m := &MockScreen{}
|
||||
one := NewBox("one", nil, Contents{}, 0, false)
|
||||
two := NewBox("two", nil, Contents{}, 0, false)
|
||||
three := NewBox("three", nil, Contents{}, 0, false)
|
||||
container := NewContainer(
|
||||
Contents{{Container: one}, {Container: two}, {Container: three}},
|
||||
LayoutHorizontalEven,
|
||||
)
|
||||
m.Init()
|
||||
m.Resize(0, 0, 5, 29)
|
||||
container.SetSize(0, 0, 5, 29)
|
||||
container.Draw(m)
|
||||
result := m.DumpContents()
|
||||
if result != expect {
|
||||
fmt.Printf("expected:\n%+v", expect)
|
||||
fmt.Printf("actual:\n%+v", result)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainerTwoBoxesHPercentStack(t *testing.T) {
|
||||
expect := `┌─ one ──────┐┌─ two ┐
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
└────────────┘└──────┘
|
||||
`
|
||||
m := &MockScreen{}
|
||||
one := NewBox("one", nil, Contents{}, 0, false)
|
||||
two := NewBox("two", nil, Contents{}, 0, false)
|
||||
container := NewContainer(
|
||||
Contents{
|
||||
{Container: one, Offsets: Offsets{Percent: 2}},
|
||||
{Container: two, Offsets: Offsets{Percent: 1}}},
|
||||
LayoutHorizontalPercent,
|
||||
)
|
||||
m.Init()
|
||||
m.Resize(0, 0, 5, 22)
|
||||
container.SetSize(0, 0, 5, 22)
|
||||
container.Draw(m)
|
||||
result := m.DumpContents()
|
||||
if result != expect {
|
||||
fmt.Printf("expected:\n%+v", expect)
|
||||
fmt.Printf("actual:\n%+v", result)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainerTwoBoxesVStack(t *testing.T) {
|
||||
expect := `┌─ one ──┐
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└────────┘
|
||||
┌─ two ──┐
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└────────┘
|
||||
`
|
||||
m := &MockScreen{}
|
||||
one := NewBox("one", nil, Contents{}, 0, false)
|
||||
two := NewBox("two", nil, Contents{}, 0, false)
|
||||
container := NewContainer(
|
||||
Contents{{Container: one}, {Container: two}},
|
||||
LayoutVerticalEven,
|
||||
)
|
||||
m.Init()
|
||||
m.Resize(0, 0, 10, 10)
|
||||
container.SetSize(0, 0, 10, 10)
|
||||
container.Draw(m)
|
||||
result := m.DumpContents()
|
||||
if result != expect {
|
||||
fmt.Printf("expected:\n%+v", expect)
|
||||
fmt.Printf("actual:\n%+v", result)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainerTwoBoxesPercentageVStack(t *testing.T) {
|
||||
expect := `┌─ one ──┐
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└────────┘
|
||||
┌─ two ──┐
|
||||
│ │
|
||||
│ │
|
||||
└────────┘
|
||||
`
|
||||
m := &MockScreen{}
|
||||
one := NewBox("one", nil, Contents{}, 0, false)
|
||||
two := NewBox("two", nil, Contents{}, 0, false)
|
||||
container := NewContainer(
|
||||
Contents{
|
||||
{Container: one, Offsets: Offsets{Percent: 2}},
|
||||
{Container: two, Offsets: Offsets{Percent: 1}}},
|
||||
LayoutVerticalPercent,
|
||||
)
|
||||
m.Init()
|
||||
m.Resize(0, 0, 10, 10)
|
||||
container.SetSize(0, 0, 10, 10)
|
||||
container.Draw(m)
|
||||
result := m.DumpContents()
|
||||
if result != expect {
|
||||
fmt.Printf("expected:\n%+v", expect)
|
||||
fmt.Printf("actual:\n%+v", result)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewEditableTextLine(t *testing.T) {
|
||||
e := NewEditableTextLine("")
|
||||
e.InsertAtCursor('a')
|
||||
e.InsertAtCursor('b')
|
||||
e.InsertAtCursor('c')
|
||||
if e.text != "abc" {
|
||||
fmt.Printf("expected: 'abc', actual: '%+v'", e.text)
|
||||
t.Fail()
|
||||
}
|
||||
e.MoveCursor(-1)
|
||||
e.InsertAtCursor('d')
|
||||
if e.text != "abdc" {
|
||||
fmt.Printf("expected: 'abdc', actual: '%+v'", e.text)
|
||||
t.Fail()
|
||||
}
|
||||
e.MoveCursor(-20)
|
||||
e.InsertAtCursor('e')
|
||||
if e.text != "eabdc" {
|
||||
fmt.Printf("expected: 'eabdc', actual: '%+v'", e.text)
|
||||
t.Fail()
|
||||
}
|
||||
e.MoveCursor(20)
|
||||
e.InsertAtCursor('f')
|
||||
if e.text != "eabdcf" {
|
||||
fmt.Printf("expected: 'eabdcf', actual: '%+v'", e.text)
|
||||
t.Fail()
|
||||
}
|
||||
e.MoveCursor(1)
|
||||
e.InsertAtCursor('g')
|
||||
if e.text != "eabdcfg" {
|
||||
fmt.Printf("expected: 'eabdcfg', actual: '%+v'", e.text)
|
||||
t.Fail()
|
||||
}
|
||||
e.DeleteAtCursor()
|
||||
e.DeleteAtCursor()
|
||||
e.MoveCursor(-1)
|
||||
e.DeleteAtCursor()
|
||||
if e.text != "eabc" {
|
||||
fmt.Printf("expected: 'eabc', actual: '%+v'", e.text)
|
||||
t.Fail()
|
||||
}
|
||||
e.ResetCursor(false)
|
||||
e.InsertAtCursor('h')
|
||||
e.ResetCursor(true)
|
||||
e.InsertAtCursor('g')
|
||||
if e.text != "geabch" {
|
||||
fmt.Printf("expected: 'geabch', actual: '%+v'", e.text)
|
||||
t.Fail()
|
||||
}
|
||||
e.SetText("the rain in spain")
|
||||
e.InsertAtCursor('s')
|
||||
if e.text != "the rain in spains" {
|
||||
fmt.Printf("expected: 'the rain in spains', actual: '%+v'", e.text)
|
||||
t.Fail()
|
||||
}
|
||||
e.SetText("")
|
||||
e.InsertAtCursor('s')
|
||||
if e.text != "s" {
|
||||
fmt.Printf("expected: 's', actual: '%+v'", e.text)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user