it now displays books from the DB!
This commit is contained in:
parent
3bff06aad7
commit
8c6c9325da
5
Makefile
5
Makefile
@ -11,9 +11,12 @@ endif
|
|||||||
|
|
||||||
build: server manager
|
build: server manager
|
||||||
|
|
||||||
run: build
|
run-server: build
|
||||||
./server
|
./server
|
||||||
|
|
||||||
|
run-manager: build
|
||||||
|
./manager
|
||||||
|
|
||||||
server: $(GOFILES) $(STATICFILES)
|
server: $(GOFILES) $(STATICFILES)
|
||||||
go build -o server ./cmd/serve
|
go build -o server ./cmd/serve
|
||||||
|
|
||||||
|
@ -1,26 +1,113 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"git.yetaga.in/alazyreader/library/book"
|
||||||
"git.yetaga.in/alazyreader/library/config"
|
"git.yetaga.in/alazyreader/library/config"
|
||||||
|
"git.yetaga.in/alazyreader/library/database"
|
||||||
"git.yetaga.in/alazyreader/library/ui"
|
"git.yetaga.in/alazyreader/library/ui"
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
"github.com/kelseyhightower/envconfig"
|
"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 (
|
const (
|
||||||
IN_MENU = iota
|
IN_MENU = iota
|
||||||
IN_BOOK
|
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() {
|
func main() {
|
||||||
var c config.Config
|
var c config.Config
|
||||||
err := envconfig.Process("library", &c)
|
err := envconfig.Process("library", &c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
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()
|
screen, err := tcell.NewScreen()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
@ -30,21 +117,23 @@ func main() {
|
|||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
l := ui.NewList([]string{"foo", "bar", "baz"}, 0)
|
l := ui.NewList(Titles(state.Get("library").([]book.Book)), 0)
|
||||||
menu := ui.NewBox(
|
menu := ui.NewBox(
|
||||||
"library",
|
"library",
|
||||||
[]string{"(q)uit"},
|
[]string{"(n)ew", "(i)mport", "(q)uit"},
|
||||||
ui.Contents{{
|
ui.Contents{{
|
||||||
Offsets: ui.Offsets{Top: 1, Left: 2, Bottom: -2, Right: -2},
|
Offsets: ui.Offsets{Top: 1, Left: 2, Bottom: -2, Right: -2},
|
||||||
Container: l,
|
Container: l,
|
||||||
}},
|
}},
|
||||||
)
|
)
|
||||||
book := ui.NewBookDetails()
|
book := ui.NewBookDetails(&book.Book{
|
||||||
|
Title: "test title",
|
||||||
|
})
|
||||||
activeBook := ui.NewBox(
|
activeBook := ui.NewBox(
|
||||||
"book",
|
"book",
|
||||||
[]string{"test"},
|
nil,
|
||||||
ui.Contents{{
|
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,
|
Container: book,
|
||||||
}},
|
}},
|
||||||
)
|
)
|
||||||
@ -64,16 +153,19 @@ func main() {
|
|||||||
container.Draw(screen)
|
container.Draw(screen)
|
||||||
screen.Sync()
|
screen.Sync()
|
||||||
|
|
||||||
state := IN_MENU
|
// init UI state
|
||||||
|
state.Set("ui_state", IN_MENU)
|
||||||
|
|
||||||
// UI loop
|
// UI loop
|
||||||
for {
|
for {
|
||||||
e := screen.PollEvent()
|
e := screen.PollEvent()
|
||||||
switch v := e.(type) {
|
switch v := e.(type) {
|
||||||
case *tcell.EventError:
|
case *tcell.EventError:
|
||||||
|
fmt.Fprintf(os.Stderr, "%v", v)
|
||||||
screen.Beep()
|
screen.Beep()
|
||||||
case *tcell.EventKey: // input handling
|
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 {
|
if v.Key() == tcell.KeyUp && l.Selected() > 0 {
|
||||||
l.SetSelected(l.Selected() - 1)
|
l.SetSelected(l.Selected() - 1)
|
||||||
}
|
}
|
||||||
@ -86,11 +178,12 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if v.Key() == tcell.KeyRight {
|
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 {
|
if v.Key() == tcell.KeyLeft {
|
||||||
state = IN_MENU
|
state.Set("ui_state", IN_MENU)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
screen.Clear()
|
screen.Clear()
|
||||||
@ -100,6 +193,12 @@ func main() {
|
|||||||
container.SetSize(0, 0, h, w)
|
container.SetSize(0, 0, h, w)
|
||||||
screen.Clear()
|
screen.Clear()
|
||||||
container.Draw(screen)
|
container.Draw(screen)
|
||||||
|
case *EventBookUpdate:
|
||||||
|
// TK
|
||||||
|
case *EventLoadBook:
|
||||||
|
book.SetBook(GetBookByID(v.ID, books))
|
||||||
|
screen.Clear()
|
||||||
|
container.Draw(screen)
|
||||||
case *tcell.EventInterrupt:
|
case *tcell.EventInterrupt:
|
||||||
case *tcell.EventMouse:
|
case *tcell.EventMouse:
|
||||||
case *tcell.EventTime:
|
case *tcell.EventTime:
|
||||||
@ -108,3 +207,23 @@ func main() {
|
|||||||
screen.Show() // repaint
|
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{}
|
||||||
|
}
|
||||||
|
105
ui/ui.go
105
ui/ui.go
@ -1,6 +1,7 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.yetaga.in/alazyreader/library/book"
|
"git.yetaga.in/alazyreader/library/book"
|
||||||
@ -230,10 +231,15 @@ type List struct {
|
|||||||
x, y int
|
x, y int
|
||||||
h, w int
|
h, w int
|
||||||
selected 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{
|
return &List{
|
||||||
listItems: listItems,
|
listItems: listItems,
|
||||||
selected: initialSelected,
|
selected: initialSelected,
|
||||||
@ -246,11 +252,11 @@ func (l *List) SetSize(x, y, h, w int) {
|
|||||||
|
|
||||||
func (l *List) Draw(s tcell.Screen) {
|
func (l *List) Draw(s tcell.Screen) {
|
||||||
for i := range l.listItems {
|
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)
|
s.SetContent(l.x+j, l.y+i, r, nil, tcell.StyleDefault)
|
||||||
}
|
}
|
||||||
if i == l.selected {
|
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
|
return l.selected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *List) SelectedID() int {
|
||||||
|
return l.listItems[l.selected].Key
|
||||||
|
}
|
||||||
|
|
||||||
func (l *List) SetSelected(i int) {
|
func (l *List) SetSelected(i int) {
|
||||||
l.selected = i
|
l.selected = i
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *List) ListMembers() []string {
|
func (l *List) ListMembers() []ListKeyValue {
|
||||||
return l.listItems
|
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 {
|
type BookDetails struct {
|
||||||
x, y int
|
x, y int
|
||||||
h, w int
|
h, w int
|
||||||
selected int
|
book *book.Book
|
||||||
book book.Book
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBookDetails() *BookDetails {
|
func NewBookDetails(b *book.Book) *BookDetails {
|
||||||
return &BookDetails{}
|
return &BookDetails{
|
||||||
|
book: b,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *BookDetails) SetBook(b *book.Book) {
|
||||||
|
l.book = b
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *BookDetails) SetSize(x, y, h, w int) {
|
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) {
|
func (l *BookDetails) Draw(s tcell.Screen) {
|
||||||
items := []string{"title", "authors", "isbn-10", "isbn-13"}
|
if l.book == nil {
|
||||||
for i := range items {
|
return
|
||||||
for j, r := range items[i] {
|
}
|
||||||
s.SetContent(l.x+j, l.y+i, r, nil, tcell.StyleDefault)
|
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.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)
|
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
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user