From e6bdb09eaecd476660bb8055f21ac90374b0e422 Mon Sep 17 00:00:00 2001 From: Moritz Biering Date: Sat, 12 Oct 2024 16:55:10 +0200 Subject: [PATCH] improve robustness of application; minor ui improvements --- go.mod | 4 +-- go.sum | 4 +++ pkg/tagesschau/api.go | 10 +++++++ pkg/tui/base_selector.go | 8 +++--- pkg/tui/base_viewer.go | 14 ++++++++-- pkg/tui/details_viewer.go | 10 +++---- pkg/tui/helper.go | 14 +++------- pkg/tui/image_viewer.go | 12 +++++---- pkg/tui/keybinds.go | 19 ++------------ pkg/tui/navigator.go | 54 ++++++++++++++++++++------------------ pkg/tui/search_selector.go | 19 +++----------- pkg/tui/text_viewer.go | 4 +-- pkg/tui/tui.go | 33 ++++++++++++++++------- pkg/tui/util.go | 3 +-- pkg/tui/view_manager.go | 34 +++++++++++------------- 15 files changed, 124 insertions(+), 118 deletions(-) diff --git a/go.mod b/go.mod index 8b49b58..86e8e42 100644 --- a/go.mod +++ b/go.mod @@ -43,8 +43,8 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/sahilm/fuzzy v0.1.1 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - github.com/yuin/goldmark v1.7.4 // indirect - github.com/yuin/goldmark-emoji v1.0.3 // indirect + github.com/yuin/goldmark v1.7.6 // indirect + github.com/yuin/goldmark-emoji v1.0.4 // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.26.0 // indirect diff --git a/go.sum b/go.sum index b4d2e4b..65d0ac5 100644 --- a/go.sum +++ b/go.sum @@ -129,8 +129,12 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg= github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark v1.7.6 h1:cZgJxVh5mL5cu8KOnwxvFJy5TFB0BHUskZZyq7TYbDg= +github.com/yuin/goldmark v1.7.6/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4= github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= +github.com/yuin/goldmark-emoji v1.0.4 h1:vCwMkPZSNefSUnOW2ZKRUjBSD5Ok3W78IXhGxxAEF90= +github.com/yuin/goldmark-emoji v1.0.4/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= diff --git a/pkg/tagesschau/api.go b/pkg/tagesschau/api.go index 5ff2290..dcbc680 100644 --- a/pkg/tagesschau/api.go +++ b/pkg/tagesschau/api.go @@ -16,6 +16,8 @@ const ( homepageAPI string = baseUrl + "api2u/homepage/" searchAPI string = baseUrl + "api2u/search/" shortNewsUrl string = baseUrl + "multimedia/sendung/tagesschau_in_100_sekunden" + + emptyArticleToken string = "EMPTY_ARTICLE" ) type ImageSize int @@ -68,6 +70,14 @@ type Article struct { DetailsWeb string `json:"detailsweb"` } +func EMPTY_ARTICLE() Article { + return Article{Type: emptyArticleToken} +} + +func (n Article) IsEmptyArticle() bool { + return n.Type == emptyArticleToken +} + func (n Article) Title() string { if n.Topline != "" { return n.Topline diff --git a/pkg/tui/base_selector.go b/pkg/tui/base_selector.go index 4035268..e9223dc 100644 --- a/pkg/tui/base_selector.go +++ b/pkg/tui/base_selector.go @@ -17,7 +17,7 @@ const ( ) type Selector interface { - PushCurrentArticle() tea.Cmd + PushSelectedArticle() tea.Cmd SelectorType() SelectorType SetVisible(bool) IsVisible() bool @@ -80,9 +80,9 @@ func (s *BaseSelector) getSelectedArticle() tagesschau.Article { return s.articles[s.selectedIndex] } -func (s BaseSelector) PushCurrentArticle() tea.Cmd { +func (s BaseSelector) PushSelectedArticle() tea.Cmd { return func() tea.Msg { - return ChangedActiveArticle(s.getSelectedArticle()) + return UpdatedArticle(s.getSelectedArticle()) } } @@ -150,7 +150,7 @@ func (s BaseSelector) Update(msg tea.Msg) (BaseSelector, tea.Cmd) { if s.list.Index() != s.selectedIndex { s.selectedIndex = s.list.Index() - cmds = append(cmds, s.PushCurrentArticle()) + cmds = append(cmds, s.PushSelectedArticle()) } return s, tea.Batch(cmds...) diff --git a/pkg/tui/base_viewer.go b/pkg/tui/base_viewer.go index 0fe054d..ff5399f 100644 --- a/pkg/tui/base_viewer.go +++ b/pkg/tui/base_viewer.go @@ -3,6 +3,7 @@ package tui import ( "fmt" "strings" + "time" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/viewport" @@ -12,6 +13,10 @@ import ( "github.com/zMoooooritz/nachrichten/pkg/tagesschau" ) +const ( + emptyArticleHeader string = "LEER" +) + type ViewerType int const ( @@ -112,16 +117,21 @@ func (v *BaseViewer) SetDims(w, h int) { } func (v *BaseViewer) SetHeaderData(article tagesschau.Article) { - if article.IsRegionalArticle() { + date := time.Now() + if article.IsEmptyArticle() { + v.title = emptyArticleHeader + } else if article.IsRegionalArticle() { v.title = article.Desc + date = article.Date } else { if article.Topline != "" { v.title = article.Topline } else { v.title = article.Desc } + date = article.Date } - v.date = article.Date.Format(germanDateFormat) + v.date = date.Format(germanDateFormat) } func (v BaseViewer) Init() tea.Cmd { diff --git a/pkg/tui/details_viewer.go b/pkg/tui/details_viewer.go index d90bc0e..7fedabd 100644 --- a/pkg/tui/details_viewer.go +++ b/pkg/tui/details_viewer.go @@ -30,10 +30,8 @@ func (d *Details) Update(msg tea.Msg) (Viewer, tea.Cmd) { ) switch msg := msg.(type) { - case ChangedActiveArticle: + case UpdatedArticle: d.SetArticle(tagesschau.Article(msg)) - case RefreshActiveViewer: - d.SetArticle(d.shared.activeArticle) } if d.isActive { @@ -61,7 +59,7 @@ func (d *Details) handleNumberInput(number int) tea.Cmd { article, err := tagesschau.LoadArticle(related[index].Details) if err == nil { return tea.Batch( - func() tea.Msg { return ChangedActiveArticle(*article) }, + func() tea.Msg { return UpdatedArticle(*article) }, func() tea.Msg { return ShowTextViewer{} }, ) } @@ -76,7 +74,9 @@ func (d *Details) SetArticle(article tagesschau.Article) { func (d Details) buildDetails(article tagesschau.Article) string { details := "" - if article.IsRegionalArticle() { + if article.IsEmptyArticle() { + details = "" + } else if article.IsRegionalArticle() { details = d.buildRegionalArticleDetails(article) } else { details = d.buildNationalArticleDetails(article) diff --git a/pkg/tui/helper.go b/pkg/tui/helper.go index 2edc29e..6a60da3 100644 --- a/pkg/tui/helper.go +++ b/pkg/tui/helper.go @@ -28,6 +28,7 @@ func NewHelper(shared *SharedState, hstate HelpState) *Helper { state: hstate, } h.model.FullSeparator = " • " + h.model.ShortSeparator = " • " h.model.Styles.ShortKey = shared.style.InactiveStyle h.model.Styles.FullKey = shared.style.InactiveStyle @@ -35,10 +36,10 @@ func NewHelper(shared *SharedState, hstate HelpState) *Helper { } func (h Helper) View() string { - if h.IsVisible() { - return "\n" + lipgloss.NewStyle().Width(h.model.Width).AlignHorizontal(lipgloss.Center).Render(h.model.View(h.shared.keymap)) + if !h.IsVisible() { + return "" } - return "" + return lipgloss.NewStyle().Width(h.model.Width).AlignHorizontal(lipgloss.Center).Render(h.model.View(h.shared.keymap)) } func (h *Helper) Update(msg tea.Msg) (*Helper, tea.Cmd) { @@ -66,13 +67,6 @@ func (h *Helper) nextState() { } } -func (h *Helper) Height() int { - if h.IsVisible() { - return 2 - } - return 0 -} - func (h *Helper) SetWidth(width int) { h.model.Width = width } diff --git a/pkg/tui/image_viewer.go b/pkg/tui/image_viewer.go index abca410..6969eb4 100644 --- a/pkg/tui/image_viewer.go +++ b/pkg/tui/image_viewer.go @@ -29,10 +29,8 @@ func (i *ImageViewer) Update(msg tea.Msg) (Viewer, tea.Cmd) { ) switch msg := msg.(type) { - case ChangedActiveArticle: + case UpdatedArticle: i.SetArticle(tagesschau.Article(msg)) - case RefreshActiveViewer: - i.SetArticle(i.shared.activeArticle) } if i.isFocused || i.isFullScreen { @@ -46,8 +44,12 @@ func (i *ImageViewer) Update(msg tea.Msg) (Viewer, tea.Cmd) { func (i *ImageViewer) SetArticle(article tagesschau.Article) { i.SetHeaderData(article) - img := i.shared.imageCache.GetImage(article.ID, article.ImageData.ImageVariants.RectSmall) - i.pushImageToViewer(img) + if article.IsEmptyArticle() { + i.viewport.SetContent("") + } else { + img := i.shared.imageCache.GetImage(article.ID, article.ImageData.ImageVariants.RectSmall) + i.pushImageToViewer(img) + } } func (i *ImageViewer) pushImageToViewer(img image.Image) { diff --git a/pkg/tui/keybinds.go b/pkg/tui/keybinds.go index 303d5d4..959d2d9 100644 --- a/pkg/tui/keybinds.go +++ b/pkg/tui/keybinds.go @@ -157,22 +157,7 @@ func (k KeyMap) ShortHelp() []key.Binding { func (k KeyMap) FullHelp() [][]key.Binding { return [][]key.Binding{ - {k.left}, - {k.right}, - {k.up}, - {k.down}, - {k.next}, - {k.prev}, - {k.full}, - {k.start}, - {k.end}, - {k.article}, - {k.image}, - {k.details}, - {k.open}, - {k.video}, - {k.shortNews}, - {k.help}, - {k.quit}, + {k.left, k.right, k.up, k.down, k.prev, k.next, k.help, k.quit}, + {k.full, k.start, k.end, k.article, k.image, k.details, k.open, k.video, k.shortNews}, } } diff --git a/pkg/tui/navigator.go b/pkg/tui/navigator.go index a781593..cb7ef32 100644 --- a/pkg/tui/navigator.go +++ b/pkg/tui/navigator.go @@ -80,32 +80,34 @@ func (n *Navigator) Update(msg tea.Msg) (*Navigator, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: - if n.shared.mode == NORMAL_MODE { - switch { - case key.Matches(msg, n.shared.keymap.next): - if n.isFocused && n.isVisible { - n.nextSelector() - cmds = append(cmds, n.selectors[n.activeSelectorIndex].PushCurrentArticle()) - } - case key.Matches(msg, n.shared.keymap.prev): - if n.isFocused && n.isVisible { - n.prevSelector() - cmds = append(cmds, n.selectors[n.activeSelectorIndex].PushCurrentArticle()) - } - case key.Matches(msg, n.shared.keymap.right): - if n.isVisible { - n.isFocused = false - } - case key.Matches(msg, n.shared.keymap.left): - if n.isVisible { - n.isFocused = true - } - case key.Matches(msg, n.shared.keymap.full): - n.isVisible = !n.isVisible - case key.Matches(msg, n.shared.keymap.search): - if n.isFocused && n.isVisible { - n.selectSearchSelector() - } + if n.shared.mode == INSERT_MODE { + break + } + + switch { + case key.Matches(msg, n.shared.keymap.next): + if n.isFocused && n.isVisible { + n.nextSelector() + cmds = append(cmds, n.selectors[n.activeSelectorIndex].PushSelectedArticle()) + } + case key.Matches(msg, n.shared.keymap.prev): + if n.isFocused && n.isVisible { + n.prevSelector() + cmds = append(cmds, n.selectors[n.activeSelectorIndex].PushSelectedArticle()) + } + case key.Matches(msg, n.shared.keymap.right): + if n.isVisible { + n.isFocused = false + } + case key.Matches(msg, n.shared.keymap.left): + if n.isVisible { + n.isFocused = true + } + case key.Matches(msg, n.shared.keymap.full): + n.isVisible = !n.isVisible + case key.Matches(msg, n.shared.keymap.search): + if n.isFocused && n.isVisible { + n.selectSearchSelector() } } } diff --git a/pkg/tui/search_selector.go b/pkg/tui/search_selector.go index db974cc..0c2aeba 100644 --- a/pkg/tui/search_selector.go +++ b/pkg/tui/search_selector.go @@ -1,8 +1,6 @@ package tui import ( - "time" - "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/textinput" @@ -11,15 +9,6 @@ import ( "github.com/zMoooooritz/nachrichten/pkg/tagesschau" ) -var ( - noArticles = []tagesschau.Article{ - { - Topline: "LEER", - Date: time.Now(), - }, - } -) - type SearchSelector struct { BaseSelector search textinput.Model @@ -44,7 +33,7 @@ func NewSearchSelector(selector BaseSelector) *SearchSelector { searchInput.Cursor.TextStyle = selector.shared.style.InactiveStyle searchInput.TextStyle = selector.shared.style.InactiveStyle - selector.articles = noArticles + selector.articles = []tagesschau.Article{tagesschau.EMPTY_ARTICLE()} return &SearchSelector{ BaseSelector: selector, search: searchInput, @@ -63,10 +52,10 @@ func (s *SearchSelector) Update(msg tea.Msg) (Selector, tea.Cmd) { switch msg := msg.(type) { case LoadingArticlesFailed: - s.articles = noArticles + s.articles = []tagesschau.Article{tagesschau.EMPTY_ARTICLE()} s.list.SetItems([]list.Item{}) s.selectedIndex = 0 - cmds = append(cmds, s.PushCurrentArticle()) + cmds = append(cmds, s.PushSelectedArticle()) case tagesschau.SearchResult: result := tagesschau.SearchResult(msg) s.articles = result.Articles @@ -74,7 +63,7 @@ func (s *SearchSelector) Update(msg tea.Msg) (Selector, tea.Cmd) { if s.shared.config.Settings.PreloadThumbnails { go s.shared.imageCache.LoadThumbnails(s.articles) } - cmds = append(cmds, s.PushCurrentArticle()) + cmds = append(cmds, s.PushSelectedArticle()) case tea.KeyMsg: if s.isFocused && s.isVisible { if s.shared.mode == NORMAL_MODE { diff --git a/pkg/tui/text_viewer.go b/pkg/tui/text_viewer.go index c524bc5..c9af2b1 100644 --- a/pkg/tui/text_viewer.go +++ b/pkg/tui/text_viewer.go @@ -28,10 +28,8 @@ func (r *Reader) Update(msg tea.Msg) (Viewer, tea.Cmd) { ) switch msg := msg.(type) { - case ChangedActiveArticle: + case UpdatedArticle: r.SetArticle(tagesschau.Article(msg)) - case RefreshActiveViewer: - r.SetArticle(r.shared.activeArticle) } if r.IsFocused() || r.isFullScreen { diff --git a/pkg/tui/tui.go b/pkg/tui/tui.go index b72327b..aa7039d 100644 --- a/pkg/tui/tui.go +++ b/pkg/tui/tui.go @@ -17,8 +17,7 @@ const ( ) var ( - news tagesschau.News - refreshFunc = func() tea.Msg { return RefreshActiveViewer{} } + news tagesschau.News ) type Model struct { @@ -82,6 +81,12 @@ func (m Model) Init() tea.Cmd { return tea.Batch(loadNews, m.spinner.Tick) } +func refreshFunc(article tagesschau.Article) tea.Cmd { + return func() tea.Msg { + return UpdatedArticle(article) + } +} + func loadNews() tea.Msg { news, err := tagesschau.LoadNews() if err == nil { @@ -106,8 +111,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } m.ready = true m.shared.activeArticle = news.NationalNews[0] - cmds = append(cmds, refreshFunc) - case ChangedActiveArticle: + cmds = append(cmds, refreshFunc(m.shared.activeArticle)) + case UpdatedArticle: article := tagesschau.Article(msg) if m.shared.config.Settings.PreloadThumbnails { go m.shared.imageCache.LoadThumbnail(article) @@ -140,7 +145,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height - cmds = append(cmds, refreshFunc) + cmds = append(cmds, refreshFunc(m.shared.activeArticle)) } if !m.ready { @@ -162,7 +167,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyMsg: switch { case key.Matches(msg, m.shared.keymap.full): - cmds = append(cmds, refreshFunc) + cmds = append(cmds, refreshFunc(m.shared.activeArticle)) } } @@ -184,11 +189,21 @@ func (m Model) View() string { navigatorWidthMultiplier := max(min(m.shared.config.Settings.NavigatorWidth, 0.8), 0.2) navigatorWidth := int(float32(m.width) * navigatorWidthMultiplier) - m.navigator.SetDims(navigatorWidth, m.height-m.helper.Height()) + + helperHeight := lipgloss.Height(help) + if !m.helper.IsVisible() { + helperHeight = 0 + } + + m.navigator.SetDims(navigatorWidth, m.height-helperHeight) navigator := m.navigator.View() - m.viewManager.SetDims(m.width, m.height-m.helper.Height(), lipgloss.Width(navigator)) + m.viewManager.SetDims(m.width, m.height-helperHeight, lipgloss.Width(navigator)) viewer := m.viewManager.View() - return lipgloss.JoinHorizontal(lipgloss.Top, navigator, viewer) + help + view := lipgloss.JoinHorizontal(lipgloss.Top, navigator, viewer) + if m.helper.IsVisible() { + view = lipgloss.JoinVertical(lipgloss.Center, view, help) + } + return view } diff --git a/pkg/tui/util.go b/pkg/tui/util.go index 287b222..27b0b57 100644 --- a/pkg/tui/util.go +++ b/pkg/tui/util.go @@ -14,6 +14,5 @@ func NewDotSpinner() spinner.Model { type LoadingNewsFailed struct{} type LoadingArticlesFailed struct{} -type ChangedActiveArticle tagesschau.Article -type RefreshActiveViewer struct{} +type UpdatedArticle tagesschau.Article type ShowTextViewer struct{} diff --git a/pkg/tui/view_manager.go b/pkg/tui/view_manager.go index d206e57..0013177 100644 --- a/pkg/tui/view_manager.go +++ b/pkg/tui/view_manager.go @@ -9,8 +9,6 @@ type ViewManager struct { shared *SharedState viewers []Viewer activeViewerIndex int - width int - height int } func NewViewManager(shared *SharedState) *ViewManager { @@ -28,15 +26,12 @@ func NewViewManager(shared *SharedState) *ViewManager { } func (v *ViewManager) SetDims(w, h, splitOffset int) { - v.width = w - v.height = h - isViewerFullscreen := v.activeViewer().IsFullScreen() for _, viewer := range v.viewers { if isViewerFullscreen { - viewer.SetDims(v.width, v.height) + viewer.SetDims(w, h) } else { - viewer.SetDims(v.width-splitOffset, v.height) + viewer.SetDims(w-splitOffset, h) } } } @@ -53,9 +48,11 @@ func (v ViewManager) activeViewer() Viewer { func (v *ViewManager) showViewer(vt ViewerType) tea.Cmd { currViewer := v.activeViewer() nextViewer := v.activeViewer() - for _, viewer := range v.viewers { + for index, viewer := range v.viewers { if viewer.ViewerType() == vt { nextViewer = viewer + v.activeViewerIndex = index + break } } if currViewer.ViewerType() == nextViewer.ViewerType() { @@ -70,7 +67,7 @@ func (v *ViewManager) showViewer(vt ViewerType) tea.Cmd { currViewer.SetFocused(false) currViewer.SetFullScreen(false) - return refreshFunc + return refreshFunc(v.shared.activeArticle) } func (v ViewManager) Init() tea.Cmd { @@ -87,15 +84,16 @@ func (v *ViewManager) Update(msg tea.Msg) (*ViewManager, tea.Cmd) { case ShowTextViewer: v.showViewer(VT_TEXT) case tea.KeyMsg: - if v.shared.mode == NORMAL_MODE { - switch { - case key.Matches(msg, v.shared.keymap.article): - v.showViewer(VT_TEXT) - case key.Matches(msg, v.shared.keymap.image): - v.showViewer(VT_IMAGE) - case key.Matches(msg, v.shared.keymap.details): - v.showViewer(VT_DETAILS) - } + if v.shared.mode == INSERT_MODE { + break + } + switch { + case key.Matches(msg, v.shared.keymap.article): + v.showViewer(VT_TEXT) + case key.Matches(msg, v.shared.keymap.image): + v.showViewer(VT_IMAGE) + case key.Matches(msg, v.shared.keymap.details): + v.showViewer(VT_DETAILS) } }