basic importing works
This commit is contained in:
parent
ebbdb99f37
commit
39fd64ace7
@ -5,6 +5,18 @@ import (
|
|||||||
"github.com/gdamore/tcell"
|
"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
|
// save change to book
|
||||||
type EventBookUpdate struct {
|
type EventBookUpdate struct {
|
||||||
tcell.EventTime
|
tcell.EventTime
|
||||||
@ -66,6 +78,18 @@ func NewEventOpenImport() *EventOpenImport {
|
|||||||
return e
|
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
|
// close import window
|
||||||
type EventCloseImport struct {
|
type EventCloseImport struct {
|
||||||
tcell.EventTime
|
tcell.EventTime
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"git.yetaga.in/alazyreader/library/book"
|
"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/database"
|
||||||
|
"git.yetaga.in/alazyreader/library/importer"
|
||||||
"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"
|
||||||
@ -119,9 +120,7 @@ func main() {
|
|||||||
ui.StyleActive,
|
ui.StyleActive,
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
book := ui.NewBookDetails(&book.Book{
|
activeBookDetails := ui.NewBookDetails(&book.Book{})
|
||||||
Title: "test title",
|
|
||||||
})
|
|
||||||
|
|
||||||
// book display (right column)
|
// book display (right column)
|
||||||
activeBook := ui.NewBox(
|
activeBook := ui.NewBox(
|
||||||
@ -129,7 +128,7 @@ func main() {
|
|||||||
[]string{"˄˅ select", "⏎ edit", "(esc) close"},
|
[]string{"˄˅ select", "⏎ edit", "(esc) close"},
|
||||||
ui.Contents{{
|
ui.Contents{{
|
||||||
Offsets: ui.Offsets{Top: 1, Left: 2, Bottom: 0, Right: 0},
|
Offsets: ui.Offsets{Top: 1, Left: 2, Bottom: 0, Right: 0},
|
||||||
Container: book,
|
Container: activeBookDetails,
|
||||||
}},
|
}},
|
||||||
ui.StyleInactive,
|
ui.StyleInactive,
|
||||||
false,
|
false,
|
||||||
@ -160,6 +159,19 @@ func main() {
|
|||||||
)
|
)
|
||||||
popup.SetVisible(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
|
// init
|
||||||
screen.Clear()
|
screen.Clear()
|
||||||
w, h := screen.Size()
|
w, h := screen.Size()
|
||||||
@ -215,6 +227,8 @@ func main() {
|
|||||||
|
|
||||||
} else if v.Key() == tcell.KeyLeft {
|
} else if v.Key() == tcell.KeyLeft {
|
||||||
fileSelector.MoveCursor(-1)
|
fileSelector.MoveCursor(-1)
|
||||||
|
} else if v.Key() == tcell.KeyEnter {
|
||||||
|
screen.PostEvent(NewEventAttemptImport(fileSelector.Text()))
|
||||||
} else if v.Rune() != 0 {
|
} else if v.Rune() != 0 {
|
||||||
fileSelector.InsertAtCursor(v.Rune())
|
fileSelector.InsertAtCursor(v.Rune())
|
||||||
}
|
}
|
||||||
@ -233,16 +247,45 @@ func main() {
|
|||||||
activeBook.SetStyle(ui.StyleInactive)
|
activeBook.SetStyle(ui.StyleInactive)
|
||||||
menu.SetStyle(ui.StyleActive)
|
menu.SetStyle(ui.StyleActive)
|
||||||
case *EventLoadBook:
|
case *EventLoadBook:
|
||||||
book.SetBook(GetBookByID(v.ID, books))
|
activeBookDetails.SetBook(GetBookByID(v.ID, books))
|
||||||
case *EventOpenImport:
|
case *EventOpenImport:
|
||||||
state.Set("ui_state", IN_IMPORT)
|
state.Set("ui_state", IN_IMPORT)
|
||||||
menu.SetStyle(ui.StyleInactive)
|
menu.SetStyle(ui.StyleInactive)
|
||||||
popup.SetVisible(true)
|
popup.SetVisible(true)
|
||||||
popup.SetSize(6, 3, 5, 80)
|
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:
|
case *EventCloseImport:
|
||||||
state.Set("ui_state", IN_MENU)
|
state.Set("ui_state", IN_MENU)
|
||||||
|
screen.HideCursor()
|
||||||
menu.SetStyle(ui.StyleActive)
|
menu.SetStyle(ui.StyleActive)
|
||||||
popup.SetVisible(false)
|
popup.SetVisible(false)
|
||||||
|
case *EventError:
|
||||||
|
errorMessage.SetText(v.err.Error())
|
||||||
|
errorPopup.SetVisible(true)
|
||||||
case *EventQuit:
|
case *EventQuit:
|
||||||
screen.Fini()
|
screen.Fini()
|
||||||
fmt.Printf("Thank you for playing Wing Commander!\n\n")
|
fmt.Printf("Thank you for playing Wing Commander!\n\n")
|
||||||
@ -253,8 +296,10 @@ func main() {
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
// repaint
|
// repaint
|
||||||
|
l.SetMembers(Titles(state.Get("library").([]book.Book)))
|
||||||
container.Draw(screen)
|
container.Draw(screen)
|
||||||
popup.Draw(screen)
|
popup.Draw(screen)
|
||||||
|
errorPopup.Draw(screen)
|
||||||
screen.Show()
|
screen.Show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -201,7 +201,7 @@ func (m *MySQL) AddBook(ctx context.Context, b *book.Book) error {
|
|||||||
INSERT INTO `+m.tableName+`
|
INSERT INTO `+m.tableName+`
|
||||||
(title, authors, sortauthor, isbn10, isbn13, format, genre, publisher, series, volume, year, signed, description, notes, onloan, coverurl)
|
(title, authors, sortauthor, isbn10, isbn13, format, genre, publisher, series, volume, year, signed, description, notes, onloan, coverurl)
|
||||||
VALUES
|
VALUES
|
||||||
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, )`,
|
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
b.Title,
|
b.Title,
|
||||||
strings.Join(b.Authors, ";"),
|
strings.Join(b.Authors, ";"),
|
||||||
b.SortAuthor,
|
b.SortAuthor,
|
||||||
|
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
|
||||||
|
}
|
4
ui/ui.go
4
ui/ui.go
@ -353,6 +353,10 @@ func (l *List) ListMembers() []ListKeyValue {
|
|||||||
return l.listItems
|
return l.listItems
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *List) SetMembers(lkv []ListKeyValue) {
|
||||||
|
l.listItems = lkv
|
||||||
|
}
|
||||||
|
|
||||||
// BookDetails displays an editable list of book details
|
// BookDetails displays an editable list of book details
|
||||||
type BookDetails struct {
|
type BookDetails struct {
|
||||||
x, y int
|
x, y int
|
||||||
|
Loading…
Reference in New Issue
Block a user