diff --git a/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go b/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go index 0239a77..0f2d209 100644 --- a/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go +++ b/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go @@ -430,6 +430,8 @@ func toTrack(ch *jellyfin.Song) *mediaprovider.Track { coverArtID = ch.Id } + lastPlayed, _ := time.Parse(time.RFC3339Nano, ch.UserData.LastPlayedDate) + t := &mediaprovider.Track{ ID: ch.Id, CoverArtID: coverArtID, @@ -447,6 +449,7 @@ func toTrack(ch *jellyfin.Song) *mediaprovider.Track { Rating: ch.UserData.Rating, Favorite: ch.UserData.IsFavorite, PlayCount: ch.UserData.PlayCount, + LastPlayed: lastPlayed, } if len(ch.MediaSources) > 0 { t.FilePath = ch.MediaSources[0].Path diff --git a/res/translations/en.json b/res/translations/en.json index 02b5408..973db0b 100644 --- a/res/translations/en.json +++ b/res/translations/en.json @@ -131,6 +131,7 @@ "My Server": "My Server", "Name": "Name", "Name (A-Z)": "Name (A-Z)", + "never": "never", "Next": "Next", "Nickname": "Nickname", "Normal": "Normal", @@ -289,5 +290,26 @@ "playlist.addedtracks": { "one": "Added one track to playlist", "other": "Added {{.trackCount}} tracks to playlist" + }, + + "x_minutes_ago": { + "one": "a minute ago", + "other": "{{.minutes}} minutes ago" + }, + "x_hours_ago": { + "one": "an hour ago", + "other": "{{.hours}} hours ago" + }, + "x_days_ago": { + "one": "a day ago", + "other": "{{.days}} days ago" + }, + "x_months_ago": { + "one": "a month ago", + "other": "{{.months}} months ago" + }, + "x_years_ago": { + "one": "a year ago", + "other": "{{.years}} years ago" } } diff --git a/res/translations/es.json b/res/translations/es.json index 12b5883..0bc29ee 100644 --- a/res/translations/es.json +++ b/res/translations/es.json @@ -113,6 +113,7 @@ "My Server": "Mi servidor", "Name": "Nombre", "Name (A-Z)": "Nombre (A-Z)", + "never": "nunca", "Next": "Siguiente", "Nickname": "Apodo", "Now Playing": "Reproduciendo", @@ -252,5 +253,26 @@ "playlist.addedtracks": { "one": "Se ha añadido una pista a la lista de reproducción", "other": "Se han añadido {{.trackCount}} pistas a la lista de reproducción" + }, + + "x_minutes_ago": { + "one": "hace un minuto", + "other": "hace {{.minutes}} minutos" + }, + "x_hours_ago": { + "one": "hace una hora", + "other": "hace {{.hours}} horas" + }, + "x_days_ago": { + "one": "hace un día", + "other": "hace {{.days}} días" + }, + "x_months_ago": { + "one": "hace un mes", + "other": "hace {{.months}} meses" + }, + "x_years_ago": { + "one": "hace un año", + "other": "hace {{.years}} años" } } diff --git a/ui/util/util.go b/ui/util/util.go index 5bab70b..000e8a6 100644 --- a/ui/util/util.go +++ b/ui/util/util.go @@ -100,6 +100,39 @@ func FormatItemDate(date mediaprovider.ItemDate) string { return sb.String() } +func LastPlayedDisplayString(t time.Time) string { + if t.IsZero() { + return lang.L("never") + } + switch d := time.Since(t); { + case d.Hours() < 1: + mins := int(d.Minutes()) + return lang.LocalizePluralKey("x_minutes_ago", + fmt.Sprintf("%d minutes ago", mins), mins, + map[string]string{"minutes": strconv.Itoa(mins)}) + case d.Hours() < 24: + hrs := int(d.Hours()) + return lang.LocalizePluralKey("x_hours_ago", + fmt.Sprintf("%d hours ago", hrs), hrs, + map[string]string{"hours": strconv.Itoa(hrs)}) + case d.Hours() < 24*31: + days := int(d.Hours() / 24) + return lang.LocalizePluralKey("x_days_ago", + fmt.Sprintf("%d days ago", days), days, + map[string]string{"days": strconv.Itoa(days)}) + case d.Hours() < 24*365: + months := int(d.Hours() / (24 * 31)) + return lang.LocalizePluralKey("x_months_ago", + fmt.Sprintf("%d months ago", months), months, + map[string]string{"months": strconv.Itoa(months)}) + default: + years := int(d.Hours() / (24 * 365)) + return lang.LocalizePluralKey("x_years_ago", + fmt.Sprintf("%d years ago", years), years, + map[string]string{"years": strconv.Itoa(years)}) + } +} + var BoldRichTextStyle = widget.RichTextStyle{TextStyle: fyne.TextStyle{Bold: true}, Inline: true} func MakeOpaque(c color.Color) color.Color { diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index 05c90b6..f792e3b 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -446,6 +446,8 @@ func (t *Tracklist) doSortTracks() { t.intSort(func(tr *util.TrackListModel) int64 { return tr.Track().Size }) case ColumnPlays: t.intSort(func(tr *util.TrackListModel) int64 { return int64(tr.Track().PlayCount) }) + case ColumnLastPlayed: + t.intSort(func(tr *util.TrackListModel) int64 { return tr.Track().LastPlayed.Unix() }) case ColumnComment: t.stringSort(func(tr *util.TrackListModel) string { return tr.Track().Comment }) case ColumnBPM: diff --git a/ui/widgets/tracklistrow.go b/ui/widgets/tracklistrow.go index 397a197..bf58318 100644 --- a/ui/widgets/tracklistrow.go +++ b/ui/widgets/tracklistrow.go @@ -61,6 +61,7 @@ const ( ColumnFavorite = "Favorite" ColumnRating = "Rating" ColumnPlays = "Plays" + ColumnLastPlayed = "LastPlayed" ColumnComment = "Comment" ColumnBPM = "BPM" ColumnBitrate = "Bitrate" @@ -87,6 +88,7 @@ var ( fav := lang.L("Fav.") rating := lang.L("Rating") plays := lang.L("Plays") + lastPlayed := lang.L("Last played") comment := lang.L("Comment") bpm := lang.L("BPM") bitrate := lang.L("Bit rate") @@ -104,6 +106,7 @@ var ( {Name: ColumnFavorite, Col: ListColumn{Text: " " + fav, Alignment: fyne.TextAlignCenter, CanToggleVisible: true}}, {Name: ColumnRating, Col: ListColumn{Text: rating, Alignment: fyne.TextAlignLeading, CanToggleVisible: true}}, {Name: ColumnPlays, Col: ListColumn{Text: plays, Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}}, + {Name: ColumnLastPlayed, Col: ListColumn{Text: lastPlayed, Alignment: fyne.TextAlignLeading, CanToggleVisible: true}}, {Name: ColumnComment, Col: ListColumn{Text: comment, Alignment: fyne.TextAlignLeading, CanToggleVisible: true}}, {Name: ColumnBPM, Col: ListColumn{Text: bpm, Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}}, {Name: ColumnBitrate, Col: ListColumn{Text: bitrate, Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}}, @@ -121,6 +124,7 @@ var ( {Name: ColumnFavorite, Col: ListColumn{Text: fav, Alignment: fyne.TextAlignCenter, CanToggleVisible: true}}, {Name: ColumnRating, Col: ListColumn{Text: rating, Alignment: fyne.TextAlignLeading, CanToggleVisible: true}}, {Name: ColumnPlays, Col: ListColumn{Text: plays, Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}}, + {Name: ColumnLastPlayed, Col: ListColumn{Text: lastPlayed, Alignment: fyne.TextAlignLeading, CanToggleVisible: true}}, {Name: ColumnComment, Col: ListColumn{Text: comment, Alignment: fyne.TextAlignLeading, CanToggleVisible: true}}, {Name: ColumnBPM, Col: ListColumn{Text: bpm, Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}}, {Name: ColumnBitrate, Col: ListColumn{Text: bitrate, Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}}, @@ -144,6 +148,10 @@ var ( playsColWidth := fyne.Max( widget.NewLabel("9999").MinSize().Width, widget.NewLabel(plays).MinSize().Width+sortIconWidth) + lastPlayedColWidth := fyne.Max( + widget.NewLabel(lang.LocalizePluralKey("x_minutes_ago", "59 minutes ago", 59, map[string]string{"minutes": "59"})).MinSize().Width, + widget.NewLabel(lastPlayed).MinSize().Width+sortIconWidth, + ) bpmColWidth := fyne.Max( widget.NewLabel(bpm+" ").MinSize().Width, widget.NewLabel("9999").MinSize().Width) @@ -154,10 +162,10 @@ var ( widget.NewLabel("99.9 MB").MinSize().Width, widget.NewLabel(size).MinSize().Width+sortIconWidth) - // #, Title, Artist, Album, Composer, Time, Year, Favorite, Rating, Plays, Comment, BPM, Bitrate, Size, Path - CompactTracklistRowColumnWidths = []float32{numColWidth, -1, -1, -1, -1, timeColWidth, yearColWidth, favColWidth, ratingColWidth, playsColWidth, -1, bpmColWidth, bitrateColWidth, sizeColWidth, -1} - // #, Title/Artist, Album, Composer, Time, Year, Favorite, Rating, Plays, Comment, BPM, Bitrate, Size, Path - ExpandedTracklistRowColumnWidths = []float32{numColWidth, -1, -1, -1, timeColWidth, yearColWidth, favColWidth, ratingColWidth, playsColWidth, -1, bpmColWidth, bitrateColWidth, sizeColWidth, -1} + // #, Title, Artist, Album, Composer, Time, Year, Favorite, Rating, Plays, LastPlayed, Comment, BPM, Bitrate, Size, Path + CompactTracklistRowColumnWidths = []float32{numColWidth, -1, -1, -1, -1, timeColWidth, yearColWidth, favColWidth, ratingColWidth, playsColWidth, lastPlayedColWidth, -1, bpmColWidth, bitrateColWidth, sizeColWidth, -1} + // #, Title/Artist, Album, Composer, Time, Year, Favorite, Rating, Plays, LastPlayed, Comment, BPM, Bitrate, Size, Path + ExpandedTracklistRowColumnWidths = []float32{numColWidth, -1, -1, -1, timeColWidth, yearColWidth, favColWidth, ratingColWidth, playsColWidth, lastPlayedColWidth, -1, bpmColWidth, bitrateColWidth, sizeColWidth, -1} }) ) @@ -185,21 +193,22 @@ type tracklistRowBase struct { nextUpdateModel *util.TrackListModel nextUpdateRowNum int - num *widget.Label - name *ttwidget.RichText - artist *MultiHyperlink - album *MultiHyperlink // for disabled support, if albumID is "" - composer *MultiHyperlink - dur *widget.Label - year *widget.Label - favorite *fyne.Container - rating *StarRating - bitrate *widget.Label - plays *widget.Label - comment *ttwidget.Label - bpm *widget.Label - size *widget.Label - path *ttwidget.Label + num *widget.Label + name *ttwidget.RichText + artist *MultiHyperlink + album *MultiHyperlink // for disabled support, if albumID is "" + composer *MultiHyperlink + dur *widget.Label + year *widget.Label + favorite *fyne.Container + rating *StarRating + bitrate *widget.Label + plays *widget.Label + lastPlayed *widget.Label + comment *ttwidget.Label + bpm *widget.Label + size *widget.Label + path *ttwidget.Label // must be injected by extending widget setColVisibility func(int, bool) bool @@ -253,7 +262,7 @@ func NewExpandedTracklistRow(tracklist *Tracklist, im *backend.ImageManager, pla v := makeVerticallyCentered // func alias container := container.New(tracklist.colLayout, - v(t.num), titleArtistImg, v(t.album), v(t.composer), v(t.dur), v(t.year), v(t.favorite), v(t.rating), v(t.plays), v(t.comment), v(t.bpm), v(t.bitrate), v(t.size), v(t.path)) + v(t.num), titleArtistImg, v(t.album), v(t.composer), v(t.dur), v(t.year), v(t.favorite), v(t.rating), v(t.plays), v(t.lastPlayed), v(t.comment), v(t.bpm), v(t.bitrate), v(t.size), v(t.path)) t.Content = container t.setColVisibility = func(colNum int, vis bool) bool { c := container.Objects[colNum].(*fyne.Container) @@ -281,7 +290,7 @@ func NewCompactTracklistRow(tracklist *Tracklist, playingIcon fyne.CanvasObject) t.playingIcon = playingIcon t.Content = container.New(tracklist.colLayout, - t.num, t.name, t.artist, t.album, t.composer, t.dur, t.year, t.favorite, t.rating, t.plays, t.comment, t.bpm, t.bitrate, t.size, t.path) + t.num, t.name, t.artist, t.album, t.composer, t.dur, t.year, t.favorite, t.rating, t.plays, t.lastPlayed, t.comment, t.bpm, t.bitrate, t.size, t.path) colHiddenPtrMap := map[int]*bool{ 2: &t.artist.Hidden, @@ -292,11 +301,12 @@ func NewCompactTracklistRow(tracklist *Tracklist, playingIcon fyne.CanvasObject) 7: &t.favorite.Hidden, 8: &t.rating.Hidden, 9: &t.plays.Hidden, - 10: &t.comment.Hidden, - 11: &t.bpm.Hidden, - 12: &t.bitrate.Hidden, - 13: &t.size.Hidden, - 14: &t.path.Hidden, + 10: &t.lastPlayed.Hidden, + 11: &t.comment.Hidden, + 12: &t.bpm.Hidden, + 13: &t.bitrate.Hidden, + 14: &t.size.Hidden, + 15: &t.path.Hidden, } t.setColVisibility = func(colNum int, vis bool) bool { ptr, ok := colHiddenPtrMap[colNum] @@ -338,6 +348,7 @@ func (t *tracklistRowBase) create(tracklist *Tracklist) { t.rating.StarSize = 16 t.rating.OnRatingChanged = t.setTrackRating t.plays = util.NewTrailingAlignLabel() + t.lastPlayed = util.NewTruncatingLabel() t.comment = util.NewTruncatingTooltipLabel() t.comment.OnMouseIn = t.MouseIn t.comment.OnMouseOut = t.MouseOut @@ -405,6 +416,7 @@ func (t *tracklistRowBase) doUpdate(tm *util.TrackListModel, rowNum int) { t.dur.Text = util.SecondsToMMSS(tr.Duration.Seconds()) t.year.Text = strconv.Itoa(tr.Year) t.plays.Text = strconv.Itoa(int(tr.PlayCount)) + t.lastPlayed.Text = util.LastPlayedDisplayString(tr.LastPlayed) t.comment.Text = strings.ReplaceAll(tr.Comment, "\n", " ") t.comment.SetToolTip(tr.Comment) t.bpm.Text = strconv.Itoa(tr.BPM)