255 lines
6.3 KiB
Go
255 lines
6.3 KiB
Go
package util
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/container"
|
|
"fyne.io/fyne/v2/layout"
|
|
"fyne.io/fyne/v2/theme"
|
|
"fyne.io/fyne/v2/widget"
|
|
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
|
"github.com/dweymouth/supersonic/res"
|
|
"github.com/dweymouth/supersonic/sharedutil"
|
|
myTheme "github.com/dweymouth/supersonic/ui/theme"
|
|
"golang.org/x/net/html"
|
|
)
|
|
|
|
var BoldRichTextStyle = widget.RichTextStyle{TextStyle: fyne.TextStyle{Bold: true}, Inline: true}
|
|
|
|
func MakeOpaque(c color.Color) color.Color {
|
|
if nrgba, ok := c.(color.NRGBA); ok {
|
|
nrgba.A = 255
|
|
return nrgba
|
|
}
|
|
return c
|
|
}
|
|
|
|
func SecondsToTimeString(s float64) string {
|
|
if s < 0 {
|
|
s = 0
|
|
}
|
|
sec := int(math.Round(s))
|
|
min := sec / 60
|
|
sec -= min * 60
|
|
|
|
return fmt.Sprintf("%d:%02d", min, sec)
|
|
}
|
|
|
|
func BytesToSizeString(bytes int64) string {
|
|
var num float64
|
|
var suffix string
|
|
switch b := float64(bytes); {
|
|
case b > 1000000000:
|
|
suffix = "GB"
|
|
num = b / 1000000000
|
|
case b > 1000000:
|
|
suffix = "MB"
|
|
num = b / 1000000
|
|
case b > 1000:
|
|
suffix = "KB"
|
|
num = b / 1000
|
|
default:
|
|
suffix = "B"
|
|
num = b
|
|
}
|
|
return fmt.Sprintf(fmtStringForThreeSigFigs(num)+" %s", num, suffix)
|
|
}
|
|
|
|
func fmtStringForThreeSigFigs(num float64) string {
|
|
switch {
|
|
case num >= 100:
|
|
return "%0.0f"
|
|
case num >= 10:
|
|
return "%0.1f"
|
|
default:
|
|
return "%0.2f"
|
|
}
|
|
}
|
|
|
|
func ImageAspect(im image.Image) float32 {
|
|
b := im.Bounds()
|
|
return float32(b.Max.X-b.Min.X) / float32(b.Max.Y-b.Min.Y)
|
|
}
|
|
|
|
// Debouncer returns a function that will call callOnDone when
|
|
// it has not been invoked since the last dur interval.
|
|
func NewDebouncer(dur time.Duration, callOnDone func()) func() {
|
|
var mu sync.Mutex
|
|
var timer *time.Timer
|
|
return func() {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
if timer != nil {
|
|
timer.Stop()
|
|
}
|
|
timer = time.AfterFunc(dur, callOnDone)
|
|
}
|
|
}
|
|
|
|
func PlaintextFromHTMLString(s string) string {
|
|
tokr := html.NewTokenizer(strings.NewReader(s))
|
|
|
|
var text string
|
|
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 {
|
|
text = text + t.Data
|
|
}
|
|
}
|
|
}
|
|
return text
|
|
}
|
|
|
|
func DisplayReleaseType(releaseTypes mediaprovider.ReleaseTypes) string {
|
|
baseType := "Album"
|
|
switch {
|
|
case releaseTypes&mediaprovider.ReleaseTypeAudiobook > 0:
|
|
baseType = "Audiobook"
|
|
case releaseTypes&mediaprovider.ReleaseTypeAudioDrama > 0:
|
|
baseType = "Audio Drama"
|
|
case releaseTypes&mediaprovider.ReleaseTypeBroadcast > 0:
|
|
baseType = "Broadcast"
|
|
case releaseTypes&mediaprovider.ReleaseTypeDJMix > 0:
|
|
baseType = "DJ-Mix"
|
|
case releaseTypes&mediaprovider.ReleaseTypeEP > 0:
|
|
baseType = "EP"
|
|
case releaseTypes&mediaprovider.ReleaseTypeFieldRecording > 0:
|
|
baseType = "Field Recording"
|
|
case releaseTypes&mediaprovider.ReleaseTypeInterview > 0:
|
|
baseType = "Interview"
|
|
case releaseTypes&mediaprovider.ReleaseTypeMixtape > 0:
|
|
baseType = "Mixtape"
|
|
case releaseTypes&mediaprovider.ReleaseTypeSingle > 0:
|
|
baseType = "Single"
|
|
case releaseTypes&mediaprovider.ReleaseTypeSoundtrack > 0:
|
|
baseType = "Soundtrack"
|
|
}
|
|
|
|
var modifiers []string
|
|
if releaseTypes&mediaprovider.ReleaseTypeLive > 0 {
|
|
modifiers = append(modifiers, "Live")
|
|
}
|
|
if releaseTypes&mediaprovider.ReleaseTypeDemo > 0 {
|
|
modifiers = append(modifiers, "Demo")
|
|
}
|
|
if releaseTypes&mediaprovider.ReleaseTypeRemix > 0 {
|
|
modifiers = append(modifiers, "Remix")
|
|
}
|
|
if releaseTypes&mediaprovider.ReleaseTypeSpokenWord > 0 {
|
|
modifiers = append(modifiers, "Spoken Word")
|
|
}
|
|
if releaseTypes&mediaprovider.ReleaseTypeCompilation > 0 {
|
|
modifiers = append(modifiers, "Compilation")
|
|
}
|
|
|
|
modifiers = append(modifiers, baseType)
|
|
return strings.Join(modifiers, " ")
|
|
}
|
|
|
|
func NewRatingSubmenu(onSetRating func(int)) *fyne.MenuItem {
|
|
newRatingMenuItem := func(rating int) *fyne.MenuItem {
|
|
label := "(none)"
|
|
if rating > 0 {
|
|
label = strconv.Itoa(rating)
|
|
}
|
|
return fyne.NewMenuItem(label, func() {
|
|
onSetRating(rating)
|
|
})
|
|
}
|
|
ratingMenu := fyne.NewMenuItem("Set rating", nil)
|
|
ratingMenu.Icon = theme.NewThemedResource(res.ResStarOutlineSvg)
|
|
ratingMenu.ChildMenu = fyne.NewMenu("", []*fyne.MenuItem{
|
|
newRatingMenuItem(0),
|
|
newRatingMenuItem(1),
|
|
newRatingMenuItem(2),
|
|
newRatingMenuItem(3),
|
|
newRatingMenuItem(4),
|
|
newRatingMenuItem(5),
|
|
}...)
|
|
return ratingMenu
|
|
}
|
|
|
|
func NewReorderTracksSubmenu(onReorderTracks func(sharedutil.TrackReorderOp)) *fyne.MenuItem {
|
|
reorderMenu := fyne.NewMenuItem("Reorder tracks", nil)
|
|
reorderMenu.Icon = myTheme.SortIcon
|
|
reorderMenu.ChildMenu = fyne.NewMenu("", []*fyne.MenuItem{
|
|
fyne.NewMenuItem("Move to top", func() { onReorderTracks(sharedutil.MoveToTop) }),
|
|
fyne.NewMenuItem("Move up", func() { onReorderTracks(sharedutil.MoveUp) }),
|
|
fyne.NewMenuItem("Move down", func() { onReorderTracks(sharedutil.MoveDown) }),
|
|
fyne.NewMenuItem("Move to bottom", func() { onReorderTracks(sharedutil.MoveToBottom) }),
|
|
}...)
|
|
return reorderMenu
|
|
}
|
|
|
|
func AddHeaderBackground(obj fyne.CanvasObject) *fyne.Container {
|
|
return AddHeaderBackgroundWithColorName(obj, myTheme.ColorNamePageBackground)
|
|
}
|
|
|
|
func AddHeaderBackgroundWithColorName(obj fyne.CanvasObject, colorName fyne.ThemeColorName) *fyne.Container {
|
|
bgrnd := myTheme.NewThemedRectangle(colorName)
|
|
bgrnd.CornerRadiusName = theme.SizeNameInputRadius
|
|
return container.NewStack(bgrnd,
|
|
container.New(&layout.CustomPaddedLayout{LeftPadding: 10, RightPadding: 10, TopPadding: 10, BottomPadding: 10},
|
|
obj))
|
|
}
|
|
|
|
func NewTruncatingRichText() *widget.RichText {
|
|
rt := widget.NewRichTextWithText("")
|
|
rt.Truncation = fyne.TextTruncateEllipsis
|
|
return rt
|
|
}
|
|
|
|
func NewTruncatingLabel() *widget.Label {
|
|
rt := widget.NewLabel("")
|
|
rt.Truncation = fyne.TextTruncateEllipsis
|
|
return rt
|
|
}
|
|
|
|
func NewTrailingAlignLabel() *widget.Label {
|
|
rt := widget.NewLabel("")
|
|
rt.Alignment = fyne.TextAlignTrailing
|
|
return rt
|
|
}
|
|
|
|
type HSpace struct {
|
|
widget.BaseWidget
|
|
|
|
Width float32
|
|
}
|
|
|
|
func NewHSpace(w float32) *HSpace {
|
|
h := &HSpace{Width: w}
|
|
h.ExtendBaseWidget(h)
|
|
return h
|
|
}
|
|
|
|
func (h *HSpace) MinSize() fyne.Size {
|
|
return fyne.NewSize(h.Width, 0)
|
|
}
|
|
|
|
func (h *HSpace) CreateRenderer() fyne.WidgetRenderer {
|
|
return widget.NewSimpleRenderer(layout.NewSpacer())
|
|
}
|