From 39fd64ace7b0c41b915667fbaea412678960dec5 Mon Sep 17 00:00:00 2001 From: David Ashby Date: Sun, 1 Aug 2021 18:50:53 -0400 Subject: [PATCH] basic importing works --- cmd/manage/events.go | 24 ++++++++++++++++ cmd/manage/main.go | 55 +++++++++++++++++++++++++++++++++---- database/mysql.go | 2 +- importer/csv.go | 65 ++++++++++++++++++++++++++++++++++++++++++++ ui/ui.go | 4 +++ 5 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 importer/csv.go diff --git a/cmd/manage/events.go b/cmd/manage/events.go index f8eea27..3b2468a 100644 --- a/cmd/manage/events.go +++ b/cmd/manage/events.go @@ -5,6 +5,18 @@ import ( "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 @@ -66,6 +78,18 @@ func NewEventOpenImport() *EventOpenImport { 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 diff --git a/cmd/manage/main.go b/cmd/manage/main.go index 3606b52..db95152 100644 --- a/cmd/manage/main.go +++ b/cmd/manage/main.go @@ -11,6 +11,7 @@ import ( "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" @@ -119,9 +120,7 @@ func main() { ui.StyleActive, false, ) - book := ui.NewBookDetails(&book.Book{ - Title: "test title", - }) + activeBookDetails := ui.NewBookDetails(&book.Book{}) // book display (right column) activeBook := ui.NewBox( @@ -129,7 +128,7 @@ func main() { []string{"˄˅ select", "⏎ edit", "(esc) close"}, ui.Contents{{ Offsets: ui.Offsets{Top: 1, Left: 2, Bottom: 0, Right: 0}, - Container: book, + Container: activeBookDetails, }}, ui.StyleInactive, false, @@ -160,6 +159,19 @@ func main() { ) 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() @@ -215,6 +227,8 @@ func main() { } 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()) } @@ -233,16 +247,45 @@ func main() { activeBook.SetStyle(ui.StyleInactive) menu.SetStyle(ui.StyleActive) case *EventLoadBook: - book.SetBook(GetBookByID(v.ID, books)) + 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") @@ -253,8 +296,10 @@ func main() { default: } // repaint + l.SetMembers(Titles(state.Get("library").([]book.Book))) container.Draw(screen) popup.Draw(screen) + errorPopup.Draw(screen) screen.Show() } } diff --git a/database/mysql.go b/database/mysql.go index e222dc0..bd0d1e0 100644 --- a/database/mysql.go +++ b/database/mysql.go @@ -201,7 +201,7 @@ func (m *MySQL) AddBook(ctx context.Context, b *book.Book) error { 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, diff --git a/importer/csv.go b/importer/csv.go new file mode 100644 index 0000000..e458577 --- /dev/null +++ b/importer/csv.go @@ -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 +} diff --git a/ui/ui.go b/ui/ui.go index f4f05e5..5595ae9 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -353,6 +353,10 @@ 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