diff --git a/Makefile b/Makefile index 3e3b266..3529dfc 100644 --- a/Makefile +++ b/Makefile @@ -11,9 +11,12 @@ endif build: server manager -run: build +run-server: build ./server +run-manager: build + ./manager + server: $(GOFILES) $(STATICFILES) go build -o server ./cmd/serve diff --git a/cmd/manage/main.go b/cmd/manage/main.go index 73c1363..49f4dd8 100644 --- a/cmd/manage/main.go +++ b/cmd/manage/main.go @@ -1,26 +1,113 @@ package main import ( + "context" "fmt" "log" + "os" + "sync" + "git.yetaga.in/alazyreader/library/book" "git.yetaga.in/alazyreader/library/config" + "git.yetaga.in/alazyreader/library/database" "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 +} + const ( IN_MENU = iota IN_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 +} + +type EventLoadBook struct { + tcell.EventTime + ID int +} + +func NewEventLoadBook(id int) *EventLoadBook { + e := &EventLoadBook{ID: id} + e.SetEventNow() + return e +} + 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 == "" { + 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) @@ -30,21 +117,23 @@ func main() { log.Fatalln(err) } - l := ui.NewList([]string{"foo", "bar", "baz"}, 0) + l := ui.NewList(Titles(state.Get("library").([]book.Book)), 0) menu := ui.NewBox( "library", - []string{"(q)uit"}, + []string{"(n)ew", "(i)mport", "(q)uit"}, ui.Contents{{ Offsets: ui.Offsets{Top: 1, Left: 2, Bottom: -2, Right: -2}, Container: l, }}, ) - book := ui.NewBookDetails() + book := ui.NewBookDetails(&book.Book{ + Title: "test title", + }) activeBook := ui.NewBox( "book", - []string{"test"}, + nil, ui.Contents{{ - Offsets: ui.Offsets{Top: 1, Left: 2, Bottom: -2, Right: -2}, + Offsets: ui.Offsets{Top: 1, Left: 2, Bottom: 0, Right: 0}, Container: book, }}, ) @@ -64,16 +153,19 @@ func main() { container.Draw(screen) screen.Sync() - state := IN_MENU + // init UI state + state.Set("ui_state", IN_MENU) // 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 - if state == IN_MENU { + curr := state.Get("ui_state").(int) + if curr == IN_MENU { if v.Key() == tcell.KeyUp && l.Selected() > 0 { l.SetSelected(l.Selected() - 1) } @@ -86,11 +178,12 @@ func main() { return } if v.Key() == tcell.KeyRight { - state = IN_BOOK + screen.PostEvent(NewEventLoadBook(l.SelectedID())) + state.Set("ui_state", IN_BOOK) } - } else if state == IN_BOOK { + } else if curr == IN_BOOK { if v.Key() == tcell.KeyLeft { - state = IN_MENU + state.Set("ui_state", IN_MENU) } } screen.Clear() @@ -100,6 +193,12 @@ func main() { container.SetSize(0, 0, h, w) screen.Clear() container.Draw(screen) + case *EventBookUpdate: + // TK + case *EventLoadBook: + book.SetBook(GetBookByID(v.ID, books)) + screen.Clear() + container.Draw(screen) case *tcell.EventInterrupt: case *tcell.EventMouse: case *tcell.EventTime: @@ -108,3 +207,23 @@ func main() { screen.Show() // repaint } } + +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{} +} diff --git a/ui/ui.go b/ui/ui.go index 59973d0..a3f87d6 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -1,6 +1,7 @@ package ui import ( + "strconv" "strings" "git.yetaga.in/alazyreader/library/book" @@ -230,10 +231,15 @@ type List struct { x, y int h, w int selected int - listItems []string + listItems []ListKeyValue } -func NewList(listItems []string, initialSelected int) *List { +type ListKeyValue struct { + Key int + Value string +} + +func NewList(listItems []ListKeyValue, initialSelected int) *List { return &List{ listItems: listItems, selected: initialSelected, @@ -246,11 +252,11 @@ func (l *List) SetSize(x, y, h, w int) { func (l *List) Draw(s tcell.Screen) { for i := range l.listItems { - for j, r := range l.listItems[i] { + for j, r := range l.listItems[i].Value { s.SetContent(l.x+j, l.y+i, r, nil, tcell.StyleDefault) } if i == l.selected { - s.SetContent(l.x+len(l.listItems[i])+1, l.y+i, '<', nil, tcell.StyleDefault) + s.SetContent(l.x+len(l.listItems[i].Value)+1, l.y+i, '<', nil, tcell.StyleDefault) } } } @@ -259,24 +265,33 @@ func (l *List) Selected() int { return l.selected } +func (l *List) SelectedID() int { + return l.listItems[l.selected].Key +} + func (l *List) SetSelected(i int) { l.selected = i } -func (l *List) ListMembers() []string { +func (l *List) ListMembers() []ListKeyValue { return l.listItems } -// A List is a scrollable, pageable list with a selector token. +// BookDetails displays an editable list of book details type BookDetails struct { - x, y int - h, w int - selected int - book book.Book + x, y int + h, w int + book *book.Book } -func NewBookDetails() *BookDetails { - return &BookDetails{} +func NewBookDetails(b *book.Book) *BookDetails { + return &BookDetails{ + book: b, + } +} + +func (l *BookDetails) SetBook(b *book.Book) { + l.book = b } func (l *BookDetails) SetSize(x, y, h, w int) { @@ -284,12 +299,36 @@ func (l *BookDetails) SetSize(x, y, h, w int) { } func (l *BookDetails) Draw(s tcell.Screen) { - items := []string{"title", "authors", "isbn-10", "isbn-13"} + if l.book == nil { + 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 { - for j, r := range items[i] { - s.SetContent(l.x+j, l.y+i, r, nil, tcell.StyleDefault) + if i < l.h-2 { + kv := NewKeyValue(items[i].label, ": ", items[i].value) + kv.SetSize(l.x, l.y+i, 0, 0) + kv.Draw(s) } - s.SetContent(l.x+len(items[i]), l.y+i, ':', nil, tcell.StyleDefault) } } @@ -322,3 +361,39 @@ func (p *PaddedText) Draw(s tcell.Screen) { } s.SetContent(t, p.y, ' ', nil, tcell.StyleDefault) } + +type KeyValue struct { + x, y int + h, w int + key string + value string + separator string +} + +func NewKeyValue(key, separator, value string) *KeyValue { + return &KeyValue{ + key: key, + separator: separator, + value: value, + } +} + +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) Draw(s tcell.Screen) { + for j, r := range p.key { + s.SetContent(p.x+j, p.y, r, nil, tcell.StyleDefault) + } + for j, r := range p.separator { + s.SetContent(p.x+len(p.key)+j, p.y, r, nil, tcell.StyleDefault) + } + for j, r := range p.value { + s.SetContent(p.x+len(p.key)+len(p.separator)+j, p.y, r, nil, tcell.StyleDefault) + } +} + +func (p *KeyValue) GetValue() string { + return p.value +}