From f8cf595c885427dc92ab6e47950afee2b7f2a327 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Mon, 30 Jan 2023 17:28:27 -0800 Subject: [PATCH 1/3] add image and biography to artist page --- ui/browsing/artistpage.go | 118 +++++++++++++++++++++++++++++++++++--- 1 file changed, 110 insertions(+), 8 deletions(-) diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index c0b399c..58affbc 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -1,15 +1,20 @@ package browsing import ( + "image/color" "log" "supersonic/backend" + "supersonic/res" + "supersonic/ui/layouts" "supersonic/ui/widgets" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" + "github.com/dweymouth/go-subsonic" ) var _ fyne.Widget = (*ArtistPage)(nil) @@ -22,7 +27,7 @@ type ArtistPage struct { sm *backend.ServerManager nav func(Route) grid *widgets.AlbumGrid - titleDisp *widget.RichText + header *ArtistPageHeader container *fyne.Container OnPlayAlbum func(string, int) @@ -36,11 +41,8 @@ func NewArtistPage(artistID string, sm *backend.ServerManager, im *backend.Image nav: nav, } a.ExtendBaseWidget(a) - a.titleDisp = widget.NewRichTextWithText("Artist") - a.titleDisp.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{ - SizeName: theme.SizeNameHeadingText, - } - a.container = container.NewBorder(a.titleDisp, nil, nil, nil, layout.NewSpacer()) + a.header = NewArtistPageHeader() + a.container = container.NewBorder(a.header, nil, nil, nil, layout.NewSpacer()) a.loadAsync() return a } @@ -83,8 +85,11 @@ func (a *ArtistPage) loadAsync() { log.Printf("Failed to get artist: %s", err.Error()) return } - a.titleDisp.Segments[0].(*widget.TextSegment).Text = artist.Name - a.titleDisp.Refresh() + info, err := a.sm.Server.GetArtistInfo2(a.artistID, nil) + if err != nil { + log.Printf("Failed to get artist info: %s", err.Error()) + } + a.header.Update(artist, info) ag := widgets.NewFixedAlbumGrid(artist.Album, a.im, true /*showYear*/) ag.OnPlayAlbum = a.onPlayAlbum ag.OnShowAlbumPage = a.onShowAlbumPage @@ -108,3 +113,100 @@ type savedArtistPage struct { func (s *savedArtistPage) Restore() Page { return NewArtistPage(s.artistID, s.sm, s.im, s.nav) } + +type ArtistPageHeader struct { + widget.BaseWidget + + artistID string + artistImageCtr *fyne.Container + titleDisp *widget.RichText + biographyDisp *widget.Label + similarArtists *widget.RichText + container *fyne.Container +} + +func NewArtistPageHeader() *ArtistPageHeader { + a := &ArtistPageHeader{ + titleDisp: widget.NewRichTextWithText(""), + biographyDisp: widget.NewLabel("Artist description not available"), + similarArtists: widget.NewRichText(), + } + a.titleDisp.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{ + SizeName: theme.SizeNameHeadingText, + } + a.artistImageCtr = container.New(&layouts.CenterPadLayout{PadLeftRight: 10, PadTopBottom: 10}, + NewMissingArtistImage()) + a.biographyDisp.Wrapping = fyne.TextWrapWord + a.ExtendBaseWidget(a) + a.createContainer() + return a +} + +func (a *ArtistPageHeader) Update(artist *subsonic.ArtistID3, info *subsonic.ArtistInfo2) { + if artist == nil { + return + } + a.artistID = artist.ID + a.titleDisp.Segments[0].(*widget.TextSegment).Text = artist.Name + if info != nil { + if info.Biography != "" { + a.biographyDisp.Text = info.Biography + } + /** TODO: + if len(info.SimilarArtist) > 0 { + segments := make([]widget.RichTextSegment, 0) + segments = append(segments, &widget.TextSegment{Text: "Similar artists: "}) + for sim := info.SimilarArtist { + segments = append(segments, &widget.HyperlinkSegment{ + + }) + } + } + */ + go func() { + if res, err := fyne.LoadResourceFromURLString(info.MediumImageUrl); err != nil { + img := canvas.NewImageFromResource(res) + img.SetMinSize(fyne.NewSize(225, 225)) + a.artistImageCtr.RemoveAll() + a.artistImageCtr.Add(img) + a.artistImageCtr.Refresh() + } + }() + } + a.Refresh() +} + +func (a *ArtistPageHeader) createContainer() { + a.container = container.NewBorder(nil, nil, a.artistImageCtr, nil, + container.NewBorder(a.titleDisp, a.similarArtists, nil, nil, a.biographyDisp)) +} + +func (a *ArtistPageHeader) CreateRenderer() fyne.WidgetRenderer { + return widget.NewSimpleRenderer(a.container) +} + +type MissingArtistImage struct { + widget.BaseWidget + container *fyne.Container +} + +func NewMissingArtistImage() *MissingArtistImage { + m := &MissingArtistImage{} + m.ExtendBaseWidget(m) + img := canvas.NewImageFromResource(res.ResPeopleInvertPng) + img.FillMode = canvas.ImageFillContain + img.SetMinSize(fyne.NewSize(64, 64)) + rect := canvas.NewRectangle(color.Transparent) + rect.StrokeColor = color.Black + rect.StrokeWidth = 3 + rect.SetMinSize(fyne.NewSize(225, 225)) + m.container = container.NewMax( + container.NewCenter(img), + rect, + ) + return m +} + +func (m *MissingArtistImage) CreateRenderer() fyne.WidgetRenderer { + return widget.NewSimpleRenderer(m.container) +} From 844930f0ae6cc05b3500cc85c2f6c85589d42557 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Mon, 30 Jan 2023 19:12:05 -0800 Subject: [PATCH 2/3] fixes and improvements to artist page --- ui/browsing/artistpage.go | 72 +++++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index 58affbc..f2b4b5c 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -1,6 +1,8 @@ package browsing import ( + "bytes" + "image" "image/color" "log" "supersonic/backend" @@ -85,16 +87,17 @@ func (a *ArtistPage) loadAsync() { log.Printf("Failed to get artist: %s", err.Error()) return } - info, err := a.sm.Server.GetArtistInfo2(a.artistID, nil) - if err != nil { - log.Printf("Failed to get artist info: %s", err.Error()) - } - a.header.Update(artist, info) + a.header.Update(artist) ag := widgets.NewFixedAlbumGrid(artist.Album, a.im, true /*showYear*/) ag.OnPlayAlbum = a.onPlayAlbum ag.OnShowAlbumPage = a.onShowAlbumPage a.container.Objects[0] = ag a.container.Refresh() + info, err := a.sm.Server.GetArtistInfo2(a.artistID, nil) + if err != nil { + log.Printf("Failed to get artist info: %s", err.Error()) + } + a.header.UpdateInfo(info) }() } @@ -142,38 +145,47 @@ func NewArtistPageHeader() *ArtistPageHeader { return a } -func (a *ArtistPageHeader) Update(artist *subsonic.ArtistID3, info *subsonic.ArtistInfo2) { +func (a *ArtistPageHeader) Update(artist *subsonic.ArtistID3) { if artist == nil { return } a.artistID = artist.ID a.titleDisp.Segments[0].(*widget.TextSegment).Text = artist.Name - if info != nil { - if info.Biography != "" { - a.biographyDisp.Text = info.Biography - } - /** TODO: - if len(info.SimilarArtist) > 0 { - segments := make([]widget.RichTextSegment, 0) - segments = append(segments, &widget.TextSegment{Text: "Similar artists: "}) - for sim := info.SimilarArtist { - segments = append(segments, &widget.HyperlinkSegment{ + a.titleDisp.Refresh() +} - }) - } - } - */ - go func() { - if res, err := fyne.LoadResourceFromURLString(info.MediumImageUrl); err != nil { - img := canvas.NewImageFromResource(res) - img.SetMinSize(fyne.NewSize(225, 225)) - a.artistImageCtr.RemoveAll() - a.artistImageCtr.Add(img) - a.artistImageCtr.Refresh() - } - }() +func (a *ArtistPageHeader) UpdateInfo(info *subsonic.ArtistInfo2) { + if info == nil { + return + } + if info.Biography != "" { + a.biographyDisp.SetText(info.Biography) + } + /** TODO: + if len(info.SimilarArtist) > 0 { + segments := make([]widget.RichTextSegment, 0) + segments = append(segments, &widget.TextSegment{Text: "Similar artists: "}) + for sim := info.SimilarArtist { + segments = append(segments, &widget.HyperlinkSegment{ + + }) + } + } + */ + if info.LargeImageUrl != "" { + if res, err := fyne.LoadResourceFromURLString(info.LargeImageUrl); err == nil { + im, _, err := image.Decode(bytes.NewReader(res.Content())) + if err != nil { + return + } + img := canvas.NewImageFromImage(im) + img.FillMode = canvas.ImageFillContain + img.SetMinSize(fyne.NewSize(225, 225)) + a.artistImageCtr.RemoveAll() + a.artistImageCtr.Add(img) + a.artistImageCtr.Refresh() + } } - a.Refresh() } func (a *ArtistPageHeader) createContainer() { From 4bc25c9b0ff0f94304e37c3ee0dd6c0fb981ebd6 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 1 Feb 2023 17:03:24 -0800 Subject: [PATCH 3/3] use HTML tokenizer to parse artist biography --- README.md | 2 +- go.mod | 2 +- ui/browsing/artistpage.go | 18 +++++++++++++----- ui/util/util.go | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 72ca2b4..c0dfedc 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,10 @@ Slightly outdated screenshots of Supersonic running against the Navidrome 0 { + if ts, ok := segs[0].(*widget.TextSegment); ok && strings.TrimSpace(ts.Text) != "" { + a.biographyDisp.Segments = segs + a.biographyDisp.Refresh() + } + } } /** TODO: if len(info.SimilarArtist) > 0 { @@ -208,13 +216,13 @@ func NewMissingArtistImage() *MissingArtistImage { img := canvas.NewImageFromResource(res.ResPeopleInvertPng) img.FillMode = canvas.ImageFillContain img.SetMinSize(fyne.NewSize(64, 64)) - rect := canvas.NewRectangle(color.Transparent) + rect := canvas.NewRectangle(theme.BackgroundColor()) rect.StrokeColor = color.Black rect.StrokeWidth = 3 rect.SetMinSize(fyne.NewSize(225, 225)) m.container = container.NewMax( - container.NewCenter(img), rect, + container.NewCenter(img), ) return m } diff --git a/ui/util/util.go b/ui/util/util.go index aa2481b..e5f3b83 100644 --- a/ui/util/util.go +++ b/ui/util/util.go @@ -3,6 +3,10 @@ package util import ( "fmt" "math" + "strings" + + "fyne.io/fyne/v2/widget" + "golang.org/x/net/html" ) func SecondsToTimeString(s float64) string { @@ -15,3 +19,31 @@ func SecondsToTimeString(s float64) string { return fmt.Sprintf("%3d:%02d", min, sec) } + +func RichTextSegsFromHTMLString(s string) []widget.RichTextSegment { + tokr := html.NewTokenizer(strings.NewReader(s)) + var segs []widget.RichTextSegment + + var isLink bool + var done bool + for !done { + tt := tokr.Next() + switch { + case tt == html.ErrorToken: + done = true + case tt == html.StartTagToken: + t := tokr.Token() + isLink = t.Data == "a" + case tt == html.EndTagToken: + isLink = false + case tt == html.TextToken: + t := tokr.Token() + // for now, skip displaying Navidrome's "Read more on Last.FM" link + if !isLink { + segs = append(segs, &widget.TextSegment{Text: t.Data}) + } + } + } + + return segs +}