A simple website to serve a list of books currently in my possession. https://library.yetaga.in/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

253 lines
5.2 KiB

package main
import (
"context"
"fmt"
"log"
"os"
"runtime/debug"
"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)
}
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
}
}()
l := ui.NewList(Titles(state.Get("library").([]book.Book)), 0)
menu := ui.NewBox(
"library",
[]string{"(n)ew", "(i)mport", "(q)uit"},
ui.Contents{{
Offsets: ui.Offsets{Top: 1, Left: 2, Bottom: -2, Right: -2},
Container: l,
}},
ui.StyleActive,
false,
)
book := ui.NewBookDetails(&book.Book{
Title: "test title",
})
activeBook := ui.NewBox(
"book",
nil,
ui.Contents{{
Offsets: ui.Offsets{Top: 1, Left: 2, Bottom: 0, Right: 0},
Container: book,
}},
ui.StyleInactive,
false,
)
container := ui.NewContainer(
ui.Contents{
{Container: menu, Offsets: ui.Offsets{Percent: 1}},
{Container: activeBook, Offsets: ui.Offsets{Percent: 2}},
},
ui.LayoutHorizontalPercent,
)
// 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.Rune() == 'q' {
screen.Fini()
fmt.Printf("Thank you for playing Wing Commander!\n\n")
return
}
if v.Key() == tcell.KeyEnter {
activeBook.SetStyle(ui.StyleActive)
menu.SetStyle(ui.StyleInactive)
state.Set("ui_state", IN_BOOK)
}
} else if curr == IN_BOOK {
if v.Key() == tcell.KeyEsc {
activeBook.SetStyle(ui.StyleInactive)
menu.SetStyle(ui.StyleActive)
state.Set("ui_state", IN_MENU)
}
}
screen.Clear()
container.Draw(screen)
case *tcell.EventResize: // screen redraw
w, h := screen.Size()
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:
default:
}
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{}
}