From 61bf9be3c52ad1ee85e2f599478fecd1fb6e4da6 Mon Sep 17 00:00:00 2001 From: vpoluyaktov Date: Mon, 27 Nov 2023 16:38:10 -0800 Subject: [PATCH] Keyboard screen navigation implemented --- internal/ui/build_page.go | 148 ++++++++++---------- internal/ui/chapters_page.go | 216 +++++++++++++++++------------- internal/ui/config_page.go | 73 +++++++--- internal/ui/dialog.go | 13 +- internal/ui/download_page.go | 98 +++++++------- internal/ui/encoding_page.go | 100 +++++++------- internal/ui/footer.go | 7 +- internal/ui/search_page.go | 112 ++++++++-------- internal/ui/tui.go | 31 +++-- internal/ui/wrappers.go | 253 +++++++++++++++++++++-------------- 10 files changed, 592 insertions(+), 459 deletions(-) diff --git a/internal/ui/build_page.go b/internal/ui/build_page.go index 5869608..87a1fb5 100644 --- a/internal/ui/build_page.go +++ b/internal/ui/build_page.go @@ -10,23 +10,22 @@ import ( "abb_ia/internal/mq" "abb_ia/internal/utils" - "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) type BuildPage struct { mq *mq.Dispatcher - grid *tview.Grid + mainGrid *grid infoPanel *infoPanel - infoSection *tview.Grid - buildSection *tview.Grid - copySection *tview.Grid - uploadSection *tview.Grid + infoSection *grid + buildSection *grid + copySection *grid + uploadSection *grid buildTable *table copyTable *table uploadTable *table progressTable *table - progressSection *tview.Grid + progressSection *grid ab *dto.Audiobook } @@ -35,36 +34,27 @@ func newBuildPage(dispatcher *mq.Dispatcher) *BuildPage { p.mq = dispatcher p.mq.RegisterListener(mq.BuildPage, p.dispatchMessage) - p.grid = tview.NewGrid() - p.grid.SetRows(7, -1, -1, -1, 4) - p.grid.SetColumns(0) - - // Ignore mouse events when the grid has no focus - p.grid.SetMouseCapture(func(action tview.MouseAction, event *tcell.EventMouse) (tview.MouseAction, *tcell.EventMouse) { - if p.grid.HasFocus() { - return action, event - } else { - return action, nil - } - }) + p.mainGrid = newGrid() + p.mainGrid.SetRows(7, -1, -1, -1, 4) + p.mainGrid.SetColumns(0) // book info section - p.infoSection = tview.NewGrid() + p.infoSection = newGrid() p.infoSection.SetColumns(-2, -1) p.infoSection.SetBorder(true) p.infoSection.SetTitle(" Audiobook information: ") p.infoSection.SetTitleAlign(tview.AlignLeft) p.infoPanel = newInfoPanel() - p.infoSection.AddItem(p.infoPanel.t, 0, 0, 1, 1, 0, 0, true) + p.infoSection.AddItem(p.infoPanel.Table, 0, 0, 1, 1, 0, 0, true) f := newForm() f.SetHorizontal(false) - f.f.SetButtonsAlign(tview.AlignRight) + f.SetButtonsAlign(tview.AlignRight) f.AddButton("Stop", p.stopConfirmation) - p.infoSection.AddItem(f.f, 0, 1, 1, 1, 0, 0, false) - p.grid.AddItem(p.infoSection, 0, 0, 1, 1, 0, 0, false) + p.infoSection.AddItem(f.Form, 0, 1, 1, 1, 0, 0, false) + p.mainGrid.AddItem(p.infoSection.Grid, 0, 0, 1, 1, 0, 0, false) // audiobook build section - p.buildSection = tview.NewGrid() + p.buildSection = newGrid() p.buildSection.SetColumns(-1) p.buildSection.SetTitle(" Building audiobook... ") p.buildSection.SetTitleAlign(tview.AlignLeft) @@ -73,11 +63,11 @@ func newBuildPage(dispatcher *mq.Dispatcher) *BuildPage { p.buildTable.setHeaders(" # ", "File name", "Format", "Duration", "Total Size", "Build progress") p.buildTable.setWeights(1, 2, 1, 1, 1, 5) p.buildTable.setAlign(tview.AlignRight, tview.AlignLeft, tview.AlignLeft, tview.AlignRight, tview.AlignRight, tview.AlignLeft) - p.buildSection.AddItem(p.buildTable.t, 0, 0, 1, 1, 0, 0, true) - p.grid.AddItem(p.buildSection, 1, 0, 1, 1, 0, 0, true) + p.buildSection.AddItem(p.buildTable.Table, 0, 0, 1, 1, 0, 0, true) + p.mainGrid.AddItem(p.buildSection.Grid, 1, 0, 1, 1, 0, 0, true) // copy section - p.copySection = tview.NewGrid() + p.copySection = newGrid() p.copySection.SetColumns(-1) p.copySection.SetTitle(" Copying the book to the output directory: ") p.copySection.SetTitleAlign(tview.AlignLeft) @@ -86,11 +76,11 @@ func newBuildPage(dispatcher *mq.Dispatcher) *BuildPage { p.copyTable.setHeaders(" # ", "File name", "Format", "Duration", "Total Size", "Copy Progress") p.copyTable.setWeights(1, 2, 1, 1, 1, 5) p.copyTable.setAlign(tview.AlignRight, tview.AlignLeft, tview.AlignLeft, tview.AlignRight, tview.AlignRight, tview.AlignLeft) - p.copySection.AddItem(p.copyTable.t, 0, 0, 1, 1, 0, 0, true) - p.grid.AddItem(p.copySection, 2, 0, 1, 1, 0, 0, true) + p.copySection.AddItem(p.copyTable.Table, 0, 0, 1, 1, 0, 0, true) + p.mainGrid.AddItem(p.copySection.Grid, 2, 0, 1, 1, 0, 0, true) // upload section - p.uploadSection = tview.NewGrid() + p.uploadSection = newGrid() p.uploadSection.SetColumns(-1) p.uploadSection.SetTitle(" Uploading the book to Audiobookshelf server: ") p.uploadSection.SetTitleAlign(tview.AlignLeft) @@ -99,11 +89,11 @@ func newBuildPage(dispatcher *mq.Dispatcher) *BuildPage { p.uploadTable.setHeaders(" # ", "File name", "Format", "Duration", "Total Size", "Upload Progress") p.uploadTable.setWeights(1, 2, 1, 1, 1, 5) p.uploadTable.setAlign(tview.AlignRight, tview.AlignLeft, tview.AlignLeft, tview.AlignRight, tview.AlignRight, tview.AlignLeft) - p.uploadSection.AddItem(p.uploadTable.t, 0, 0, 1, 1, 0, 0, true) - p.grid.AddItem(p.uploadSection, 3, 0, 1, 1, 0, 0, true) + p.uploadSection.AddItem(p.uploadTable.Table, 0, 0, 1, 1, 0, 0, true) + p.mainGrid.AddItem(p.uploadSection.Grid, 3, 0, 1, 1, 0, 0, true) // total progress section - p.progressSection = tview.NewGrid() + p.progressSection = newGrid() p.progressSection.SetColumns(-1) p.progressSection.SetBorder(true) p.progressSection.SetTitle(" Build progress: ") @@ -111,9 +101,17 @@ func newBuildPage(dispatcher *mq.Dispatcher) *BuildPage { p.progressTable = newTable() p.progressTable.setWeights(1) p.progressTable.setAlign(tview.AlignLeft) - p.progressTable.t.SetSelectable(false, false) - p.progressSection.AddItem(p.progressTable.t, 0, 0, 1, 1, 0, 0, false) - p.grid.AddItem(p.progressSection, 4, 0, 1, 1, 0, 0, false) + p.progressTable.SetSelectable(false, false) + p.progressSection.AddItem(p.progressTable.Table, 0, 0, 1, 1, 0, 0, false) + p.mainGrid.AddItem(p.progressSection.Grid, 4, 0, 1, 1, 0, 0, false) + + p.mainGrid.SetNavigationOrder( + p.infoPanel.Table, + p.buildTable, + p.copyTable, + p.uploadTable, + p.progressTable, + ) return p } @@ -161,26 +159,26 @@ func (p *BuildPage) displayBookInfo(ab *dto.Audiobook) { p.ab = ab // dynamic grid layout generation - p.grid.Clear() - p.grid.SetColumns(0) - p.grid.SetRows(7, -1, 4) - p.grid.AddItem(p.infoSection, 0, 0, 1, 1, 0, 0, false) - p.grid.AddItem(p.buildSection, 1, 0, 1, 1, 0, 0, true) + p.mainGrid.Clear() + p.mainGrid.SetColumns(0) + p.mainGrid.SetRows(7, -1, 4) + p.mainGrid.AddItem(p.infoSection.Grid, 0, 0, 1, 1, 0, 0, false) + p.mainGrid.AddItem(p.buildSection.Grid, 1, 0, 1, 1, 0, 0, true) if ab.Config.IsCopyToOutputDir() && ab.Config.IsUploadToAudiobookshef() { - p.grid.SetRows(7, -1, -1, -1, 4) - p.grid.AddItem(p.copySection, 2, 0, 1, 1, 0, 0, true) - p.grid.AddItem(p.uploadSection, 3, 0, 1, 1, 0, 0, true) - p.grid.AddItem(p.progressSection, 4, 0, 1, 1, 0, 0, false) + p.mainGrid.SetRows(7, -1, -1, -1, 4) + p.mainGrid.AddItem(p.copySection.Grid, 2, 0, 1, 1, 0, 0, true) + p.mainGrid.AddItem(p.uploadSection.Grid, 3, 0, 1, 1, 0, 0, true) + p.mainGrid.AddItem(p.progressSection.Grid, 4, 0, 1, 1, 0, 0, false) } else if ab.Config.IsCopyToOutputDir() { - p.grid.SetRows(7, -1, -1, 4) - p.grid.AddItem(p.copySection, 2, 0, 1, 1, 0, 0, true) - p.grid.AddItem(p.progressSection, 3, 0, 1, 1, 0, 0, false) + p.mainGrid.SetRows(7, -1, -1, 4) + p.mainGrid.AddItem(p.copySection.Grid, 2, 0, 1, 1, 0, 0, true) + p.mainGrid.AddItem(p.progressSection.Grid, 3, 0, 1, 1, 0, 0, false) } else if ab.Config.IsUploadToAudiobookshef() { - p.grid.SetRows(7, -1, -1, 4) - p.grid.AddItem(p.uploadSection, 2, 0, 1, 1, 0, 0, true) - p.grid.AddItem(p.progressSection, 3, 0, 1, 1, 0, 0, false) + p.mainGrid.SetRows(7, -1, -1, 4) + p.mainGrid.AddItem(p.uploadSection.Grid, 2, 0, 1, 1, 0, 0, true) + p.mainGrid.AddItem(p.progressSection.Grid, 3, 0, 1, 1, 0, 0, false) } else { - p.grid.AddItem(p.progressSection, 2, 0, 1, 1, 0, 0, false) + p.mainGrid.AddItem(p.progressSection.Grid, 2, 0, 1, 1, 0, 0, false) } p.infoPanel.clear() @@ -191,33 +189,33 @@ func (p *BuildPage) displayBookInfo(ab *dto.Audiobook) { p.infoPanel.appendRow("Size:", utils.BytesToHuman(ab.TotalSize)) p.infoPanel.appendRow("Parts:", strconv.Itoa(len(ab.Parts))) - p.buildTable.clear() + p.buildTable.Clear() p.buildTable.showHeader() for i, part := range ab.Parts { p.buildTable.appendRow(" "+strconv.Itoa(i+1)+" ", filepath.Base(part.M4BFile), part.Format, utils.SecondsToTime(part.Duration), utils.BytesToHuman(part.Size), "") } p.buildTable.ScrollToBeginning() - p.copyTable.clear() + p.copyTable.Clear() p.copyTable.showHeader() for i, part := range ab.Parts { p.copyTable.appendRow(" "+strconv.Itoa(i+1)+" ", filepath.Base(part.M4BFile), part.Format, utils.SecondsToTime(part.Duration), utils.BytesToHuman(part.Size), "") } p.copyTable.ScrollToBeginning() - p.uploadTable.clear() + p.uploadTable.Clear() p.uploadTable.showHeader() for i, part := range ab.Parts { p.uploadTable.appendRow(" "+strconv.Itoa(i+1)+" ", filepath.Base(part.M4BFile), part.Format, utils.SecondsToTime(part.Duration), utils.BytesToHuman(part.Size), "") } p.uploadTable.ScrollToBeginning() - p.mq.SendMessage(mq.BuildPage, mq.TUI, &dto.SetFocusCommand{Primitive: p.buildTable.t}, true) - p.mq.SendMessage(mq.BuildPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, true) + ui.SetFocus(p.buildTable.Table) + ui.Draw() } func (p *BuildPage) stopConfirmation() { - newYesNoDialog(p.mq, "Stop Confirmation", "Are you sure you want to stop the build?", p.buildSection, p.stopBuild, func() {}) + newYesNoDialog(p.mq, "Stop Confirmation", "Are you sure you want to stop the build?", p.buildSection.Grid, p.stopBuild, func() {}) } func (p *BuildPage) stopBuild() { @@ -234,12 +232,12 @@ func (p *BuildPage) updateFileBuildProgress(dp *dto.BuildFileProgress) { progressText := fmt.Sprintf(" %3d%% ", dp.Percent) barWidth := int((float32((w - len(progressText))) * float32(dp.Percent) / 100)) progressBar := strings.Repeat("━", barWidth) + strings.Repeat(" ", w-len(progressText)-barWidth) - cell := p.buildTable.t.GetCell(dp.FileId+1, col) + cell := p.buildTable.GetCell(dp.FileId+1, col) // cell.SetExpansion(0) // cell.SetMaxWidth(50) cell.Text = fmt.Sprintf("%s |%s|", progressText, progressBar) // p.buildTable.t.Select(dp.FileId+1, col) - p.mq.SendMessage(mq.BuildPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, false) + ui.Draw() } func (p *BuildPage) updateTotalBuildProgress(dp *dto.BuildProgress) { @@ -248,8 +246,8 @@ func (p *BuildPage) updateTotalBuildProgress(dp *dto.BuildProgress) { p.progressTable.appendRow("") } } - infoCell := p.progressTable.t.GetCell(0, 0) - progressCell := p.progressTable.t.GetCell(1, 0) + infoCell := p.progressTable.GetCell(0, 0) + progressCell := p.progressTable.GetCell(1, 0) infoCell.Text = fmt.Sprintf(" [yellow]Time elapsed: [white]%10s | [yellow]Files: [white]%10s | [yellow]Speed: [white]%10s | [yellow]ETA: [white]%10s", dp.Elapsed, dp.Files, dp.Speed, dp.ETA) col := 0 @@ -260,7 +258,7 @@ func (p *BuildPage) updateTotalBuildProgress(dp *dto.BuildProgress) { // progressCell.SetExpansion(0) // progressCell.SetMaxWidth(0) progressCell.Text = fmt.Sprintf("%s |%s|", progressText, progressBar) - p.mq.SendMessage(mq.BuildPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, false) + ui.Draw() } func (p *BuildPage) updateFileCopyProgress(dp *dto.CopyFileProgress) { @@ -270,12 +268,12 @@ func (p *BuildPage) updateFileCopyProgress(dp *dto.CopyFileProgress) { progressText := fmt.Sprintf(" %3d%% ", dp.Percent) barWidth := int((float32((w - len(progressText))) * float32(dp.Percent) / 100)) progressBar := strings.Repeat("━", barWidth) + strings.Repeat(" ", w-len(progressText)-barWidth) - cell := p.copyTable.t.GetCell(dp.FileId+1, col) + cell := p.copyTable.GetCell(dp.FileId+1, col) // cell.SetExpansion(0) // cell.SetMaxWidth(50) cell.Text = fmt.Sprintf("%s |%s|", progressText, progressBar) // p.copyTable.t.Select(dp.FileId+1, col) - p.mq.SendMessage(mq.BuildPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, false) + ui.Draw() } func (p *BuildPage) updateTotalCopyProgress(dp *dto.CopyProgress) { @@ -285,8 +283,8 @@ func (p *BuildPage) updateTotalCopyProgress(dp *dto.CopyProgress) { } } p.progressSection.SetTitle(" Copy progress: ") - infoCell := p.progressTable.t.GetCell(0, 0) - progressCell := p.progressTable.t.GetCell(1, 0) + infoCell := p.progressTable.GetCell(0, 0) + progressCell := p.progressTable.GetCell(1, 0) infoCell.Text = fmt.Sprintf(" [yellow]Time elapsed: [white]%10s | [yellow]Files: [white]%10s | [yellow]Speed: [white]%12s | [yellow]ETA: [white]%10s", dp.Elapsed, dp.Files, dp.Speed, dp.ETA) col := 0 @@ -297,7 +295,7 @@ func (p *BuildPage) updateTotalCopyProgress(dp *dto.CopyProgress) { // progressCell.SetExpansion(0) // progressCell.SetMaxWidth(0) progressCell.Text = fmt.Sprintf("%s |%s|", progressText, progressBar) - p.mq.SendMessage(mq.BuildPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, false) + ui.Draw() } func (p *BuildPage) updateFileUploadProgress(dp *dto.UploadFileProgress) { @@ -307,12 +305,12 @@ func (p *BuildPage) updateFileUploadProgress(dp *dto.UploadFileProgress) { progressText := fmt.Sprintf(" %3d%% ", dp.Percent) barWidth := int((float32((w - len(progressText))) * float32(dp.Percent) / 100)) progressBar := strings.Repeat("━", barWidth) + strings.Repeat(" ", w-len(progressText)-barWidth) - cell := p.uploadTable.t.GetCell(dp.FileId+1, col) + cell := p.uploadTable.GetCell(dp.FileId+1, col) // cell.SetExpansion(0) // cell.SetMaxWidth(50) cell.Text = fmt.Sprintf("%s |%s|", progressText, progressBar) // p.uploadTable.t.Select(dp.FileId+1, col) - p.mq.SendMessage(mq.BuildPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, false) + ui.Draw() } func (p *BuildPage) updateTotalUploadProgress(dp *dto.UploadProgress) { @@ -322,8 +320,8 @@ func (p *BuildPage) updateTotalUploadProgress(dp *dto.UploadProgress) { } } p.progressSection.SetTitle(" Upload progress: ") - infoCell := p.progressTable.t.GetCell(0, 0) - progressCell := p.progressTable.t.GetCell(1, 0) + infoCell := p.progressTable.GetCell(0, 0) + progressCell := p.progressTable.GetCell(1, 0) infoCell.Text = fmt.Sprintf(" [yellow]Time elapsed: [white]%10s | [yellow]Files: [white]%10s | [yellow]Speed: [white]%12s | [yellow]ETA: [white]%10s", dp.Elapsed, dp.Files, dp.Speed, dp.ETA) col := 0 @@ -334,7 +332,7 @@ func (p *BuildPage) updateTotalUploadProgress(dp *dto.UploadProgress) { // progressCell.SetExpansion(0) // progressCell.SetMaxWidth(0) progressCell.Text = fmt.Sprintf("%s |%s|", progressText, progressBar) - p.mq.SendMessage(mq.BuildPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, false) + ui.Draw() } /* @@ -380,7 +378,7 @@ func (p *BuildPage) cleanupComplete(c *dto.CleanupComplete) { } func (p *BuildPage) bookReadyMgs(ab *dto.Audiobook) { - newMessageDialog(p.mq, "Build Complete", "Audiobook has been created", p.buildSection, p.switchToSearch) + newMessageDialog(p.mq, "Build Complete", "Audiobook has been created", p.buildSection.Grid, p.switchToSearch) } func (p *BuildPage) switchToSearch() { diff --git a/internal/ui/chapters_page.go b/internal/ui/chapters_page.go index f0336a6..c8f0356 100644 --- a/internal/ui/chapters_page.go +++ b/internal/ui/chapters_page.go @@ -11,30 +11,41 @@ import ( "abb_ia/internal/logger" "abb_ia/internal/mq" "abb_ia/internal/utils" - "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" ) type ChaptersPage struct { - mq *mq.Dispatcher - grid *tview.Grid - author *tview.InputField - title *tview.InputField - series *tview.InputField - seriesNo *tview.InputField - genre *tview.DropDown - narator *tview.InputField - cover *tview.InputField - descriptionEditor *tview.TextArea - chaptersSection *tview.Grid - chaptersTable *table - ab *dto.Audiobook - searchDescription string - replaceDescription string - searchChapters string - replaceChapters string - chaptersUndoStack *UndoStack - descriptionUndoStack *UndoStack + mq *mq.Dispatcher + mainGrid *grid + ab *dto.Audiobook + inputAuthor *tview.InputField + inputTitle *tview.InputField + inputSeries *tview.InputField + inputSeriesNo *tview.InputField + inputGenre *tview.DropDown + inputNarator *tview.InputField + inputCover *tview.InputField + buttonCreateBook *tview.Button + buttonCancel *tview.Button + textAreaDescription *tview.TextArea + inputSearchDescription *tview.InputField + inputReplaceDescription *tview.InputField + buttonDescriptionReplace *tview.Button + buttonDescriptionUndo *tview.Button + chaptersSection *grid + chaptersTable *table + inputSearchChapters *tview.InputField + inputReplaceChapters *tview.InputField + buttonChaptersReplace *tview.Button + buttonChaptersUndo *tview.Button + buttonChaptersJoin *tview.Button + searchDescription string + replaceDescription string + searchChapters string + replaceChapters string + chaptersUndoStack *UndoStack + descriptionUndoStack *UndoStack } func newChaptersPage(dispatcher *mq.Dispatcher) *ChaptersPage { @@ -42,21 +53,12 @@ func newChaptersPage(dispatcher *mq.Dispatcher) *ChaptersPage { p.mq = dispatcher p.mq.RegisterListener(mq.ChaptersPage, p.dispatchMessage) - p.grid = tview.NewGrid() - p.grid.SetRows(9, -1, -1) - p.grid.SetColumns(0) - - // Ignore mouse events when the grid has no focus - p.grid.SetMouseCapture(func(action tview.MouseAction, event *tcell.EventMouse) (tview.MouseAction, *tcell.EventMouse) { - if p.grid.HasFocus() { - return action, event - } else { - return action, nil - } - }) + p.mainGrid = newGrid() + p.mainGrid.SetRows(9, -1, -1) + p.mainGrid.SetColumns(0) // book info section - infoSection := tview.NewGrid() + infoSection := newGrid() infoSection.SetColumns(50, 50, -1, 30) infoSection.SetRows(5, 2) infoSection.SetBorder(true) @@ -64,83 +66,83 @@ func newChaptersPage(dispatcher *mq.Dispatcher) *ChaptersPage { infoSection.SetTitleAlign(tview.AlignLeft) f0 := newForm() f0.SetBorderPadding(1, 0, 1, 2) - p.author = f0.AddInputField("Author:", "", 40, nil, func(s string) { + p.inputAuthor = f0.AddInputField("Author:", "", 40, nil, func(s string) { if p.ab != nil { p.ab.Author = s } }) - p.title = f0.AddInputField("Title:", "", 40, nil, func(s string) { + p.inputTitle = f0.AddInputField("Title:", "", 40, nil, func(s string) { if p.ab != nil { p.ab.Title = s } }) - infoSection.AddItem(f0.f, 0, 0, 1, 1, 0, 0, true) + infoSection.AddItem(f0.Form, 0, 0, 1, 1, 0, 0, true) f1 := newForm() f1.SetBorderPadding(1, 0, 2, 2) - p.series = f1.AddInputField("Series:", "", 40, nil, func(s string) { + p.inputSeries = f1.AddInputField("Series:", "", 40, nil, func(s string) { if p.ab != nil { p.ab.Series = s } }) - p.seriesNo = f1.AddInputField("Series No:", "", 5, acceptInt, func(s string) { + p.inputSeriesNo = f1.AddInputField("Series No:", "", 5, acceptInt, func(s string) { if p.ab != nil { p.ab.SeriesNo = s } }) - infoSection.AddItem(f1.f, 0, 1, 1, 1, 0, 0, true) + infoSection.AddItem(f1.Form, 0, 1, 1, 1, 0, 0, true) f2 := newForm() f2.SetBorderPadding(1, 0, 2, 2) - p.genre = f2.AddDropdown("Genre:", utils.AddSpaces(config.Instance().GetGenres()), 0, func(s string, i int) { + p.inputGenre = f2.AddDropdown("Genre:", utils.AddSpaces(config.Instance().GetGenres()), 0, func(s string, i int) { if p.ab != nil { p.ab.Genre = strings.TrimSpace(s) } }) - p.narator = f2.AddInputField("Narator:", "", 40, nil, func(s string) { + p.inputNarator = f2.AddInputField("Narator:", "", 40, nil, func(s string) { if p.ab != nil { p.ab.Narator = s } }) - infoSection.AddItem(f2.f, 0, 2, 1, 1, 0, 0, true) + infoSection.AddItem(f2.Form, 0, 2, 1, 1, 0, 0, true) f3 := newForm() f3.SetBorderPadding(0, 1, 1, 1) - p.cover = f3.AddInputField("Book cover:", "", 0, nil, func(s string) { + p.inputCover = f3.AddInputField("Book cover:", "", 0, nil, func(s string) { if p.ab != nil { p.ab.CoverURL = s } }) - infoSection.AddItem(f3.f, 1, 0, 1, 4, 0, 0, true) + infoSection.AddItem(f3.Form, 1, 0, 1, 4, 0, 0, true) f4 := newForm() f4.SetHorizontal(false) f4.SetButtonsAlign(tview.AlignRight) - f4.AddButton("Create Book", p.buildBook) - f4.AddButton("Cancel", p.stopConfirmation) - infoSection.AddItem(f4.f, 0, 3, 1, 1, 0, 0, false) - p.grid.AddItem(infoSection, 0, 0, 1, 1, 0, 0, false) + p.buttonCreateBook = f4.AddButton("Create Book", p.buildBook) + p.buttonCancel = f4.AddButton("Cancel", p.stopConfirmation) + infoSection.AddItem(f4.Form, 0, 3, 1, 1, 0, 0, false) + p.mainGrid.AddItem(infoSection.Grid, 0, 0, 1, 1, 0, 0, false) // description section - descriptionSection := tview.NewGrid() + descriptionSection := newGrid() descriptionSection.SetColumns(-1, 40) descriptionSection.SetBorder(false) - p.descriptionEditor = newTextArea("") - p.descriptionEditor.SetChangedFunc(p.updateDescription) - p.descriptionEditor.SetBorder(true) - p.descriptionEditor.SetTitle(" Book description: ") - p.descriptionEditor.SetTitleAlign(tview.AlignLeft) - descriptionSection.AddItem(p.descriptionEditor, 0, 0, 1, 1, 0, 0, true) + p.textAreaDescription = newTextArea("") + p.textAreaDescription.SetChangedFunc(p.updateDescription) + p.textAreaDescription.SetBorder(true) + p.textAreaDescription.SetTitle(" Book description: ") + p.textAreaDescription.SetTitleAlign(tview.AlignLeft) + descriptionSection.AddItem(p.textAreaDescription, 0, 0, 1, 1, 0, 0, true) f5 := newForm() f5.SetBorder(true) f5.SetHorizontal(true) - f5.AddInputField("Search: ", "", 30, nil, func(s string) { p.searchDescription = s }) - f5.AddInputField("Replace:", "", 30, nil, func(s string) { p.replaceDescription = s }) - f5.AddButton("Replace", p.searchReplaceDescription) - f5.AddButton(" Undo ", p.undoDescription) + p.inputSearchDescription = f5.AddInputField("Search: ", "", 30, nil, func(s string) { p.searchDescription = s }) + p.inputReplaceDescription = f5.AddInputField("Replace:", "", 30, nil, func(s string) { p.replaceDescription = s }) + p.buttonDescriptionReplace = f5.AddButton("Replace", p.searchReplaceDescription) + p.buttonDescriptionUndo = f5.AddButton(" Undo ", p.undoDescription) f5.SetButtonsAlign(tview.AlignRight) - descriptionSection.AddItem(f5.f, 0, 1, 1, 1, 0, 0, true) - p.grid.AddItem(descriptionSection, 1, 0, 1, 1, 0, 0, true) + descriptionSection.AddItem(f5.Form, 0, 1, 1, 1, 0, 0, true) + p.mainGrid.AddItem(descriptionSection.Grid, 1, 0, 1, 1, 0, 0, true) // chapters section - p.chaptersSection = tview.NewGrid() + p.chaptersSection = newGrid() p.chaptersSection.SetColumns(-1, 40) p.chaptersTable = newTable() p.chaptersTable.SetBorder(true) @@ -151,23 +153,47 @@ func newChaptersPage(dispatcher *mq.Dispatcher) *ChaptersPage { p.chaptersTable.setAlign(tview.AlignRight, tview.AlignRight, tview.AlignRight, tview.AlignRight, tview.AlignLeft) p.chaptersTable.SetSelectedFunc(p.updateChapterEntry) p.chaptersTable.SetMouseDblClickFunc(p.updateChapterEntry) - p.chaptersSection.AddItem(p.chaptersTable.t, 0, 0, 1, 1, 0, 0, true) + p.chaptersSection.AddItem(p.chaptersTable.Table, 0, 0, 1, 1, 0, 0, true) f6 := newForm() f6.SetBorder(true) f6.SetHorizontal(true) - f6.AddInputField("Search: ", "", 30, nil, func(s string) { p.searchChapters = s }) - f6.AddInputField("Replace:", "", 30, nil, func(s string) { p.replaceChapters = s }) - f6.AddButton("Replace", p.searchReplaceChapters) - f6.AddButton(" Undo ", p.undoChapters) - f6.AddButton(" Join Similar Chapters ", p.joinChapters) + p.inputSearchChapters = f6.AddInputField("Search: ", "", 30, nil, func(s string) { p.searchChapters = s }) + p.inputReplaceChapters = f6.AddInputField("Replace:", "", 30, nil, func(s string) { p.replaceChapters = s }) + p.buttonChaptersReplace = f6.AddButton("Replace", p.searchReplaceChapters) + p.buttonChaptersUndo = f6.AddButton(" Undo ", p.undoChapters) + p.buttonChaptersJoin = f6.AddButton(" Join Similar Chapters ", p.joinChapters) f6.SetButtonsAlign(tview.AlignRight) f6.SetMouseDblClickFunc(func() {}) - p.chaptersSection.AddItem(f6.f, 0, 1, 1, 1, 0, 0, false) - p.grid.AddItem(p.chaptersSection, 2, 0, 1, 1, 0, 0, true) + p.chaptersSection.AddItem(f6.Form, 0, 1, 1, 1, 0, 0, false) + p.mainGrid.AddItem(p.chaptersSection.Grid, 2, 0, 1, 1, 0, 0, true) p.chaptersUndoStack = NewUndoStack() p.descriptionUndoStack = NewUndoStack() + // screen elements navigation order + p.mainGrid.SetNavigationOrder( + p.inputAuthor, + p.inputTitle, + p.inputSeries, + p.inputSeriesNo, + p.inputGenre, + p.inputNarator, + p.inputCover, + p.buttonCreateBook, + p.buttonCancel, + p.textAreaDescription, + p.inputSearchDescription, + p.inputReplaceDescription, + p.buttonDescriptionReplace, + p.buttonDescriptionUndo, + p.chaptersTable, + p.inputSearchChapters, + p.inputReplaceChapters, + p.buttonChaptersReplace, + p.buttonChaptersUndo, + p.buttonChaptersJoin, + ) + return p } @@ -199,24 +225,24 @@ func (p *ChaptersPage) dispatchMessage(m *mq.Message) { func (p *ChaptersPage) displayBookInfo(ab *dto.Audiobook) { p.ab = ab - p.author.SetText(ab.Author) - p.title.SetText(ab.Title) - p.cover.SetText(ab.CoverURL) - p.descriptionEditor.SetText(ab.Description, false) + p.inputAuthor.SetText(ab.Author) + p.inputTitle.SetText(ab.Title) + p.inputCover.SetText(ab.CoverURL) + p.textAreaDescription.SetText(ab.Description, false) - p.chaptersTable.clear() + p.chaptersTable.Clear() p.chaptersTable.showHeader() p.chaptersTable.ScrollToBeginning() - p.mq.SendMessage(mq.EncodingPage, mq.TUI, &dto.SetFocusCommand{Primitive: p.chaptersSection}, false) + ui.SetFocus(p.chaptersSection.Grid) } func (p *ChaptersPage) displayParts(ab *dto.Audiobook) { - p.chaptersTable.clear() + p.chaptersTable.Clear() p.chaptersTable.showHeader() for _, part := range ab.Parts { p.addPart(&part) } - p.mq.SendMessage(mq.ChaptersPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, false) + ui.Draw() } func (p *ChaptersPage) addPart(part *dto.Part) { @@ -227,7 +253,7 @@ func (p *ChaptersPage) addPart(part *dto.Part) { for _, chapter := range part.Chapters { p.addChapter(&chapter) } - p.mq.SendMessage(mq.ChaptersPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, false) + ui.Draw() } func (p *ChaptersPage) addChapter(chapter *dto.Chapter) { @@ -241,12 +267,12 @@ func (p *ChaptersPage) addChapter(chapter *dto.Chapter) { func (p *ChaptersPage) updateDescription() { if p.ab != nil { - p.ab.Description = p.descriptionEditor.GetText() + p.ab.Description = p.textAreaDescription.GetText() } } func (p *ChaptersPage) updateChapterEntry(row int, col int) { - chapterNo, err := strconv.Atoi(p.chaptersTable.t.GetCell(row, 0).Text) + chapterNo, err := strconv.Atoi(p.chaptersTable.GetCell(row, 0).Text) if err != nil { // Part Number line found return @@ -254,14 +280,14 @@ func (p *ChaptersPage) updateChapterEntry(row int, col int) { chapter, _ := p.ab.GetChapter(chapterNo) durationH := utils.SecondsToTime(chapter.Duration) - d := newDialogWindow(p.mq, 11, 80, p.chaptersSection) + d := newDialogWindow(p.mq, 11, 80, p.chaptersSection.Grid) f := newForm() f.SetTitle("Update Chapter Name:") f.AddTextView("Chapter #: ", strconv.Itoa(chapter.Number), 5, 1, true, false) f.AddTextView("Duration: ", strings.TrimLeft(durationH, " "), 10, 1, true, false) nameF := f.AddInputField("Chapter name:", chapter.Name, 60, nil, nil) f.AddButton("Save changes", func() { - cell := p.chaptersTable.t.GetCell(row, col) + cell := p.chaptersTable.GetCell(row, col) cell.Text = nameF.GetText() chapter.Name = nameF.GetText() p.ab.SetChapter(chapterNo, *chapter) @@ -270,7 +296,7 @@ func (p *ChaptersPage) updateChapterEntry(row int, col int) { f.AddButton("Cancel", func() { d.Close() }) - d.setForm(f.f) + d.setForm(f.Form) d.Show() } @@ -290,14 +316,14 @@ func (p *ChaptersPage) undoDescription() { ab, err := p.descriptionUndoStack.Pop() if err == nil { p.ab.Description = ab.Description - p.descriptionEditor.SetText(p.ab.Description, false) + p.textAreaDescription.SetText(p.ab.Description, false) } - p.mq.SendMessage(mq.ChaptersPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, true) + ui.Draw() } func (p *ChaptersPage) refreshDescription(ab *dto.Audiobook) { - p.descriptionEditor.SetText(ab.Description, false) - p.mq.SendMessage(mq.ChaptersPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, true) + p.textAreaDescription.SetText(ab.Description, false) + ui.Draw() } func (p *ChaptersPage) searchReplaceChapters() { @@ -335,7 +361,7 @@ func (p *ChaptersPage) refreshChapters(ab *dto.Audiobook) { } func (p *ChaptersPage) stopConfirmation() { - newYesNoDialog(p.mq, "Stop Confirmation", "Are you sure you want to stop editing chapters?", p.chaptersSection, p.stopChapters, func() {}) + newYesNoDialog(p.mq, "Stop Confirmation", "Are you sure you want to stop editing chapters?", p.chaptersSection.Grid, p.stopChapters, func() {}) } func (p *ChaptersPage) stopChapters() { @@ -346,12 +372,12 @@ func (p *ChaptersPage) stopChapters() { func (p *ChaptersPage) buildBook() { // update ab fields just to ensure (they are not updated automatically if a value wasn't change) - p.ab.Author = p.author.GetText() - p.ab.Title = p.title.GetText() - p.ab.Series = p.series.GetText() - p.ab.SeriesNo = p.seriesNo.GetText() - p.ab.Narator = p.narator.GetText() - _, p.ab.Genre = p.genre.GetCurrentOption() + p.ab.Author = p.inputAuthor.GetText() + p.ab.Title = p.inputTitle.GetText() + p.ab.Series = p.inputSeries.GetText() + p.ab.SeriesNo = p.inputSeriesNo.GetText() + p.ab.Narator = p.inputNarator.GetText() + _, p.ab.Genre = p.inputGenre.GetCurrentOption() p.mq.SendMessage(mq.ChaptersPage, mq.BuildController, &dto.BuildCommand{Audiobook: p.ab}, true) p.mq.SendMessage(mq.ChaptersPage, mq.Frame, &dto.SwitchToPageCommand{Name: "BuildPage"}, true) diff --git a/internal/ui/config_page.go b/internal/ui/config_page.go index 1679411..90611dd 100644 --- a/internal/ui/config_page.go +++ b/internal/ui/config_page.go @@ -7,19 +7,20 @@ import ( "abb_ia/internal/dto" "abb_ia/internal/logger" "abb_ia/internal/utils" + "github.com/rivo/tview" "abb_ia/internal/mq" ) type ConfigPage struct { - mq *mq.Dispatcher - grid *tview.Grid + mq *mq.Dispatcher + mainGrid *grid configCopy config.Config - configSection *tview.Grid - buildSection *tview.Grid - absSection *tview.Grid + configSection *grid + buildSection *grid + absSection *grid // Audobookbuilder config section logFileNameField *tview.InputField @@ -58,12 +59,12 @@ func newConfigPage(dispatcher *mq.Dispatcher) *ConfigPage { p.mq = dispatcher p.mq.RegisterListener(mq.ConfigPage, p.dispatchMessage) - p.grid = tview.NewGrid() - p.grid.SetRows(-1, -1, -1) - p.grid.SetColumns(0) + p.mainGrid = newGrid() + p.mainGrid.SetRows(-1, -1, -1) + p.mainGrid.SetColumns(0) // Audobookbuilder config section - p.configSection = tview.NewGrid() + p.configSection = newGrid() p.configSection.SetColumns(-2, -2, -1) p.configSection.SetBorder(true) p.configSection.SetTitle(" Audiobook Builder Configuration: ") @@ -75,7 +76,7 @@ func newConfigPage(dispatcher *mq.Dispatcher) *ConfigPage { p.maxSearchRows = configFormLeft.AddInputField("Maximum rows in the search result:", "", 4, acceptInt, func(t string) { p.configCopy.SetSearchRowsMax(utils.ToInt(t)) }) p.useMockField = configFormLeft.AddCheckbox("Use mock?", false, func(t bool) { p.configCopy.SetUseMock(t) }) p.saveMockField = configFormLeft.AddCheckbox("Save mock?", false, func(t bool) { p.configCopy.SetSaveMock(t) }) - p.configSection.AddItem(configFormLeft.f, 0, 0, 1, 1, 0, 0, true) + p.configSection.AddItem(configFormLeft.Form, 0, 0, 1, 1, 0, 0, true) configFormRight := newForm() configFormRight.SetHorizontal(false) @@ -84,19 +85,19 @@ func newConfigPage(dispatcher *mq.Dispatcher) *ConfigPage { p.tmpDir = configFormRight.AddInputField("Temporary (working) directory:", "", 40, nil, func(t string) { p.configCopy.SetTmpDir(t) }) p.logFileNameField = configFormRight.AddInputField("Log file name:", "", 40, nil, func(t string) { p.configCopy.SetLogfileName(t) }) p.logLevelField = configFormRight.AddDropdown("Log level:", utils.AddSpaces(logger.LogLeves()), 1, func(o string, i int) { p.configCopy.SetLogLevel(strings.TrimSpace(o)) }) - p.configSection.AddItem(configFormRight.f, 0, 1, 1, 1, 0, 0, true) + p.configSection.AddItem(configFormRight.Form, 0, 1, 1, 1, 0, 0, true) buttonsForm := newForm() buttonsForm.SetHorizontal(false) buttonsForm.SetButtonsAlign(tview.AlignRight) p.saveConfigButton = buttonsForm.AddButton("Save Settings", p.SaveConfig) p.cancelButton = buttonsForm.AddButton("Cancel", p.Cancel) - p.configSection.AddItem(buttonsForm.f, 0, 2, 1, 1, 0, 0, false) + p.configSection.AddItem(buttonsForm.Form, 0, 2, 1, 1, 0, 0, false) - p.grid.AddItem(p.configSection, 0, 0, 1, 1, 0, 0, true) + p.mainGrid.AddItem(p.configSection.Grid, 0, 0, 1, 1, 0, 0, true) // audiobook build configuration section - p.buildSection = tview.NewGrid() + p.buildSection = newGrid() p.buildSection.SetColumns(-1, -1) p.buildSection.SetBorder(true) p.buildSection.SetTitle(" Audiobook Build Configuration: ") @@ -109,18 +110,18 @@ func newConfigPage(dispatcher *mq.Dispatcher) *ConfigPage { p.reEncodeFiles = buildFormLeft.AddCheckbox("Re-encode .mp3 files to the same Bit Rate?", false, func(t bool) { p.configCopy.SetReEncodeFiles(t) }) p.bitRate = buildFormLeft.AddInputField("Bit Rate (Kbps):", "", 4, acceptInt, func(t string) { p.configCopy.SetBitRate(utils.ToInt(t)) }) p.sampleRate = buildFormLeft.AddInputField("Sample Rate (Hz):", "", 6, acceptInt, func(t string) { p.configCopy.SetSampleRate(utils.ToInt(t)) }) - p.buildSection.AddItem(buildFormLeft.f, 0, 0, 1, 1, 0, 0, true) + p.buildSection.AddItem(buildFormLeft.Form, 0, 0, 1, 1, 0, 0, true) buildFormRight := newForm() buildFormRight.SetHorizontal(false) p.maxFileSize = buildFormRight.AddInputField("Audiobook part max file size (Mb):", "", 6, acceptInt, func(t string) { p.configCopy.SetMaxFileSizeMb(utils.ToInt(t)) }) p.shortenTitles = buildFormRight.AddCheckbox("Shorten titles (for ex. Old Time Radio -> OTRR)?", false, func(t bool) { p.configCopy.SetShortenTitles(t) }) - p.buildSection.AddItem(buildFormRight.f, 0, 1, 1, 1, 0, 0, true) + p.buildSection.AddItem(buildFormRight.Form, 0, 1, 1, 1, 0, 0, true) - p.grid.AddItem(p.buildSection, 1, 0, 1, 1, 0, 0, true) + p.mainGrid.AddItem(p.buildSection.Grid, 1, 0, 1, 1, 0, 0, true) // audiobookshelf config section - p.absSection = tview.NewGrid() + p.absSection = newGrid() p.absSection.SetColumns(-1) p.absSection.SetBorder(true) p.absSection.SetTitle(" Audiobookshelf Integration: ") @@ -134,13 +135,41 @@ func newConfigPage(dispatcher *mq.Dispatcher) *ConfigPage { p.audiobookshelfPassword = absFormLeft.AddPasswordField("Audiobookshelf Server Password:", "", 40, 0, func(t string) { p.configCopy.SetAudiobookshelfPassword(t) }) p.audiobookshelfLibrary = absFormLeft.AddInputField("Audiobookshelf destination Library:", "", 40, nil, func(t string) { p.configCopy.SetAudiobookshelfLibrary(t) }) p.scanAudiobookshelf = absFormLeft.AddCheckbox("Scan the Audiobookshelf library after copy/upload?", false, func(t bool) { p.configCopy.SetScanAudiobookshelf(t) }) - p.absSection.AddItem(absFormLeft.f, 0, 0, 1, 1, 0, 0, true) + p.absSection.AddItem(absFormLeft.Form, 0, 0, 1, 1, 0, 0, true) // absFormRight := newForm() // absFormRight.SetHorizontal(false) // p.absSection.AddItem(absFormRight.f, 0, 1, 1, 1, 0, 0, true) - p.grid.AddItem(p.absSection, 2, 0, 1, 1, 0, 0, true) + p.mainGrid.AddItem(p.absSection.Grid, 2, 0, 1, 1, 0, 0, true) + + // screen navigation order + p.mainGrid.SetNavigationOrder( + p.searchCondition, + p.maxSearchRows, + p.useMockField, + p.saveMockField, + p.outputDir, + p.copyToOutputDir, + p.tmpDir, + p.logFileNameField, + p.logLevelField, + p.saveConfigButton, + p.cancelButton, + p.concurrentDownloaders, + p.concurrentEncoders, + p.reEncodeFiles, + p.bitRate, + p.sampleRate, + p.maxFileSize, + p.shortenTitles, + p.uploadToAudiobookshelf, + p.audiobookshelfUrl, + p.audiobookshelfUser, + p.audiobookshelfPassword, + p.audiobookshelfLibrary, + p.scanAudiobookshelf, + ) return p } @@ -189,8 +218,8 @@ func (p *ConfigPage) displayConfig(c *dto.DisplayConfigCommand) { p.audiobookshelfUser.SetText(p.configCopy.GetAudiobookshelfUser()) p.audiobookshelfPassword.SetText(p.configCopy.GetAudiobookshelfPassword()) - p.mq.SendMessage(mq.ConfigPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, true) - p.mq.SendMessage(mq.ConfigPage, mq.TUI, &dto.SetFocusCommand{Primitive: p.configSection}, true) + ui.Draw() + ui.SetFocus(p.configSection.Grid) } func (p *ConfigPage) SaveConfig() { diff --git a/internal/ui/dialog.go b/internal/ui/dialog.go index 941b83b..8a23b24 100644 --- a/internal/ui/dialog.go +++ b/internal/ui/dialog.go @@ -3,6 +3,7 @@ package ui import ( "abb_ia/internal/dto" "abb_ia/internal/mq" + "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) @@ -57,16 +58,16 @@ func (d *dialogWindow) Show() { d.mq.SendMessage(mq.DialogWindow, mq.Frame, &dto.AddPageCommand{Name: "DialogWindow", Grid: d.grid}, false) d.mq.SendMessage(mq.DialogWindow, mq.Frame, &dto.ShowPageCommand{Name: "DialogWindow"}, false) d.mq.SendMessage(mq.DialogWindow, mq.TUI, &dto.SetFocusCommand{Primitive: d.form}, false) - d.mq.SendMessage(mq.DialogWindow, mq.TUI, &dto.DrawCommand{Primitive: nil}, true) + ui.Draw() } func (d *dialogWindow) Close() { d.mq.SendMessage(mq.DialogWindow, mq.Frame, &dto.RemovePageCommand{Name: "DialogWindow"}, false) d.mq.SendMessage(mq.DialogWindow, mq.Frame, &dto.RemovePageCommand{Name: "Shadow"}, false) if d.focus != nil { - d.mq.SendMessage(mq.DialogWindow, mq.TUI, &dto.SetFocusCommand{Primitive: d.focus}, false) + ui.SetFocus(d.focus) } - d.mq.SendMessage(mq.DialogWindow, mq.TUI, &dto.DrawCommand{Primitive: nil}, true) + ui.Draw() } func (d *dialogWindow) setForm(f *tview.Form) { @@ -91,6 +92,7 @@ func (d *dialogWindow) setFormAttributes() { } type OkFunc func() + func newMessageDialog(dispatcher *mq.Dispatcher, title string, message string, focus tview.Primitive, okFunc OkFunc) { d := newDialogWindow(dispatcher, 12, 80, focus) f := newForm() @@ -106,11 +108,12 @@ func newMessageDialog(dispatcher *mq.Dispatcher, title string, message string, f okFunc() d.Close() }) - d.setForm(f.f) + d.setForm(f.Form) d.Show() } type YesNoFunc func() + func newYesNoDialog(dispatcher *mq.Dispatcher, title string, message string, focus tview.Primitive, yesFunc YesNoFunc, noFunc YesNoFunc) { d := newDialogWindow(dispatcher, 11, 60, focus) f := newForm() @@ -130,6 +133,6 @@ func newYesNoDialog(dispatcher *mq.Dispatcher, title string, message string, foc noFunc() d.Close() }) - d.setForm(f.f) + d.setForm(f.Form) d.Show() } diff --git a/internal/ui/download_page.go b/internal/ui/download_page.go index 8a94fa0..da25f83 100644 --- a/internal/ui/download_page.go +++ b/internal/ui/download_page.go @@ -8,18 +8,20 @@ import ( "abb_ia/internal/dto" "abb_ia/internal/mq" "abb_ia/internal/utils" - "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" ) type DownloadPage struct { - mq *mq.Dispatcher - grid *tview.Grid - infoPanel *infoPanel - filesSection *tview.Grid - filesTable *table - progressTable *table - ab *dto.Audiobook + mq *mq.Dispatcher + mainGrid *grid + infoSection *grid + infoPanel *infoPanel + filesSection *grid + filesTable *table + progressSection *grid + progressTable *table + ab *dto.Audiobook } func newDownloadPage(dispatcher *mq.Dispatcher) *DownloadPage { @@ -27,36 +29,27 @@ func newDownloadPage(dispatcher *mq.Dispatcher) *DownloadPage { p.mq = dispatcher p.mq.RegisterListener(mq.DownloadPage, p.dispatchMessage) - p.grid = tview.NewGrid() - p.grid.SetRows(7, -1, 4) - p.grid.SetColumns(0) - - // Ignore mouse events when the grid has no focus - p.grid.SetMouseCapture(func(action tview.MouseAction, event *tcell.EventMouse) (tview.MouseAction, *tcell.EventMouse) { - if p.grid.HasFocus() { - return action, event - } else { - return action, nil - } - }) + p.mainGrid = newGrid() + p.mainGrid.SetRows(7, -1, 4) + p.mainGrid.SetColumns(0) // book info section - infoSection := tview.NewGrid() - infoSection.SetColumns(-2, -1) - infoSection.SetBorder(true) - infoSection.SetTitle(" Audiobook information: ") - infoSection.SetTitleAlign(tview.AlignLeft) + p.infoSection = newGrid() + p.infoSection.SetColumns(-2, -1) + p.infoSection.SetBorder(true) + p.infoSection.SetTitle(" Audiobook information: ") + p.infoSection.SetTitleAlign(tview.AlignLeft) p.infoPanel = newInfoPanel() - infoSection.AddItem(p.infoPanel.t, 0, 0, 1, 1, 0, 0, true) + p.infoSection.AddItem(p.infoPanel.Table, 0, 0, 1, 1, 0, 0, true) f := newForm() f.SetHorizontal(false) - f.f.SetButtonsAlign(tview.AlignRight) + f.SetButtonsAlign(tview.AlignRight) f.AddButton("Stop", p.stopConfirmation) - infoSection.AddItem(f.f, 0, 1, 1, 1, 0, 0, false) - p.grid.AddItem(infoSection, 0, 0, 1, 1, 0, 0, false) + p.infoSection.AddItem(f.Form, 0, 1, 1, 1, 0, 0, false) + p.mainGrid.AddItem(p.infoSection.Grid, 0, 0, 1, 1, 0, 0, false) // files downnload section - p.filesSection = tview.NewGrid() + p.filesSection = newGrid() p.filesSection.SetColumns(-1) p.filesSection.SetTitle(" Downloading .mp3 files... ") p.filesSection.SetTitleAlign(tview.AlignLeft) @@ -66,21 +59,28 @@ func newDownloadPage(dispatcher *mq.Dispatcher) *DownloadPage { p.filesTable.setHeaders(" # ", "File name", "Format", "Duration", "Total Size", "Download progress") p.filesTable.setWeights(1, 2, 1, 1, 1, 5) p.filesTable.setAlign(tview.AlignRight, tview.AlignLeft, tview.AlignLeft, tview.AlignRight, tview.AlignRight, tview.AlignLeft) - p.filesSection.AddItem(p.filesTable.t, 0, 0, 1, 1, 0, 0, true) - p.grid.AddItem(p.filesSection, 1, 0, 1, 1, 0, 0, true) + p.filesSection.AddItem(p.filesTable.Table, 0, 0, 1, 1, 0, 0, true) + p.mainGrid.AddItem(p.filesSection.Grid, 1, 0, 1, 1, 0, 0, true) // download progress section - progressSection := tview.NewGrid() - progressSection.SetColumns(-1) - progressSection.SetBorder(true) - progressSection.SetTitle(" Download progress: ") - progressSection.SetTitleAlign(tview.AlignLeft) + p.progressSection = newGrid() + p.progressSection.SetColumns(-1) + p.progressSection.SetBorder(true) + p.progressSection.SetTitle(" Download progress: ") + p.progressSection.SetTitleAlign(tview.AlignLeft) p.progressTable = newTable() p.progressTable.setWeights(1) p.progressTable.setAlign(tview.AlignLeft) - p.progressTable.t.SetSelectable(false, false) - progressSection.AddItem(p.progressTable.t, 0, 0, 1, 1, 0, 0, false) - p.grid.AddItem(progressSection, 2, 0, 1, 1, 0, 0, false) + p.progressTable.SetSelectable(false, false) + p.progressSection.AddItem(p.progressTable.Table, 0, 0, 1, 1, 0, 0, false) + p.mainGrid.AddItem(p.progressSection.Grid, 2, 0, 1, 1, 0, 0, false) + + // screen navigation order + p.mainGrid.SetNavigationOrder( + p.infoPanel.Table, + p.filesTable, + p.progressTable, + ) return p } @@ -117,18 +117,18 @@ func (p *DownloadPage) displayBookInfo(ab *dto.Audiobook) { p.infoPanel.appendRow("Size:", utils.BytesToHuman(ab.IAItem.TotalSize)) p.infoPanel.appendRow("Files", strconv.Itoa(len(ab.IAItem.AudioFiles))) - p.filesTable.clear() + p.filesTable.Clear() p.filesTable.showHeader() for i, f := range ab.IAItem.AudioFiles { p.filesTable.appendRow(" "+strconv.Itoa(i+1)+" ", f.Name, f.Format, utils.SecondsToTime(f.Length), utils.BytesToHuman(f.Size), "") } p.filesTable.ScrollToBeginning() - p.mq.SendMessage(mq.DownloadPage, mq.TUI, &dto.SetFocusCommand{Primitive: p.filesTable.t}, true) - p.mq.SendMessage(mq.DownloadPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, true) + ui.SetFocus(p.filesTable.Table) + ui.Draw() } func (p *DownloadPage) stopConfirmation() { - newYesNoDialog(p.mq, "Stop Confirmation", "Are you sure you want to stop the download?", p.filesSection, p.stopDownload, func() {}) + newYesNoDialog(p.mq, "Stop Confirmation", "Are you sure you want to stop the download?", p.filesSection.Grid, p.stopDownload, func() {}) } func (p *DownloadPage) stopDownload() { @@ -150,12 +150,12 @@ func (p *DownloadPage) updateFileProgress(dp *dto.DownloadFileProgress) { fillerWidth = 0 } progressBar := strings.Repeat("━", barWidth) + strings.Repeat(" ", fillerWidth) - cell := p.filesTable.t.GetCell(dp.FileId+1, col) + cell := p.filesTable.GetCell(dp.FileId+1, col) cell.SetExpansion(0) cell.SetMaxWidth(50) cell.Text = fmt.Sprintf("%s |%s|", progressText, progressBar) // p.downloadTable.t.Select(dp.FileId+1, col) - p.mq.SendMessage(mq.DownloadPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, true) + ui.Draw() } func (p *DownloadPage) updateTotalProgress(dp *dto.DownloadProgress) { @@ -164,8 +164,8 @@ func (p *DownloadPage) updateTotalProgress(dp *dto.DownloadProgress) { p.progressTable.appendRow("") } } - infoCell := p.progressTable.t.GetCell(0, 0) - progressCell := p.progressTable.t.GetCell(1, 0) + infoCell := p.progressTable.GetCell(0, 0) + progressCell := p.progressTable.GetCell(1, 0) infoCell.Text = fmt.Sprintf(" [yellow]Time elapsed: [white]%10s | [yellow]Downloaded: [white]%10s | [yellow]Files: [white]%10s | [yellow]Speed: [white]%12s | [yellow]ETA: [white]%10s", dp.Elapsed, dp.Bytes, dp.Files, dp.Speed, dp.ETA) col := 0 @@ -176,7 +176,7 @@ func (p *DownloadPage) updateTotalProgress(dp *dto.DownloadProgress) { // progressCell.SetExpansion(0) // progressCell.SetMaxWidth(0) progressCell.Text = fmt.Sprintf("%s |%s|", progressText, progressBar) - p.mq.SendMessage(mq.DownloadPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, true) + ui.Draw() } func (p *DownloadPage) downloadComplete(c *dto.DownloadComplete) { diff --git a/internal/ui/encoding_page.go b/internal/ui/encoding_page.go index ee5452c..3d15ce4 100644 --- a/internal/ui/encoding_page.go +++ b/internal/ui/encoding_page.go @@ -8,18 +8,20 @@ import ( "abb_ia/internal/dto" "abb_ia/internal/mq" "abb_ia/internal/utils" - "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" ) type EncodingPage struct { - mq *mq.Dispatcher - grid *tview.Grid - infoPanel *infoPanel - filesSection *tview.Grid - filesTable *table - progressTable *table - ab *dto.Audiobook + mq *mq.Dispatcher + mainGrid *grid + infoSection *grid + infoPanel *infoPanel + filesSection *grid + filesTable *table + progressSection *grid + progressTable *table + ab *dto.Audiobook } func newEncodingPage(dispatcher *mq.Dispatcher) *EncodingPage { @@ -27,36 +29,27 @@ func newEncodingPage(dispatcher *mq.Dispatcher) *EncodingPage { p.mq = dispatcher p.mq.RegisterListener(mq.EncodingPage, p.dispatchMessage) - p.grid = tview.NewGrid() - p.grid.SetRows(7, -1, 4) - p.grid.SetColumns(0) - - // Ignore mouse events when the grid has no focus - p.grid.SetMouseCapture(func(action tview.MouseAction, event *tcell.EventMouse) (tview.MouseAction, *tcell.EventMouse) { - if p.grid.HasFocus() { - return action, event - } else { - return action, nil - } - }) + p.mainGrid = newGrid() + p.mainGrid.SetRows(7, -1, 4) + p.mainGrid.SetColumns(0) // book info section - infoSection := tview.NewGrid() - infoSection.SetColumns(-2, -1) - infoSection.SetBorder(true) - infoSection.SetTitle(" Audiobook information: ") - infoSection.SetTitleAlign(tview.AlignLeft) + p.infoSection = newGrid() + p.infoSection.SetColumns(-2, -1) + p.infoSection.SetBorder(true) + p.infoSection.SetTitle(" Audiobook information: ") + p.infoSection.SetTitleAlign(tview.AlignLeft) p.infoPanel = newInfoPanel() - infoSection.AddItem(p.infoPanel.t, 0, 0, 1, 1, 0, 0, true) + p.infoSection.AddItem(p.infoPanel.Table, 0, 0, 1, 1, 0, 0, true) f := newForm() f.SetHorizontal(false) - f.f.SetButtonsAlign(tview.AlignRight) + f.SetButtonsAlign(tview.AlignRight) f.AddButton("Stop", p.stopConfirmation) - infoSection.AddItem(f.f, 0, 1, 1, 1, 0, 0, false) - p.grid.AddItem(infoSection, 0, 0, 1, 1, 0, 0, false) + p.infoSection.AddItem(f.Form, 0, 1, 1, 1, 0, 0, false) + p.mainGrid.AddItem(p.infoSection.Grid, 0, 0, 1, 1, 0, 0, false) // files re-encoding section - p.filesSection = tview.NewGrid() + p.filesSection = newGrid() p.filesSection.SetColumns(-1) p.filesSection.SetTitle(" Re-encodinging .mp3 files to the same bitrate... ") p.filesSection.SetTitleAlign(tview.AlignLeft) @@ -66,21 +59,28 @@ func newEncodingPage(dispatcher *mq.Dispatcher) *EncodingPage { p.filesTable.setHeaders(" # ", "File name", "Format", "Duration", "Total Size", "Encoding progress") p.filesTable.setWeights(1, 2, 1, 1, 1, 5) p.filesTable.setAlign(tview.AlignRight, tview.AlignLeft, tview.AlignLeft, tview.AlignRight, tview.AlignRight, tview.AlignLeft) - p.filesSection.AddItem(p.filesTable.t, 0, 0, 1, 1, 0, 0, true) - p.grid.AddItem(p.filesSection, 1, 0, 1, 1, 0, 0, true) + p.filesSection.AddItem(p.filesTable.Table, 0, 0, 1, 1, 0, 0, true) + p.mainGrid.AddItem(p.filesSection.Grid, 1, 0, 1, 1, 0, 0, true) // encoding progress section - progressSection := tview.NewGrid() - progressSection.SetColumns(-1) - progressSection.SetBorder(true) - progressSection.SetTitle(" Encoding progress: ") - progressSection.SetTitleAlign(tview.AlignLeft) + p.progressSection = newGrid() + p.progressSection.SetColumns(-1) + p.progressSection.SetBorder(true) + p.progressSection.SetTitle(" Encoding progress: ") + p.progressSection.SetTitleAlign(tview.AlignLeft) p.progressTable = newTable() p.progressTable.setWeights(1) p.progressTable.setAlign(tview.AlignLeft) - p.progressTable.t.SetSelectable(false, false) - progressSection.AddItem(p.progressTable.t, 0, 0, 1, 1, 0, 0, false) - p.grid.AddItem(progressSection, 2, 0, 1, 1, 0, 0, false) + p.progressTable.SetSelectable(false, false) + p.progressSection.AddItem(p.progressTable.Table, 0, 0, 1, 1, 0, 0, false) + p.mainGrid.AddItem(p.progressSection.Grid, 2, 0, 1, 1, 0, 0, false) + + p.mainGrid.SetNavigationOrder( + p.infoPanel.Table, + p.filesTable, + p.progressTable, + ) + return p } @@ -116,18 +116,18 @@ func (p *EncodingPage) displayBookInfo(ab *dto.Audiobook) { p.infoPanel.appendRow("Size:", utils.BytesToHuman(ab.IAItem.TotalSize)) p.infoPanel.appendRow("Files", strconv.Itoa(len(ab.IAItem.AudioFiles))) - p.filesTable.clear() + p.filesTable.Clear() p.filesTable.showHeader() for i, f := range ab.IAItem.AudioFiles { p.filesTable.appendRow(" "+strconv.Itoa(i+1)+" ", f.Name, fmt.Sprintf("MP3 %d kb/s", ab.Config.GetBitRate()), utils.SecondsToTime(f.Length), utils.BytesToHuman(f.Size), "") } - p.filesTable.t.ScrollToBeginning() - p.mq.SendMessage(mq.EncodingPage, mq.TUI, &dto.SetFocusCommand{Primitive: p.filesTable.t}, true) - p.mq.SendMessage(mq.EncodingPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, true) + p.filesTable.ScrollToBeginning() + ui.SetFocus(p.filesTable.Table) + ui.Draw() } func (p *EncodingPage) stopConfirmation() { - newYesNoDialog(p.mq, "Stop Confirmation", "Are you sure you want to stop encoding?", p.filesSection, p.stopEncoding, func() {}) + newYesNoDialog(p.mq, "Stop Confirmation", "Are you sure you want to stop encoding?", p.filesSection.Grid, p.stopEncoding, func() {}) } func (p *EncodingPage) stopEncoding() { @@ -142,12 +142,12 @@ func (p *EncodingPage) updateFileProgress(dp *dto.EncodingFileProgress) { progressText := fmt.Sprintf(" %3d%% ", dp.Percent) barWidth := int((float32((w - len(progressText))) * float32(dp.Percent) / 100)) progressBar := strings.Repeat("━", barWidth) + strings.Repeat(" ", w-len(progressText)-barWidth) - cell := p.filesTable.t.GetCell(dp.FileId+1, col) + cell := p.filesTable.GetCell(dp.FileId+1, col) cell.SetExpansion(0) cell.SetMaxWidth(50) cell.Text = fmt.Sprintf("%s |%s|", progressText, progressBar) // p.encodingTable.t.Select(dp.FileId+1, col) - p.mq.SendMessage(mq.EncodingPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, true) + ui.Draw() } func (p *EncodingPage) updateTotalProgress(dp *dto.EncodingProgress) { @@ -156,8 +156,8 @@ func (p *EncodingPage) updateTotalProgress(dp *dto.EncodingProgress) { p.progressTable.appendRow("") } } - infoCell := p.progressTable.t.GetCell(0, 0) - progressCell := p.progressTable.t.GetCell(1, 0) + infoCell := p.progressTable.GetCell(0, 0) + progressCell := p.progressTable.GetCell(1, 0) infoCell.Text = fmt.Sprintf(" [yellow]Time elapsed: [white]%10s | [yellow]Files: [white]%10s | [yellow]Speed: [white]%10s | [yellow]ETA: [white]%10s", dp.Elapsed, dp.Files, dp.Speed, dp.ETA) col := 0 @@ -168,7 +168,7 @@ func (p *EncodingPage) updateTotalProgress(dp *dto.EncodingProgress) { // progressCell.SetExpansion(0) // progressCell.SetMaxWidth(0) progressCell.Text = fmt.Sprintf("%s |%s|", progressText, progressBar) - p.mq.SendMessage(mq.EncodingPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, true) + ui.Draw() } func (p *EncodingPage) encodingComplete(c *dto.EncodingComplete) { diff --git a/internal/ui/footer.go b/internal/ui/footer.go index f2a6ecc..ee94c75 100644 --- a/internal/ui/footer.go +++ b/internal/ui/footer.go @@ -4,6 +4,7 @@ import ( "abb_ia/internal/config" "abb_ia/internal/dto" "abb_ia/internal/mq" + "github.com/rivo/tview" ) @@ -71,7 +72,7 @@ func (f *footer) dispatchMessage(m *mq.Message) { func (f *footer) updateStatus(s *dto.UpdateStatus) { f.statusMessage.SetText(s.Message) - f.mq.SendMessage(mq.Footer, mq.TUI, &dto.DrawCommand{Primitive: nil}, true) + ui.Draw() } func (f *footer) toggleBusyIndicator(c *dto.SetBusyIndicator) { @@ -86,7 +87,7 @@ func (f *footer) toggleBusyIndicator(c *dto.SetBusyIndicator) { f.busyIndicator.SetText("") f.busyIndicator.SetTextColor(footerFgColor) f.busyIndicator.SetBackgroundColor(footerBgColor) - f.mq.SendMessage(mq.Footer, mq.TUI, &dto.DrawCommand{Primitive: nil}, true) + ui.Draw() } } @@ -98,7 +99,7 @@ func (f *footer) updateBusyIndicator() { // for f.busyFlag { // for i := 0; i < len(busyChars); i++ { // f.busyIndicator.SetText(busyChars[i]) - // f.mq.SendMessage(mq.Footer, mq.TUI, &dto.DrawCommand{Primitive: nil}, true) + // ui.Draw() // time.Sleep(200 * time.Millisecond) // if !f.busyFlag { // break diff --git a/internal/ui/search_page.go b/internal/ui/search_page.go index 601a2ed..b16ba34 100644 --- a/internal/ui/search_page.go +++ b/internal/ui/search_page.go @@ -9,25 +9,27 @@ import ( "abb_ia/internal/logger" "abb_ia/internal/mq" "abb_ia/internal/utils" - "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" ) type SearchPage struct { mq *mq.Dispatcher - grid *tview.Grid + mainGrid *grid searchCriteria string searchResult []*dto.IAItem - searchSection *tview.Grid - inputField *tview.InputField - searchButton *tview.Button - clearButton *tview.Button + searchSection *grid + inputField *tview.InputField + searchButton *tview.Button + clearButton *tview.Button + createAudioBookButton *tview.Button + SettingsButton *tview.Button - resultSection *tview.Grid + resultSection *grid resultTable *table - detailsSection *tview.Grid + detailsSection *grid descriptionView *tview.TextView filesTable *table } @@ -39,21 +41,12 @@ func newSearchPage(dispatcher *mq.Dispatcher) *SearchPage { p.searchCriteria = config.Instance().GetSearchCondition() - p.grid = tview.NewGrid() - p.grid.SetRows(5, -1, -1) - p.grid.SetColumns(0) - - // Ignore mouse events when the grid has no focus - p.grid.SetMouseCapture(func(action tview.MouseAction, event *tcell.EventMouse) (tview.MouseAction, *tcell.EventMouse) { - if p.grid.HasFocus() { - return action, event - } else { - return action, nil - } - }) + p.mainGrid = newGrid() + p.mainGrid.SetRows(5, -1, -1) + p.mainGrid.SetColumns(0) // search section - p.searchSection = tview.NewGrid() + p.searchSection = newGrid() p.searchSection.SetColumns(-2, -1) p.searchSection.SetBorder(true) p.searchSection.SetTitle(" Internet Archive Search ") @@ -63,18 +56,18 @@ func newSearchPage(dispatcher *mq.Dispatcher) *SearchPage { p.inputField = f.AddInputField("Search criteria", config.Instance().GetSearchCondition(), 40, nil, func(t string) { p.searchCriteria = t }) p.searchButton = f.AddButton("Search", p.runSearch) p.clearButton = f.AddButton("Clear", p.clearEverything) - p.searchSection.AddItem(f.f, 0, 0, 1, 1, 0, 0, true) + p.searchSection.AddItem(f.Form, 0, 0, 1, 1, 0, 0, true) f = newForm() f.SetHorizontal(false) - f.f.SetButtonsAlign(tview.AlignRight) - p.searchButton = f.AddButton("Create Audiobook", p.createBook) - p.clearButton = f.AddButton("Settings", p.updateConfig) - p.searchSection.AddItem(f.f, 0, 1, 1, 1, 0, 0, true) + f.SetButtonsAlign(tview.AlignRight) + p.createAudioBookButton = f.AddButton("Create Audiobook", p.createBook) + p.SettingsButton = f.AddButton("Settings", p.updateConfig) + p.searchSection.AddItem(f.Form, 0, 1, 1, 1, 0, 0, true) - p.grid.AddItem(p.searchSection, 0, 0, 1, 1, 0, 0, true) + p.mainGrid.AddItem(p.searchSection.Grid, 0, 0, 1, 1, 0, 0, true) // result section - p.resultSection = tview.NewGrid() + p.resultSection = newGrid() p.resultSection.SetColumns(-1) p.resultSection.SetTitle(" Search result: ") p.resultSection.SetTitleAlign(tview.AlignLeft) @@ -84,12 +77,12 @@ func newSearchPage(dispatcher *mq.Dispatcher) *SearchPage { p.resultTable.setHeaders("Author", "Title", "Files", "Duration (hh:mm:ss)", "Total Size") p.resultTable.setWeights(3, 6, 2, 1, 1) p.resultTable.setAlign(tview.AlignLeft, tview.AlignLeft, tview.AlignRight, tview.AlignRight, tview.AlignRight) - p.resultTable.t.SetSelectionChangedFunc(p.updateDetails) - p.resultSection.AddItem(p.resultTable.t, 0, 0, 1, 1, 0, 0, true) - p.grid.AddItem(p.resultSection, 1, 0, 1, 1, 0, 0, true) + p.resultTable.SetSelectionChangedFunc(p.updateDetails) + p.resultSection.AddItem(p.resultTable.Table, 0, 0, 1, 1, 0, 0, true) + p.mainGrid.AddItem(p.resultSection.Grid, 1, 0, 1, 1, 0, 0, true) // details section - p.detailsSection = tview.NewGrid() + p.detailsSection = newGrid() p.detailsSection.SetRows(-1) p.detailsSection.SetColumns(-1, 1, -1) @@ -102,23 +95,35 @@ func newSearchPage(dispatcher *mq.Dispatcher) *SearchPage { p.detailsSection.AddItem(p.descriptionView, 0, 0, 1, 1, 0, 0, true) p.filesTable = newTable() - p.filesTable.t.SetBorder(true) - p.filesTable.t.SetTitle(" Files: ") - p.filesTable.t.SetTitleAlign(tview.AlignLeft) + p.filesTable.SetBorder(true) + p.filesTable.SetTitle(" Files: ") + p.filesTable.SetTitleAlign(tview.AlignLeft) p.filesTable.setHeaders("File name", "Format", "Duration", "Size") p.filesTable.setWeights(3, 1, 1, 1) p.filesTable.setAlign(tview.AlignLeft, tview.AlignRight, tview.AlignRight, tview.AlignRight) - p.detailsSection.AddItem(p.filesTable.t, 0, 2, 1, 1, 0, 0, true) + p.detailsSection.AddItem(p.filesTable.Table, 0, 2, 1, 1, 0, 0, true) - p.grid.AddItem(p.detailsSection, 2, 0, 1, 1, 0, 0, true) + p.mainGrid.AddItem(p.detailsSection.Grid, 2, 0, 1, 1, 0, 0, true) p.mq.SendMessage(mq.SearchPage, mq.Frame, &dto.SwitchToPageCommand{Name: "SearchPage"}, false) - p.mq.SendMessage(mq.SearchPage, mq.TUI, &dto.SetFocusCommand{Primitive: p.searchSection}, true) + ui.SetFocus(p.searchSection.Grid) - p.grid.Focus(func(pr tview.Primitive) { - p.mq.SendMessage(mq.SearchPage, mq.TUI, &dto.SetFocusCommand{Primitive: p.searchSection}, true) + p.mainGrid.Focus(func(pr tview.Primitive) { + ui.SetFocus(p.searchSection.Grid) }) + // screen navigation order + p.mainGrid.SetNavigationOrder( + p.inputField, + p.searchButton, + p.clearButton, + p.createAudioBookButton, + p.SettingsButton, + p.resultTable.Table, + p.descriptionView, + p.filesTable.Table, + ) + return p } @@ -151,15 +156,15 @@ func (p *SearchPage) runSearch() { p.resultTable.showHeader() // Disable Search Button here p.mq.SendMessage(mq.SearchPage, mq.SearchController, &dto.SearchCommand{SearchCondition: p.searchCriteria}, false) - p.mq.SendMessage(mq.SearchPage, mq.TUI, &dto.SetFocusCommand{Primitive: p.resultTable.t}, true) + p.mq.SendMessage(mq.SearchPage, mq.TUI, &dto.SetFocusCommand{Primitive: p.resultTable.Table}, true) } func (p *SearchPage) clearSearchResults() { p.searchResult = make([]*dto.IAItem, 0) p.resultSection.SetTitle(" Search result: ") - p.resultTable.clear() + p.resultTable.Clear() p.descriptionView.SetText("") - p.filesTable.clear() + p.filesTable.Clear() } func (p *SearchPage) clearEverything() { @@ -172,9 +177,8 @@ func (p *SearchPage) updateResult(i *dto.IAItem) { p.searchResult = append(p.searchResult, i) p.resultTable.appendRow(i.Creator, i.Title, strconv.Itoa(len(i.AudioFiles)), utils.SecondsToTime(i.TotalLength), utils.BytesToHuman(i.TotalSize)) p.resultTable.ScrollToBeginning() - // p.mq.SendMessage(mq.SearchPage, mq.TUI, &dto.DrawCommand{Primitive: p.resultTable.t}, true) // single primitive refresh is not supported by tview (but supported by cview) p.updateDetails(1, 0) - p.mq.SendMessage(mq.SearchPage, mq.TUI, &dto.DrawCommand{Primitive: nil}, true) + ui.Draw() } func (p *SearchPage) updateTitle(sp *dto.SearchProgress) { @@ -186,24 +190,22 @@ func (p *SearchPage) updateDetails(row int, col int) { d := p.searchResult[row-1].Description p.descriptionView.SetText(d) p.descriptionView.ScrollToBeginning() - // p.mq.SendMessage(mq.SearchPage, mq.TUI, &dto.DrawCommand{Primitive: p.descriptionView}, true) // single primitive refresh is not supported by tview (but supported by cview) - p.filesTable.clear() + p.filesTable.Clear() p.filesTable.showHeader() files := p.searchResult[row-1].AudioFiles for _, f := range files { p.filesTable.appendRow(f.Name, f.Format, utils.SecondsToTime(f.Length), utils.BytesToHuman(f.Size)) } p.filesTable.ScrollToBeginning() - // p.mq.SendMessage(mq.SearchPage, mq.TUI, &dto.DrawCommand{Primitive: p.filesTable.t}, true) // single primitive refresh is not supported by tview (but supported by cview) } } func (p *SearchPage) createBook() { // get selectet row from the results table - row, _ := p.resultTable.t.GetSelection() + row, _ := p.resultTable.GetSelection() if row <= 0 || len(p.searchResult) <= 0 || len(p.searchResult) < row { - newMessageDialog(p.mq, "Error", "Please perform a search first", p.searchSection, func() {}) + newMessageDialog(p.mq, "Error", "Please perform a search first", p.searchSection.Grid, func() {}) } else { item := p.searchResult[row-1] // create new audiobook object @@ -212,7 +214,7 @@ func (p *SearchPage) createBook() { c := config.Instance().GetCopy() ab.Config = &c - d := newDialogWindow(p.mq, 17, 55, p.resultSection) + d := newDialogWindow(p.mq, 17, 55, p.resultSection.Grid) f := newForm() f.SetTitle("Create Audiobook") f.AddInputField("Concurrent Downloaders:", utils.ToString(ab.Config.GetConcurrentDownloaders()), 4, acceptInt, func(t string) { ab.Config.SetConcurrentDownloaders(utils.ToInt(t)) }) @@ -229,7 +231,7 @@ func (p *SearchPage) createBook() { f.AddButton("Cancel", func() { d.Close() }) - d.setForm(f.f) + d.setForm(f.Form) d.Show() } } @@ -248,7 +250,7 @@ func (p *SearchPage) showNothingFoundError(dto *dto.NothingFoundError) { newMessageDialog(p.mq, "Error", "No results were found for your search term: [darkblue]'"+dto.SearchCondition+"'[black].\n"+ "Please revise your search criteria.", - p.searchSection, func() {}) + p.searchSection.Grid, func() {}) } func (p *SearchPage) showFFMPEGNotFoundError(dto *dto.FFMPEGNotFoundError) { @@ -256,7 +258,7 @@ func (p *SearchPage) showFFMPEGNotFoundError(dto *dto.FFMPEGNotFoundError) { "This application requires the utilities [darkblue]ffmpeg[black] and [darkblue]ffprobe[black].\n"+ "Please install both [darkblue]ffmpeg[black] and [darkblue]ffprobe[black] by following the instructions provided on FFMPEG website\n"+ "[darkblue]https://ffmpeg.org/download.html", - p.searchSection, func() {}) + p.searchSection.Grid, func() {}) } func (p *SearchPage) showNewVersionMessage(dto *dto.NewAppVersionFound) { @@ -264,5 +266,5 @@ func (p *SearchPage) showNewVersionMessage(dto *dto.NewAppVersionFound) { "New version of the application has been released: [darkblue]"+dto.NewVersion+"[black]\n"+ "Your current version is [darkblue]"+dto.CurrentVersion+"[black]\n"+ "You can download the new version of the application from:\n[darkblue]https://abb_ia/releases", - p.searchSection, func() {}) + p.searchSection.Grid, func() {}) } diff --git a/internal/ui/tui.go b/internal/ui/tui.go index ecfb2f1..f921878 100644 --- a/internal/ui/tui.go +++ b/internal/ui/tui.go @@ -7,6 +7,7 @@ import ( "abb_ia/internal/dto" "abb_ia/internal/mq" + "github.com/rivo/tview" ) @@ -22,11 +23,13 @@ type TUI struct { app *tview.Application } +var ui *TUI + type Fn func() func NewTUI(dispatcher *mq.Dispatcher) *TUI { - ui := TUI{} + ui = &TUI{} ui.app = tview.NewApplication() ui.app.EnableMouse(true) setColorTheme() @@ -48,12 +51,12 @@ func NewTUI(dispatcher *mq.Dispatcher) *TUI { frame := newFrame(dispatcher) frame.addHeader(header) frame.addFooter(footer) - frame.addPage("SearchPage", searchPage.grid) - frame.addPage("ConfigPage", configPage.grid) - frame.addPage("DownloadPage", downloadPage.grid) - frame.addPage("EncodingPage", encodingPage.grid) - frame.addPage("ChaptersPage", chaptersPage.grid) - frame.addPage("BuildPage", buildPage.grid) + frame.addPage("SearchPage", searchPage.mainGrid.Grid) + frame.addPage("ConfigPage", configPage.mainGrid.Grid) + frame.addPage("DownloadPage", downloadPage.mainGrid.Grid) + frame.addPage("EncodingPage", encodingPage.mainGrid.Grid) + frame.addPage("ChaptersPage", chaptersPage.mainGrid.Grid) + frame.addPage("BuildPage", buildPage.mainGrid.Grid) ui.components = append(ui.components, frame) ui.components = append(ui.components, header) @@ -68,7 +71,7 @@ func NewTUI(dispatcher *mq.Dispatcher) *TUI { frame.switchToPage("SearchPage") ui.app.SetRoot(frame.grid, true) - return &ui + return ui } func (ui *TUI) startEventListener() { @@ -101,6 +104,18 @@ func (ui *TUI) checkMQ() { } } +func (ui *TUI) SetFocus(p tview.Primitive) { + ui.app.SetFocus(p) +} + +func (ui *TUI) GetFocus() tview.Primitive { + return ui.app.GetFocus() +} + +func (ui *TUI) Draw() { + go ui.app.Draw() +} + func (ui *TUI) dispatchMessage(m *mq.Message) { switch cmd := m.Dto.(type) { case *dto.DrawCommand: diff --git a/internal/ui/wrappers.go b/internal/ui/wrappers.go index 632b8c1..e3776fd 100644 --- a/internal/ui/wrappers.go +++ b/internal/ui/wrappers.go @@ -1,6 +1,7 @@ package ui import ( + "fmt" "math" "strconv" "sync" @@ -9,11 +10,114 @@ import ( "github.com/rivo/tview" ) +// // ////////////////////////////////////////////////////////////// +// // tview.Grid wrapper +// // ////////////////////////////////////////////////////////////// +type grid struct { + *tview.Grid + navigationOrder []tview.Primitive +} + +func newGrid() *grid { + g := &grid{} + g.Grid = tview.NewGrid() + g.navigationOrder = []tview.Primitive{} + + // Ignore mouse events when the grid has no focus + g.SetMouseCapture(func(action tview.MouseAction, event *tcell.EventMouse) (tview.MouseAction, *tcell.EventMouse) { + if g.HasFocus() { + return action, event + } else if len(g.navigationOrder) == 0 { + return action, event + } else { + return action, nil + } + }) + + // Tab / Backtab navigation + g.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + if g.Grid.HasFocus() { + switch event.Key() { + case tcell.KeyTab: + if len(g.navigationOrder) == 0 { + return event + } + currentElement, err := g.getFocusIndex() + if err != nil { + return event + } + nextElement := currentElement + 1 + if nextElement > len(g.navigationOrder)-1 { + nextElement = 0 + } + nextPrimitive := g.navigationOrder[nextElement] + ui.SetFocus(nextPrimitive) + ui.Draw() + return nil + + case tcell.KeyBacktab: + if len(g.navigationOrder) == 0 { + return event + } + currentElement, err := g.getFocusIndex() + if err != nil { + return event + } + previousElement := currentElement - 1 + if previousElement < 0 { + previousElement = len(g.navigationOrder) - 1 + } + previousPrimitive := g.navigationOrder[previousElement] + ui.SetFocus(previousPrimitive) + ui.Draw() + return nil + default: + return event + } + } else { + return event + } + }) + + g.SetFocusFunc(func() { + // g.SetBorderColor(yellow) + }) + + g.SetBlurFunc(func() { + // g.SetBorderColor(black) + }) + + return g +} + +func (g *grid) SetNavigationOrder(navigationOrder ...tview.Primitive) { + g.navigationOrder = navigationOrder +} + +// return an index of the focus element in the navigation list +func (g *grid) getFocusIndex() (int, error) { + index := 0 + found := false + focus := ui.GetFocus() + for i, v := range g.navigationOrder { + if focus == v { + found = true + index = i + break + } + } + if found { + return index, nil + } else { + return index, fmt.Errorf("focus element not found") + } +} + // ////////////////////////////////////////////////////////////// // tview.Table wrapper // ////////////////////////////////////////////////////////////// type table struct { - t *tview.Table + *tview.Table headers []string colWeight []int colWidth []int @@ -22,52 +126,35 @@ type table struct { func newTable() *table { t := &table{} - t.t = tview.NewTable() - t.t.SetDrawFunc(t.draw) - t.t.SetSelectable(true, false) - t.t.SetSeparator(tview.Borders.Vertical) - // t.t.SetSortClicked(false) - // t.t.SetSortFunc() // TODO implement sorting - // t.t.ShowFocus(true) - t.t.SetBorder(false) - t.t.Clear() - // t.t.SetEvaluateAllRows(true) + t.Table = tview.NewTable() + t.Table.SetDrawFunc(t.draw) + t.Table.SetSelectable(true, false) + t.Table.SetSeparator(tview.Borders.Vertical) + // t.Table.SetSortClicked(false) + // t.Table.SetSortFunc() // TODO implement sorting?? + // t.Table.ShowFocus(true) + t.Table.SetBorder(false) + t.Table.Clear() + // t.Table.SetEvaluateAllRows(true) return t } -// Enter Key -func (t *table) SetSelectedFunc(f func(row, column int)) { - t.t.SetSelectedFunc(f) -} - // Mouse Double Click func (t *table) SetMouseDblClickFunc(f func(row, column int)) { - t.t.SetMouseCapture(func(action tview.MouseAction, event *tcell.EventMouse) (tview.MouseAction, *tcell.EventMouse) { + t.Table.SetMouseCapture(func(action tview.MouseAction, event *tcell.EventMouse) (tview.MouseAction, *tcell.EventMouse) { switch action { case tview.MouseLeftDoubleClick: { - f(t.t.GetSelection()) + f(t.Table.GetSelection()) } } return action, event }) } -func (t *table) SetBorder(b bool) { - t.t.SetBorder(b) -} - -func (t *table) SetTitle(s string) { - t.t.SetTitle(s) -} - -func (t *table) SetTitleAlign(a int) { - t.t.SetTitleAlign(a) -} - func (t *table) draw(screen tcell.Screen, x int, y int, width int, height int) (int, int, int, int) { t.recalculateColumnWidths() - return t.t.GetInnerRect() + return t.Table.GetInnerRect() } func (t *table) setHeaders(headers ...string) { @@ -92,25 +179,25 @@ func (t *table) showHeader() { cell.SetExpansion(t.colWeight[c]) cell.SetMaxWidth(t.colWidth[c]) cell.NotSelectable = true - t.t.SetCell(0, c, cell) + t.SetCell(0, c, cell) } - t.t.SetFixed(1, 0) - t.t.Select(1, 0) + t.SetFixed(1, 0) + t.Select(1, 0) } func (t *table) appendRow(cols ...string) { - row := t.t.GetRowCount() + row := t.GetRowCount() for col, val := range cols { cell := tview.NewTableCell(val) cell.SetAlign(int(t.aligns[col])) cell.SetExpansion(t.colWeight[col]) cell.SetMaxWidth(t.colWidth[col]) - t.t.SetCell(row, col, cell) + t.SetCell(row, col, cell) } } func (t *table) appendSeparator(cols ...string) { - row := t.t.GetRowCount() + row := t.GetRowCount() for col, val := range cols { cell := tview.NewTableCell(val) cell.SetAlign(int(t.aligns[col])) @@ -119,18 +206,10 @@ func (t *table) appendSeparator(cols ...string) { // cell.NotSelectable = true cell.SetTextColor(tview.Styles.PrimaryTextColor) cell.SetBackgroundColor(lightBlue) - t.t.SetCell(row, col, cell) + t.SetCell(row, col, cell) } } -func (t *table) ScrollToBeginning() { - t.t.ScrollToBeginning() -} - -func (t *table) clear() { - t.t.Clear() -} - // TODO - implement more accurate calculation func (t *table) recalculateColumnWidths() { if len(t.colWeight) == 0 { @@ -140,7 +219,7 @@ func (t *table) recalculateColumnWidths() { for _, w := range t.colWeight { allWeights += w } - _, _, tw, _ := t.t.GetInnerRect() // table weight + _, _, tw, _ := t.Table.GetInnerRect() // table weight m := (float64(tw-len(t.colWeight)-1) / float64(allWeights)) // multiplier t.colWidth = make([]int, len(t.colWeight)) @@ -156,81 +235,61 @@ func (t *table) getColumnWidth(col int) int { return t.colWidth[col] } -func (t *table) GetRowCount() int { - return t.t.GetColumnCount() -} - // ////////////////////////////////////////////////////////////// // tview.Table wrapper (vertical layout) // ////////////////////////////////////////////////////////////// type infoPanel struct { - t *tview.Table + *tview.Table } func newInfoPanel() *infoPanel { p := &infoPanel{} - p.t = tview.NewTable() - p.t.SetSelectable(false, false) - p.t.SetBorder(false) + p.Table = tview.NewTable() + p.SetSelectable(false, false) + p.SetBorder(false) return p } func (p *infoPanel) appendRow(label string, value string) { - row := p.t.GetRowCount() + row := p.GetRowCount() // label labelCell := tview.NewTableCell(" " + label) labelCell.SetTextColor(yellow) - p.t.SetCell(row, 0, labelCell) + p.SetCell(row, 0, labelCell) // value valueCell := tview.NewTableCell(" " + value) - p.t.SetCell(row, 1, valueCell) + p.SetCell(row, 1, valueCell) } func (p *infoPanel) clear() { - p.t.Clear() + p.Clear() } // ////////////////////////////////////////////////////////////// // tview.Form wrapper // ////////////////////////////////////////////////////////////// type form struct { - f *tview.Form + *tview.Form mu sync.Mutex } func newForm() *form { f := &form{} - f.f = tview.NewForm() - f.f.SetFieldTextColor(black) + f.Form = tview.NewForm() + f.SetFieldTextColor(black) // f.f.SetFieldBackgroundColor(black) - f.f.SetButtonTextColor(black) + f.SetButtonTextColor(black) // f.f.SetButtonBackgroundColor() return f } -func (f *form) SetHorizontal(b bool) { - f.f.SetHorizontal(b) -} - func (f *form) SetTitle(t string) { - f.f.SetTitle(" " + t + " ") -} - -func (f *form) SetBorder(b bool) { - f.f.SetBorder(b) -} - -func (f *form) SetButtonsAlign(a int) { - f.f.SetButtonsAlign(a) -} - -func (f *form) SetBorderPadding(top int, bottom int, left int, right int) { - f.f.SetBorderPadding(top, bottom, left, right) + f.Form.SetTitle(" " + t + " ") } // Mouse Double Click func (f *form) SetMouseDblClickFunc(fn func()) { - f.f.SetMouseCapture(func(action tview.MouseAction, event *tcell.EventMouse) (tview.MouseAction, *tcell.EventMouse) { + f.SetMouseCapture(func(action tview.MouseAction, event *tcell.EventMouse) (tview.MouseAction, *tcell.EventMouse) { switch action { case tview.MouseLeftDoubleClick: { @@ -248,9 +307,9 @@ func (f *form) AddInputField(label, value string, fieldWidth int, accept func(te // value = fmt.Sprintf("%*s", fieldWidth, value) // } - f.f.AddInputField(label, value, fieldWidth, accept, changed) + f.Form.AddInputField(label, value, fieldWidth, accept, changed) // return just created input field - obj := f.f.GetFormItem(f.f.GetFormItemCount() - 1).(*tview.InputField) + obj := f.GetFormItem(f.GetFormItemCount() - 1).(*tview.InputField) f.mu.Unlock() return obj } @@ -262,63 +321,63 @@ func acceptInt(textToCheck string, lastChar rune) bool { func (f *form) AddButton(label string, selected func()) *tview.Button { f.mu.Lock() - f.f.AddButton(label, selected) + f.Form.AddButton(label, selected) // return just created button - obj := f.f.GetButton(f.f.GetButtonCount() - 1) + obj := f.GetButton(f.GetButtonCount() - 1) f.mu.Unlock() return obj } func (f *form) AddCheckbox(label string, checked bool, changed func(checked bool)) *tview.Checkbox { f.mu.Lock() - f.f.AddCheckbox(label, checked, changed) + f.Form.AddCheckbox(label, checked, changed) // return just created checkbox - obj := f.f.GetFormItem(f.f.GetFormItemCount() - 1).(*tview.Checkbox) + obj := f.GetFormItem(f.GetFormItemCount() - 1).(*tview.Checkbox) f.mu.Unlock() return obj } func (f *form) AddDropdown(label string, options []string, initialOption int, selected func(option string, optionIndex int)) *tview.DropDown { f.mu.Lock() - f.f.AddDropDown(label, options, initialOption, selected) + f.AddDropDown(label, options, initialOption, selected) // return just created dropdown - obj := f.f.GetFormItem(f.f.GetFormItemCount() - 1).(*tview.DropDown) + obj := f.GetFormItem(f.GetFormItemCount() - 1).(*tview.DropDown) f.mu.Unlock() return obj } func (f *form) AddPasswordField(label, value string, fieldWidth int, mask rune, changed func(text string)) *tview.InputField { f.mu.Lock() - f.f.AddPasswordField(label, value, fieldWidth, mask, changed) + f.Form.AddPasswordField(label, value, fieldWidth, mask, changed) // return just created InputField - obj := f.f.GetFormItem(f.f.GetFormItemCount() - 1).(*tview.InputField) + obj := f.GetFormItem(f.GetFormItemCount() - 1).(*tview.InputField) f.mu.Unlock() return obj } func (f *form) AddTextArea(label, text string, fieldWidth, fieldHeight, maxLength int, changed func(text string)) *tview.TextArea { f.mu.Lock() - f.f.AddTextArea(label, text, fieldWidth, fieldHeight, maxLength, changed) + f.Form.AddTextArea(label, text, fieldWidth, fieldHeight, maxLength, changed) // return just created InputField - obj := f.f.GetFormItem(f.f.GetFormItemCount() - 1).(*tview.TextArea) + obj := f.GetFormItem(f.GetFormItemCount() - 1).(*tview.TextArea) f.mu.Unlock() return obj } func (f *form) AddTextView(label, text string, fieldWidth, fieldHeight int, dynamicColors, scrollable bool) *tview.TextView { f.mu.Lock() - f.f.AddTextView(label, text, fieldWidth, fieldHeight, dynamicColors, scrollable) + f.Form.AddTextView(label, text, fieldWidth, fieldHeight, dynamicColors, scrollable) // return just created InputField - obj := f.f.GetFormItem(f.f.GetFormItemCount() - 1).(*tview.TextView) + obj := f.GetFormItem(f.GetFormItemCount() - 1).(*tview.TextView) f.mu.Unlock() return obj } func (f *form) AddFormItem(item tview.FormItem) *tview.FormItem { f.mu.Lock() - f.f.AddFormItem(item) + f.Form.AddFormItem(item) // return just created FormItem - obj := f.f.GetFormItem(f.f.GetFormItemCount() - 1) + obj := f.GetFormItem(f.GetFormItemCount() - 1) f.mu.Unlock() return &obj }