Merge branch 'main' into feature/new-playlist-btn
This commit is contained in:
@@ -45,8 +45,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Download mpv dll
|
- name: Download mpv dll
|
||||||
run: >
|
run: >
|
||||||
wget https://github.com/shinchiro/mpv-winbuild-cmake/releases/download/20250810/mpv-dev-x86_64-20250810-git-01b7edc.7z &&
|
wget https://github.com/shinchiro/mpv-winbuild-cmake/releases/download/20250921/mpv-dev-x86_64-20250921-git-f147b13.7z &&
|
||||||
7z x mpv-dev-x86_64-20250810-git-01b7edc.7z
|
7z x mpv-dev-x86_64-20250921-git-f147b13.7z
|
||||||
|
|
||||||
- name: Download smtc dll
|
- name: Download smtc dll
|
||||||
run: >
|
run: >
|
||||||
|
|||||||
@@ -58,9 +58,7 @@ func init() {
|
|||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
flag.Func("volume-adjust-pct", "adjusts volume up or down by the given percentage (positive or negative)", func(s string) error {
|
flag.Func("volume-adjust-pct", "adjusts volume up or down by the given percentage (positive or negative)", func(s string) error {
|
||||||
if strings.HasSuffix(s, "%") {
|
s = strings.TrimSuffix(s, "%")
|
||||||
s = s[:len(s)-1]
|
|
||||||
}
|
|
||||||
v, err := strconv.ParseFloat(s, 64)
|
v, err := strconv.ParseFloat(s, 64)
|
||||||
VolumePctCLIArg = v
|
VolumePctCLIArg = v
|
||||||
return err
|
return err
|
||||||
|
|||||||
+6
-4
@@ -112,9 +112,11 @@ type NowPlayingPageConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PlaybackConfig struct {
|
type PlaybackConfig struct {
|
||||||
Autoplay bool
|
Autoplay bool
|
||||||
RepeatMode string
|
RepeatMode string
|
||||||
UseWaveformSeekbar bool
|
SkipOneStarWhenShuffling bool
|
||||||
|
SkipKeywordWhenShuffling string
|
||||||
|
UseWaveformSeekbar bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type LocalPlaybackConfig struct {
|
type LocalPlaybackConfig struct {
|
||||||
@@ -317,7 +319,7 @@ func (c *Config) WriteConfigFile(filepath string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
os.WriteFile(filepath, b, 0644)
|
os.WriteFile(filepath, b, 0o644)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,9 +39,7 @@ type ImageCache struct {
|
|||||||
cache map[string]CacheItem
|
cache map[string]CacheItem
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var ErrNotFound = errors.New("item not found")
|
||||||
ErrNotFound = errors.New("item not found")
|
|
||||||
)
|
|
||||||
|
|
||||||
func (i *ImageCache) Init(ctx context.Context, evictionInterval time.Duration) {
|
func (i *ImageCache) Init(ctx context.Context, evictionInterval time.Duration) {
|
||||||
i.cache = make(map[string]CacheItem)
|
i.cache = make(map[string]CacheItem)
|
||||||
@@ -238,5 +236,4 @@ func (i *ImageCache) EvictExpired() {
|
|||||||
delete(i.cache, key)
|
delete(i.cache, key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ type ImageManager struct {
|
|||||||
maxOnDiskCacheSizeBytes int64
|
maxOnDiskCacheSizeBytes int64
|
||||||
filesWrittenSinceLastPrune bool
|
filesWrittenSinceLastPrune bool
|
||||||
|
|
||||||
serverFetchSema chan interface{}
|
serverFetchSema chan any
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewImageManager returns a new ImageManager.
|
// NewImageManager returns a new ImageManager.
|
||||||
@@ -64,7 +64,7 @@ func NewImageManager(ctx context.Context, s *ServerManager, baseCacheDir string)
|
|||||||
DefaultTTL: 1 * time.Minute,
|
DefaultTTL: 1 * time.Minute,
|
||||||
},
|
},
|
||||||
maxOnDiskCacheSizeBytes: defaultDiskCacheSizeBytes,
|
maxOnDiskCacheSizeBytes: defaultDiskCacheSizeBytes,
|
||||||
serverFetchSema: make(chan interface{}, maxConcurrentServerFetches),
|
serverFetchSema: make(chan any, maxConcurrentServerFetches),
|
||||||
}
|
}
|
||||||
s.OnLogout(func() {
|
s.OnLogout(func() {
|
||||||
i.thumbnailCache.Clear()
|
i.thumbnailCache.Clear()
|
||||||
|
|||||||
@@ -130,7 +130,6 @@ func (c *Client) Quit() error {
|
|||||||
|
|
||||||
func (c *Client) sendRequest(path string) (string, error) {
|
func (c *Client) sendRequest(path string) (string, error) {
|
||||||
resp, err := c.httpC.Get("http://supersonic/" + path)
|
resp, err := c.httpC.Get("http://supersonic/" + path)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ func (s *serverImpl) createHandler() http.Handler {
|
|||||||
search = strings.ToLower(search)
|
search = strings.ToLower(search)
|
||||||
|
|
||||||
filtered := make([]mediaprovider.Playlist, 0)
|
filtered := make([]mediaprovider.Playlist, 0)
|
||||||
for i := 0; i < len(all); i++ {
|
for i := range all {
|
||||||
playlist := all[i]
|
playlist := all[i]
|
||||||
name := strings.ReplaceAll(playlist.Name, " ", "")
|
name := strings.ReplaceAll(playlist.Name, " ", "")
|
||||||
name = strings.ToLower(name)
|
name = strings.ToLower(name)
|
||||||
|
|||||||
+18
-2
@@ -80,10 +80,26 @@ func (l *LrcLibFetcher) fetchFromServer(name, artist, album string, durationSecs
|
|||||||
req.Header.Add("Accept", "application/json")
|
req.Header.Add("Accept", "application/json")
|
||||||
req.Header.Add("User-Agent", "Supersonic")
|
req.Header.Add("User-Agent", "Supersonic")
|
||||||
|
|
||||||
|
// Navidrome and Gonic substitute "[Unknown Album]" and "Unknown Album", respectively,
|
||||||
|
// for an empty album name. This will break LrcLib matching.
|
||||||
|
// TODO: if OpenSubsonic later clarifies that servers should not do this, remove this workaround.
|
||||||
|
// N.B.: This workaround will break if servers decide to internationalize the default album name
|
||||||
|
if strings.Contains(album, "Unknown Album") {
|
||||||
|
album = ""
|
||||||
|
}
|
||||||
|
if strings.Contains(artist, "Unknown Artist") {
|
||||||
|
artist = ""
|
||||||
|
}
|
||||||
|
|
||||||
q := req.URL.Query()
|
q := req.URL.Query()
|
||||||
|
addIf := func(key, value string) {
|
||||||
|
if value != "" {
|
||||||
|
q.Add(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
q.Add("track_name", name)
|
q.Add("track_name", name)
|
||||||
q.Add("artist_name", artist)
|
addIf("artist_name", artist)
|
||||||
q.Add("album_name", album)
|
addIf("album_name", album)
|
||||||
q.Add("duration", strconv.Itoa(durationSecs))
|
q.Add("duration", strconv.Itoa(durationSecs))
|
||||||
req.URL.RawQuery = q.Encode()
|
req.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
|||||||
@@ -348,7 +348,7 @@ func (j *jellyfinMediaProvider) SetFavorite(params mediaprovider.RatingFavoriteP
|
|||||||
}
|
}
|
||||||
|
|
||||||
numBatches := int(math.Ceil(float64(len(allIDs)) / float64(batchSize)))
|
numBatches := int(math.Ceil(float64(len(allIDs)) / float64(batchSize)))
|
||||||
for i := 0; i < numBatches; i++ {
|
for i := range numBatches {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
batchSetFavorite(i*batchSize, &wg)
|
batchSetFavorite(i*batchSize, &wg)
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
@@ -434,6 +434,8 @@ func toTrack(ch *jellyfin.Song) *mediaprovider.Track {
|
|||||||
coverArtID = ch.Id
|
coverArtID = ch.Id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lastPlayed, _ := time.Parse(time.RFC3339Nano, ch.UserData.LastPlayedDate)
|
||||||
|
|
||||||
t := &mediaprovider.Track{
|
t := &mediaprovider.Track{
|
||||||
ID: ch.Id,
|
ID: ch.Id,
|
||||||
CoverArtID: coverArtID,
|
CoverArtID: coverArtID,
|
||||||
@@ -442,7 +444,7 @@ func toTrack(ch *jellyfin.Song) *mediaprovider.Track {
|
|||||||
Duration: time.Duration(ch.RunTimeTicks/runTimeTicksPerMicrosecond) * time.Microsecond,
|
Duration: time.Duration(ch.RunTimeTicks/runTimeTicksPerMicrosecond) * time.Microsecond,
|
||||||
TrackNumber: ch.IndexNumber,
|
TrackNumber: ch.IndexNumber,
|
||||||
DiscNumber: ch.DiscNumber,
|
DiscNumber: ch.DiscNumber,
|
||||||
//Genre: ch.Genres,
|
// Genre: ch.Genres,
|
||||||
ArtistIDs: artistIDs,
|
ArtistIDs: artistIDs,
|
||||||
ArtistNames: artistNames,
|
ArtistNames: artistNames,
|
||||||
Album: ch.Album,
|
Album: ch.Album,
|
||||||
@@ -451,6 +453,7 @@ func toTrack(ch *jellyfin.Song) *mediaprovider.Track {
|
|||||||
Rating: ch.UserData.Rating,
|
Rating: ch.UserData.Rating,
|
||||||
Favorite: ch.UserData.IsFavorite,
|
Favorite: ch.UserData.IsFavorite,
|
||||||
PlayCount: ch.UserData.PlayCount,
|
PlayCount: ch.UserData.PlayCount,
|
||||||
|
LastPlayed: lastPlayed,
|
||||||
}
|
}
|
||||||
if len(ch.MediaSources) > 0 {
|
if len(ch.MediaSources) > 0 {
|
||||||
t.FilePath = ch.MediaSources[0].Path
|
t.FilePath = ch.MediaSources[0].Path
|
||||||
|
|||||||
@@ -32,9 +32,11 @@ type MediaIterator[M any] interface {
|
|||||||
Next() *M
|
Next() *M
|
||||||
}
|
}
|
||||||
|
|
||||||
type ArtistIterator = MediaIterator[Artist]
|
type (
|
||||||
type AlbumIterator = MediaIterator[Album]
|
ArtistIterator = MediaIterator[Artist]
|
||||||
type TrackIterator = MediaIterator[Track]
|
AlbumIterator = MediaIterator[Album]
|
||||||
|
TrackIterator = MediaIterator[Track]
|
||||||
|
)
|
||||||
|
|
||||||
type MediaFilter[M, F any] interface {
|
type MediaFilter[M, F any] interface {
|
||||||
Options() F
|
Options() F
|
||||||
|
|||||||
@@ -22,11 +22,8 @@ func (s *subsonicMediaProvider) ArtistSortOrders() []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func filterArtistMatches(f mediaprovider.ArtistFilter, artist *subsonic.ArtistID3) bool {
|
func filterArtistMatches(_ mediaprovider.ArtistFilter, artist *subsonic.ArtistID3) bool {
|
||||||
if artist == nil {
|
return artist != nil
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *subsonicMediaProvider) IterateArtists(sortOrder string, filter mediaprovider.ArtistFilter) mediaprovider.ArtistIterator {
|
func (s *subsonicMediaProvider) IterateArtists(sortOrder string, filter mediaprovider.ArtistFilter) mediaprovider.ArtistIterator {
|
||||||
@@ -176,9 +173,7 @@ func (s *subsonicMediaProvider) artistFetchFnFromStandardSort(sortFn func([]*sub
|
|||||||
}
|
}
|
||||||
var artists []*subsonic.ArtistID3
|
var artists []*subsonic.ArtistID3
|
||||||
for _, idx := range idxs.Index {
|
for _, idx := range idxs.Index {
|
||||||
for _, ar := range idx.Artist {
|
artists = append(artists, idx.Artist...)
|
||||||
artists = append(artists, ar)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
artists = sortFn(artists)
|
artists = sortFn(artists)
|
||||||
return artists, nil
|
return artists, nil
|
||||||
|
|||||||
@@ -319,7 +319,8 @@ func (s *subsonicMediaProvider) ClientDecidesScrobble() bool { return true }
|
|||||||
func (s *subsonicMediaProvider) TrackBeganPlayback(trackID string) error {
|
func (s *subsonicMediaProvider) TrackBeganPlayback(trackID string) error {
|
||||||
return s.client.Scrobble(trackID, map[string]string{
|
return s.client.Scrobble(trackID, map[string]string{
|
||||||
"time": strconv.FormatInt(time.Now().UnixMilli(), 10),
|
"time": strconv.FormatInt(time.Now().UnixMilli(), 10),
|
||||||
"submission": "false"})
|
"submission": "false",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *subsonicMediaProvider) TrackEndedPlayback(trackID string, _ int, submission bool) error {
|
func (s *subsonicMediaProvider) TrackEndedPlayback(trackID string, _ int, submission bool) error {
|
||||||
@@ -328,7 +329,8 @@ func (s *subsonicMediaProvider) TrackEndedPlayback(trackID string, _ int, submis
|
|||||||
}
|
}
|
||||||
return s.client.Scrobble(trackID, map[string]string{
|
return s.client.Scrobble(trackID, map[string]string{
|
||||||
"time": strconv.FormatInt(time.Now().UnixMilli(), 10),
|
"time": strconv.FormatInt(time.Now().UnixMilli(), 10),
|
||||||
"submission": "true"})
|
"submission": "true",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *subsonicMediaProvider) SetFavorite(params mediaprovider.RatingFavoriteParameters, favorite bool) error {
|
func (s *subsonicMediaProvider) SetFavorite(params mediaprovider.RatingFavoriteParameters, favorite bool) error {
|
||||||
@@ -363,7 +365,7 @@ func (s *subsonicMediaProvider) SetRating(params mediaprovider.RatingFavoritePar
|
|||||||
}
|
}
|
||||||
|
|
||||||
numBatches := int(math.Ceil(float64(len(params.TrackIDs)) / float64(batchSize)))
|
numBatches := int(math.Ceil(float64(len(params.TrackIDs)) / float64(batchSize)))
|
||||||
for i := 0; i < numBatches; i++ {
|
for i := range numBatches {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
batchSetRating(i*batchSize, &wg)
|
batchSetRating(i*batchSize, &wg)
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|||||||
+1
-3
@@ -24,9 +24,7 @@ var (
|
|||||||
_ types.OrgMprisMediaPlayer2PlayerAdapterLoopStatus = (*MPRISHandler)(nil)
|
_ types.OrgMprisMediaPlayer2PlayerAdapterLoopStatus = (*MPRISHandler)(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var errNotSupported = errors.New("not supported")
|
||||||
errNotSupported = errors.New("not supported")
|
|
||||||
)
|
|
||||||
|
|
||||||
type MPRISHandler struct {
|
type MPRISHandler struct {
|
||||||
// Function called if the player is requested to quit through MPRIS.
|
// Function called if the player is requested to quit through MPRIS.
|
||||||
|
|||||||
+33
-19
@@ -11,6 +11,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/charlievieth/strcase"
|
||||||
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||||
"github.com/dweymouth/supersonic/backend/player"
|
"github.com/dweymouth/supersonic/backend/player"
|
||||||
"github.com/dweymouth/supersonic/backend/player/dlna"
|
"github.com/dweymouth/supersonic/backend/player/dlna"
|
||||||
@@ -27,7 +28,8 @@ type PlaybackManager struct {
|
|||||||
wfmGen *WaveformImageGenerator
|
wfmGen *WaveformImageGenerator
|
||||||
cache *AudioCache
|
cache *AudioCache
|
||||||
cmdQueue *playbackCommandQueue
|
cmdQueue *playbackCommandQueue
|
||||||
cfg *AppConfig
|
appCfg *AppConfig
|
||||||
|
cfg *PlaybackConfig
|
||||||
|
|
||||||
localPlayer player.BasePlayer
|
localPlayer player.BasePlayer
|
||||||
remotePlayersLock sync.Mutex
|
remotePlayersLock sync.Mutex
|
||||||
@@ -36,8 +38,6 @@ type PlaybackManager struct {
|
|||||||
|
|
||||||
onWaveformImgUpdate []func(*WaveformImage)
|
onWaveformImgUpdate []func(*WaveformImage)
|
||||||
|
|
||||||
autoplay bool
|
|
||||||
|
|
||||||
lastPlayTime float64
|
lastPlayTime float64
|
||||||
lastPlayingID string
|
lastPlayingID string
|
||||||
wfmUpdateImageCancel context.CancelFunc
|
wfmUpdateImageCancel context.CancelFunc
|
||||||
@@ -66,8 +66,8 @@ func NewPlaybackManager(
|
|||||||
pm := &PlaybackManager{
|
pm := &PlaybackManager{
|
||||||
engine: e,
|
engine: e,
|
||||||
cmdQueue: q,
|
cmdQueue: q,
|
||||||
cfg: appCfg,
|
appCfg: appCfg,
|
||||||
autoplay: playbackCfg.Autoplay,
|
cfg: playbackCfg,
|
||||||
localPlayer: p,
|
localPlayer: p,
|
||||||
cache: c,
|
cache: c,
|
||||||
}
|
}
|
||||||
@@ -117,7 +117,7 @@ func (p *PlaybackManager) addOnTrackChangeHook() {
|
|||||||
|
|
||||||
p.OnSongChange(func(item mediaprovider.MediaItem, _ *mediaprovider.Track) {
|
p.OnSongChange(func(item mediaprovider.MediaItem, _ *mediaprovider.Track) {
|
||||||
// Autoplay if enabled and we are on the last track
|
// Autoplay if enabled and we are on the last track
|
||||||
if p.autoplay && p.NowPlayingIndex() == len(p.engine.playQueue)-1 {
|
if p.cfg.Autoplay && p.NowPlayingIndex() == len(p.engine.playQueue)-1 {
|
||||||
p.enqueueAutoplayTracks()
|
p.enqueueAutoplayTracks()
|
||||||
}
|
}
|
||||||
p.handleWaveformImageSongChange(item)
|
p.handleWaveformImageSongChange(item)
|
||||||
@@ -470,13 +470,23 @@ func (p *PlaybackManager) PlayTrackAt(idx int) {
|
|||||||
|
|
||||||
func (p *PlaybackManager) PlayRandomSongs(genreName string) error {
|
func (p *PlaybackManager) PlayRandomSongs(genreName string) error {
|
||||||
return p.fetchAndPlayTracks(func() ([]*mediaprovider.Track, error) {
|
return p.fetchAndPlayTracks(func() ([]*mediaprovider.Track, error) {
|
||||||
return p.engine.sm.Server.GetRandomTracks(genreName, p.cfg.EnqueueBatchSize)
|
tr, err := p.engine.sm.Server.GetRandomTracks(genreName, p.appCfg.EnqueueBatchSize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return sharedutil.FilterSlice(tr, func(t *mediaprovider.Track) bool {
|
||||||
|
skipKwd := p.cfg.SkipKeywordWhenShuffling
|
||||||
|
include :=
|
||||||
|
(skipKwd == "" || !strcase.Contains(t.Title, skipKwd)) &&
|
||||||
|
(!p.cfg.SkipOneStarWhenShuffling || t.Rating != 1)
|
||||||
|
return include
|
||||||
|
}), nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PlaybackManager) PlaySimilarSongs(id string) error {
|
func (p *PlaybackManager) PlaySimilarSongs(id string) error {
|
||||||
return p.fetchAndPlayTracks(func() ([]*mediaprovider.Track, error) {
|
return p.fetchAndPlayTracks(func() ([]*mediaprovider.Track, error) {
|
||||||
return p.engine.sm.Server.GetSimilarTracks(id, p.cfg.EnqueueBatchSize)
|
return p.engine.sm.Server.GetSimilarTracks(id, p.appCfg.EnqueueBatchSize)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,7 +508,7 @@ func (p *PlaybackManager) PlayRandomAlbums(genreName string) error {
|
|||||||
}
|
}
|
||||||
iter := mp.IterateAlbums(mediaprovider.AlbumSortRandom, mediaprovider.NewAlbumFilter(options))
|
iter := mp.IterateAlbums(mediaprovider.AlbumSortRandom, mediaprovider.NewAlbumFilter(options))
|
||||||
insertMode := Replace
|
insertMode := Replace
|
||||||
for i := 0; i < 20; i++ {
|
for i := range 20 {
|
||||||
al := iter.Next()
|
al := iter.Next()
|
||||||
if al == nil {
|
if al == nil {
|
||||||
break
|
break
|
||||||
@@ -592,7 +602,7 @@ func (p *PlaybackManager) GetLoopMode() LoopMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *PlaybackManager) IsAutoplay() bool {
|
func (p *PlaybackManager) IsAutoplay() bool {
|
||||||
return p.autoplay
|
return p.cfg.Autoplay
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PlaybackManager) PlaybackStatus() PlaybackStatus {
|
func (p *PlaybackManager) PlaybackStatus() PlaybackStatus {
|
||||||
@@ -604,7 +614,7 @@ func (p *PlaybackManager) SetVolume(vol int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *PlaybackManager) SetAutoplay(autoplay bool) {
|
func (p *PlaybackManager) SetAutoplay(autoplay bool) {
|
||||||
p.autoplay = autoplay
|
p.cfg.Autoplay = autoplay
|
||||||
if autoplay && p.NowPlayingIndex() == len(p.engine.playQueue)-1 {
|
if autoplay && p.NowPlayingIndex() == len(p.engine.playQueue)-1 {
|
||||||
p.enqueueAutoplayTracks()
|
p.enqueueAutoplayTracks()
|
||||||
}
|
}
|
||||||
@@ -705,11 +715,15 @@ func (p *PlaybackManager) enqueueAutoplayTracks() {
|
|||||||
// tracks we will enqueue
|
// tracks we will enqueue
|
||||||
var tracks []*mediaprovider.Track
|
var tracks []*mediaprovider.Track
|
||||||
|
|
||||||
filterRecentlyPlayed := func(tracks []*mediaprovider.Track) []*mediaprovider.Track {
|
filterAutoplayTracks := func(tracks []*mediaprovider.Track) []*mediaprovider.Track {
|
||||||
return sharedutil.FilterSlice(tracks, func(t *mediaprovider.Track) bool {
|
return sharedutil.FilterSlice(tracks, func(t *mediaprovider.Track) bool {
|
||||||
return !slices.ContainsFunc(queue, func(i mediaprovider.MediaItem) bool {
|
shouldSkip :=
|
||||||
|
(p.cfg.SkipOneStarWhenShuffling && t.Rating == 1) ||
|
||||||
|
(p.cfg.SkipKeywordWhenShuffling != "" && strcase.Contains(t.Title, p.cfg.SkipKeywordWhenShuffling))
|
||||||
|
recentlyPlayed := slices.ContainsFunc(queue, func(i mediaprovider.MediaItem) bool {
|
||||||
return i.Metadata().Type == mediaprovider.MediaItemTypeTrack && i.Metadata().ID == t.ID
|
return i.Metadata().Type == mediaprovider.MediaItemTypeTrack && i.Metadata().ID == t.ID
|
||||||
})
|
})
|
||||||
|
return !shouldSkip && !recentlyPlayed
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -722,11 +736,11 @@ func (p *PlaybackManager) enqueueAutoplayTracks() {
|
|||||||
|
|
||||||
// similar tracks by artist
|
// similar tracks by artist
|
||||||
if len(tr.ArtistIDs) > 0 {
|
if len(tr.ArtistIDs) > 0 {
|
||||||
similar, err := s.GetSimilarTracks(tr.ArtistIDs[0], p.cfg.EnqueueBatchSize)
|
similar, err := s.GetSimilarTracks(tr.ArtistIDs[0], p.appCfg.EnqueueBatchSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("autoplay error: failed to get similar tracks: %v", err)
|
log.Printf("autoplay error: failed to get similar tracks: %v", err)
|
||||||
}
|
}
|
||||||
tracks = filterRecentlyPlayed(similar)
|
tracks = filterAutoplayTracks(similar)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fallback to random tracks from genre
|
// fallback to random tracks from genre
|
||||||
@@ -735,11 +749,11 @@ func (p *PlaybackManager) enqueueAutoplayTracks() {
|
|||||||
if g == "" {
|
if g == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
byGenre, err := s.GetRandomTracks(g, p.cfg.EnqueueBatchSize)
|
byGenre, err := s.GetRandomTracks(g, p.appCfg.EnqueueBatchSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("autoplay error: failed to get tracks by genre: %v", err)
|
log.Printf("autoplay error: failed to get tracks by genre: %v", err)
|
||||||
}
|
}
|
||||||
tracks = filterRecentlyPlayed(byGenre)
|
tracks = filterAutoplayTracks(byGenre)
|
||||||
if len(tracks) > 0 {
|
if len(tracks) > 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -750,11 +764,11 @@ func (p *PlaybackManager) enqueueAutoplayTracks() {
|
|||||||
// random tracks works regardless of the type of the last playing media
|
// random tracks works regardless of the type of the last playing media
|
||||||
if len(tracks) == 0 {
|
if len(tracks) == 0 {
|
||||||
// fallback to random tracks
|
// fallback to random tracks
|
||||||
random, err := s.GetRandomTracks("", p.cfg.EnqueueBatchSize)
|
random, err := s.GetRandomTracks("", p.appCfg.EnqueueBatchSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("autoplay error: failed to get random tracks: %v", err)
|
log.Printf("autoplay error: failed to get random tracks: %v", err)
|
||||||
}
|
}
|
||||||
tracks = filterRecentlyPlayed(random)
|
tracks = filterAutoplayTracks(random)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(tracks) > 0 {
|
if len(tracks) > 0 {
|
||||||
|
|||||||
@@ -618,7 +618,7 @@ func (d *DLNAPlayer) lookupProxyURL(key string) (string, bool) {
|
|||||||
d.proxyURLLock.Lock()
|
d.proxyURLLock.Lock()
|
||||||
defer d.proxyURLLock.Unlock()
|
defer d.proxyURLLock.Unlock()
|
||||||
|
|
||||||
for i := 0; i < len(d.proxyURLs); i++ {
|
for i := range len(d.proxyURLs) {
|
||||||
if d.proxyURLs[i].key == key {
|
if d.proxyURLs[i].key == key {
|
||||||
url := d.proxyURLs[i].url
|
url := d.proxyURLs[i].url
|
||||||
// Move accessed entry to the most recent position
|
// Move accessed entry to the most recent position
|
||||||
@@ -632,7 +632,7 @@ func (d *DLNAPlayer) lookupProxyURL(key string) (string, bool) {
|
|||||||
|
|
||||||
func (d *DLNAPlayer) _updateProxyURL(key, url string) {
|
func (d *DLNAPlayer) _updateProxyURL(key, url string) {
|
||||||
// Check if the key already exists, and if so, move it to the most recently used position
|
// Check if the key already exists, and if so, move it to the most recently used position
|
||||||
for i := 0; i < len(d.proxyURLs); i++ {
|
for i := range len(d.proxyURLs) {
|
||||||
if d.proxyURLs[i].key == key {
|
if d.proxyURLs[i].key == key {
|
||||||
if i < len(d.proxyURLs)-1 {
|
if i < len(d.proxyURLs)-1 {
|
||||||
// Shift elements to the left from found position to the end
|
// Shift elements to the left from found position to the end
|
||||||
@@ -652,19 +652,19 @@ func (d *DLNAPlayer) _updateProxyURL(key, url string) {
|
|||||||
|
|
||||||
type retryLogger struct{}
|
type retryLogger struct{}
|
||||||
|
|
||||||
func (retryLogger) Error(msg string, keysAndValues ...interface{}) {
|
func (retryLogger) Error(msg string, keysAndValues ...any) {
|
||||||
log.Println(msg, keysAndValues)
|
log.Println(msg, keysAndValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (retryLogger) Info(msg string, keysAndValues ...interface{}) {
|
func (retryLogger) Info(msg string, keysAndValues ...any) {
|
||||||
log.Println(msg, keysAndValues)
|
log.Println(msg, keysAndValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (retryLogger) Warn(msg string, keysAndValues ...interface{}) {
|
func (retryLogger) Warn(msg string, keysAndValues ...any) {
|
||||||
log.Println(msg, keysAndValues)
|
log.Println(msg, keysAndValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (retryLogger) Debug(msg string, keysAndValues ...interface{}) {
|
func (retryLogger) Debug(msg string, keysAndValues ...any) {
|
||||||
// log only retries, not every request
|
// log only retries, not every request
|
||||||
if strings.Contains(msg, "retrying request") {
|
if strings.Contains(msg, "retrying request") {
|
||||||
log.Println(msg, keysAndValues)
|
log.Println(msg, keysAndValues)
|
||||||
|
|||||||
@@ -109,7 +109,6 @@ func (j *JukeboxPlayer) SetNextTrack(track *mediaprovider.Track) error {
|
|||||||
}
|
}
|
||||||
j.queueLength += 1
|
j.queueLength += 1
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *JukeboxPlayer) SeekSeconds(secs float64) error {
|
func (j *JukeboxPlayer) SeekSeconds(secs float64) error {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package mpv
|
|||||||
// #include <mpv/client.h>
|
// #include <mpv/client.h>
|
||||||
// int mpv_get_peaks(mpv_handle* handle, double* lPeak, double* rPeak, double* lRMS, double* rRMS);
|
// int mpv_get_peaks(mpv_handle* handle, double* lPeak, double* rPeak, double* lRMS, double* rRMS);
|
||||||
import "C"
|
import "C"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/supersonic-app/go-mpv"
|
"github.com/supersonic-app/go-mpv"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -492,7 +492,7 @@ func (p *Player) eventHandler(ctx context.Context) {
|
|||||||
default:
|
default:
|
||||||
e := p.mpv.WaitEvent(1 /*timeout seconds*/)
|
e := p.mpv.WaitEvent(1 /*timeout seconds*/)
|
||||||
if e.Event_Id != mpv.EVENT_NONE {
|
if e.Event_Id != mpv.EVENT_NONE {
|
||||||
//log.Printf("mpv event: %+v\n", e)
|
// log.Printf("mpv event: %+v\n", e)
|
||||||
}
|
}
|
||||||
switch e.Event_Id {
|
switch e.Event_Id {
|
||||||
case mpv.EVENT_PLAYBACK_RESTART:
|
case mpv.EVENT_PLAYBACK_RESTART:
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func SavePlayQueue(serverID string, pm *PlaybackManager, filepath string, server
|
|||||||
TimePos: stats.TimePos,
|
TimePos: stats.TimePos,
|
||||||
}
|
}
|
||||||
b, _ := json.Marshal(saved)
|
b, _ := json.Marshal(saved)
|
||||||
err := os.WriteFile(filepath, b, 0644)
|
err := os.WriteFile(filepath, b, 0o644)
|
||||||
|
|
||||||
if server != nil {
|
if server != nil {
|
||||||
// save to server
|
// save to server
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ func (w *WaveformImageJob) Get() *WaveformImage {
|
|||||||
result := NewWaveformImage()
|
result := NewWaveformImage()
|
||||||
|
|
||||||
// Copy each scanline from w.img to result
|
// Copy each scanline from w.img to result
|
||||||
for y := 0; y < height; y++ {
|
for y := range height {
|
||||||
srcOffset := w.img.PixOffset(0, y)
|
srcOffset := w.img.PixOffset(0, y)
|
||||||
dstOffset := result.PixOffset(0, y)
|
dstOffset := result.PixOffset(0, y)
|
||||||
copy(result.Pix[dstOffset:dstOffset+w.progress*4], w.img.Pix[srcOffset:srcOffset+w.progress*4])
|
copy(result.Pix[dstOffset:dstOffset+w.progress*4], w.img.Pix[srcOffset:srcOffset+w.progress*4])
|
||||||
@@ -226,7 +226,7 @@ func generateWaveformImage(ctx context.Context, data *waveformData, job *Wavefor
|
|||||||
opaqueColor := color.NRGBA{R: 255, G: 255, B: 255, A: 255}
|
opaqueColor := color.NRGBA{R: 255, G: 255, B: 255, A: 255}
|
||||||
translucentColor := color.NRGBA{R: 255, G: 255, B: 255, A: 128}
|
translucentColor := color.NRGBA{R: 255, G: 255, B: 255, A: 128}
|
||||||
|
|
||||||
for x := 0; x < 1024; x++ {
|
for x := range 1024 {
|
||||||
for data.progress <= x {
|
for data.progress <= x {
|
||||||
if data.done {
|
if data.done {
|
||||||
return
|
return
|
||||||
@@ -308,7 +308,7 @@ func analyzeWavFile(ctx context.Context, transcodeFile string, data *waveformDat
|
|||||||
currentSize := stat.Size()
|
currentSize := stat.Size()
|
||||||
|
|
||||||
// how many bytes can we read without nearing EOF
|
// how many bytes can we read without nearing EOF
|
||||||
readableBytes := currentSize - int64(samplesPerChunk)*int64(curChunk)*bytesPerSample - 8192 //buffer for safety
|
readableBytes := currentSize - int64(samplesPerChunk)*int64(curChunk)*bytesPerSample - 8192 // buffer for safety
|
||||||
|
|
||||||
// Estimate how many samples we can read
|
// Estimate how many samples we can read
|
||||||
maxSamples := int(readableBytes / bytesPerSample)
|
maxSamples := int(readableBytes / bytesPerSample)
|
||||||
@@ -341,7 +341,7 @@ func analyzeWavFile(ctx context.Context, transcodeFile string, data *waveformDat
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Process samples
|
// Process samples
|
||||||
for i := 0; i < n; i++ {
|
for i := range n {
|
||||||
sample := float64(buf.Data[i]) / float64(1<<15) // Normalize to [-1, 1]
|
sample := float64(buf.Data[i]) / float64(1<<15) // Normalize to [-1, 1]
|
||||||
chunkSamples = append(chunkSamples, sample)
|
chunkSamples = append(chunkSamples, sample)
|
||||||
|
|
||||||
@@ -391,7 +391,7 @@ func computePeakAndRMS(chunk []float64) (peak float64, rms float64) {
|
|||||||
sumSquares += float64(v * v)
|
sumSquares += float64(v * v)
|
||||||
}
|
}
|
||||||
rms = math.Sqrt(sumSquares / float64(len(chunk)))
|
rms = math.Sqrt(sumSquares / float64(len(chunk)))
|
||||||
return
|
return peak, rms
|
||||||
}
|
}
|
||||||
|
|
||||||
func float64ToByte(val float64) byte {
|
func float64ToByte(val float64) byte {
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ import (
|
|||||||
"golang.org/x/sys/windows"
|
"golang.org/x/sys/windows"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SMTCPlaybackState int
|
type (
|
||||||
type SMTCButton int
|
SMTCPlaybackState int
|
||||||
|
SMTCButton int
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// constants from smtc.h in github.com/supersonic-app/smtc-dll
|
// constants from smtc.h in github.com/supersonic-app/smtc-dll
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ package windows
|
|||||||
|
|
||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
type SMTCPlaybackState int
|
type (
|
||||||
type SMTCButton int
|
SMTCPlaybackState int
|
||||||
|
SMTCButton int
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// constants from smtc.h in github.com/supersonic-app/smtc-dll
|
// constants from smtc.h in github.com/supersonic-app/smtc-dll
|
||||||
@@ -22,18 +24,18 @@ const (
|
|||||||
|
|
||||||
type SMTC struct{}
|
type SMTC struct{}
|
||||||
|
|
||||||
var smtcUnsupportedErr = errors.New("SMTC is not supported on this platformo")
|
var errSMTCUnsupported = errors.New("no support for SMTC on this platform")
|
||||||
|
|
||||||
func InitSMTCForWindow(hwnd uintptr) (*SMTC, error) {
|
func InitSMTCForWindow(hwnd uintptr) (*SMTC, error) {
|
||||||
return nil, smtcUnsupportedErr
|
return nil, errSMTCUnsupported
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SMTC) SetEnabled(enabled bool) error {
|
func (s *SMTC) SetEnabled(enabled bool) error {
|
||||||
return smtcUnsupportedErr
|
return errSMTCUnsupported
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SMTC) SetThumbnail(filepath string) error {
|
func (s *SMTC) SetThumbnail(filepath string) error {
|
||||||
return smtcUnsupportedErr
|
return errSMTCUnsupported
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SMTC) OnButtonPressed(func(SMTCButton)) {}
|
func (s *SMTC) OnButtonPressed(func(SMTCButton)) {}
|
||||||
@@ -43,13 +45,13 @@ func (s *SMTC) OnSeek(f func(millis int)) {}
|
|||||||
func (s *SMTC) Shutdown() {}
|
func (s *SMTC) Shutdown() {}
|
||||||
|
|
||||||
func (s *SMTC) UpdatePlaybackState(state SMTCPlaybackState) error {
|
func (s *SMTC) UpdatePlaybackState(state SMTCPlaybackState) error {
|
||||||
return smtcUnsupportedErr
|
return errSMTCUnsupported
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SMTC) UpdateMetadata(title, artist string) error {
|
func (s *SMTC) UpdateMetadata(title, artist string) error {
|
||||||
return smtcUnsupportedErr
|
return errSMTCUnsupported
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SMTC) UpdatePosition(positionMillis, durationMillis int) error {
|
func (s *SMTC) UpdatePosition(positionMillis, durationMillis int) error {
|
||||||
return smtcUnsupportedErr
|
return errSMTCUnsupported
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ package windows
|
|||||||
extern void goButtonClicked(int);
|
extern void goButtonClicked(int);
|
||||||
*/
|
*/
|
||||||
import "C"
|
import "C"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"image"
|
"image"
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ require (
|
|||||||
al.essio.dev/pkg/shellescape v1.5.1 // indirect
|
al.essio.dev/pkg/shellescape v1.5.1 // indirect
|
||||||
fyne.io/systray v1.11.0 // indirect
|
fyne.io/systray v1.11.0 // indirect
|
||||||
github.com/BurntSushi/toml v1.4.0 // indirect
|
github.com/BurntSushi/toml v1.4.0 // indirect
|
||||||
|
github.com/charlievieth/strcase v0.0.5 // indirect
|
||||||
github.com/danieljoos/wincred v1.2.2 // indirect
|
github.com/danieljoos/wincred v1.2.2 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/fogleman/gg v1.3.0 // indirect
|
github.com/fogleman/gg v1.3.0 // indirect
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
|
|||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/cenkalti/dominantcolor v1.0.3 h1:Pt0vfRZ8enkZh1n22RvoboA53SMM/v2aEwNQTZKSqww=
|
github.com/cenkalti/dominantcolor v1.0.3 h1:Pt0vfRZ8enkZh1n22RvoboA53SMM/v2aEwNQTZKSqww=
|
||||||
github.com/cenkalti/dominantcolor v1.0.3/go.mod h1:mGpFMbWUnyXaGN48Zbf9bU9HJP1eCCD7dnsscb4lyR4=
|
github.com/cenkalti/dominantcolor v1.0.3/go.mod h1:mGpFMbWUnyXaGN48Zbf9bU9HJP1eCCD7dnsscb4lyR4=
|
||||||
|
github.com/charlievieth/strcase v0.0.5 h1:gV4iXVyD6eI5KdfOV+/vIVCKXZwtCWOmDMcu7Uy00Rs=
|
||||||
|
github.com/charlievieth/strcase v0.0.5/go.mod h1:FIOYY1aDBMSIOFqmVomHBpoK+bteGlESRsgsdWjrhx8=
|
||||||
github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
|
github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
|
||||||
github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
|
github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
|||||||
@@ -132,6 +132,7 @@
|
|||||||
"My Server": "My Server",
|
"My Server": "My Server",
|
||||||
"Name": "Name",
|
"Name": "Name",
|
||||||
"Name (A-Z)": "Name (A-Z)",
|
"Name (A-Z)": "Name (A-Z)",
|
||||||
|
"never": "never",
|
||||||
"New Playlist": "New Playlist",
|
"New Playlist": "New Playlist",
|
||||||
"Next": "Next",
|
"Next": "Next",
|
||||||
"Nickname": "Nickname",
|
"Nickname": "Nickname",
|
||||||
@@ -213,8 +214,10 @@
|
|||||||
"Singles": "Singles",
|
"Singles": "Singles",
|
||||||
"Size": "Size",
|
"Size": "Size",
|
||||||
"Skip duplicate tracks": "Skip duplicate tracks",
|
"Skip duplicate tracks": "Skip duplicate tracks",
|
||||||
|
"Skip one-star tracks": "Skip one-star tracks",
|
||||||
"Skip SSL certificate verification": "Skip SSL certificate verification",
|
"Skip SSL certificate verification": "Skip SSL certificate verification",
|
||||||
"Skip this version": "Skip this version",
|
"Skip this version": "Skip this version",
|
||||||
|
"Skip tracks with keyword": "Skip tracks with keyword",
|
||||||
"Smaller": "Smaller",
|
"Smaller": "Smaller",
|
||||||
"Sort": "Sort",
|
"Sort": "Sort",
|
||||||
"Soundtrack": "Soundtrack",
|
"Soundtrack": "Soundtrack",
|
||||||
@@ -254,6 +257,7 @@
|
|||||||
"version": "version",
|
"version": "version",
|
||||||
"Visualizations": "Visualizations",
|
"Visualizations": "Visualizations",
|
||||||
"Volume": "Volume",
|
"Volume": "Volume",
|
||||||
|
"When enqueuing random": "When enqueuing random",
|
||||||
"wrong URL": "wrong URL",
|
"wrong URL": "wrong URL",
|
||||||
"wrong username/password": "wrong username/password",
|
"wrong username/password": "wrong username/password",
|
||||||
"Year": "Year",
|
"Year": "Year",
|
||||||
@@ -289,5 +293,26 @@
|
|||||||
"playlist.addedtracks": {
|
"playlist.addedtracks": {
|
||||||
"one": "Added one track to playlist",
|
"one": "Added one track to playlist",
|
||||||
"other": "Added {{.trackCount}} tracks 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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,6 +114,7 @@
|
|||||||
"My Server": "Mi servidor",
|
"My Server": "Mi servidor",
|
||||||
"Name": "Nombre",
|
"Name": "Nombre",
|
||||||
"Name (A-Z)": "Nombre (A-Z)",
|
"Name (A-Z)": "Nombre (A-Z)",
|
||||||
|
"never": "nunca",
|
||||||
"New Playlist": "Crear Nueva",
|
"New Playlist": "Crear Nueva",
|
||||||
"Next": "Siguiente",
|
"Next": "Siguiente",
|
||||||
"Nickname": "Apodo",
|
"Nickname": "Apodo",
|
||||||
@@ -255,5 +256,26 @@
|
|||||||
"playlist.addedtracks": {
|
"playlist.addedtracks": {
|
||||||
"one": "Se ha añadido una pista a la lista de reproducción",
|
"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"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
package wintaskbarthumbs
|
package wintaskbarthumbs
|
||||||
|
|
||||||
import _ "embed"
|
import _ "embed"
|
||||||
|
|
||||||
//go:embed media_pause.png
|
//go:embed media_pause.png
|
||||||
var MediaPausePNG []byte
|
var MediaPausePNG []byte
|
||||||
|
|
||||||
//go:embed media_play.png
|
//go:embed media_play.png
|
||||||
var MediaPlayPNG []byte
|
var MediaPlayPNG []byte
|
||||||
|
|
||||||
//go:embed media_seek_next.png
|
//go:embed media_seek_next.png
|
||||||
var MediaSeekNextPNG []byte
|
var MediaSeekNextPNG []byte
|
||||||
|
|
||||||
//go:embed media_seek_previous.png
|
//go:embed media_seek_previous.png
|
||||||
var MediaSeekPreviousPNG []byte
|
var MediaSeekPreviousPNG []byte
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Test_ReorderItems(t *testing.T) {
|
func Test_ReorderItems(t *testing.T) {
|
||||||
|
|
||||||
tracks := []*mediaprovider.Track{
|
tracks := []*mediaprovider.Track{
|
||||||
{ID: "a"}, // 0
|
{ID: "a"}, // 0
|
||||||
{ID: "b"}, // 1
|
{ID: "b"}, // 1
|
||||||
|
|||||||
@@ -62,10 +62,7 @@ func (a *albumsPageAdapter) Route() controller.Route { return controller.AlbumsR
|
|||||||
|
|
||||||
func (a *albumsPageAdapter) SortOrders() ([]string, int) {
|
func (a *albumsPageAdapter) SortOrders() ([]string, int) {
|
||||||
orders := a.mp.AlbumSortOrders()
|
orders := a.mp.AlbumSortOrders()
|
||||||
sortOrder := slices.Index(orders, a.cfg.SortOrder)
|
sortOrder := max(slices.Index(orders, a.cfg.SortOrder), 0)
|
||||||
if sortOrder < 0 {
|
|
||||||
sortOrder = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return util.LocalizeSlice(orders), sortOrder
|
return util.LocalizeSlice(orders), sortOrder
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -486,7 +486,7 @@ type ArtistPageHeader struct {
|
|||||||
menuBtn *widget.Button
|
menuBtn *widget.Button
|
||||||
container *fyne.Container
|
container *fyne.Container
|
||||||
fullSizeCoverFetching bool
|
fullSizeCoverFetching bool
|
||||||
//shareMenuItem *fyne.MenuItem
|
// shareMenuItem *fyne.MenuItem
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewArtistPageHeader(page *ArtistPage) *ArtistPageHeader {
|
func NewArtistPageHeader(page *ArtistPage) *ArtistPageHeader {
|
||||||
|
|||||||
@@ -47,10 +47,7 @@ func (a *artistsPageAdapter) Route() controller.Route { return controller.Artist
|
|||||||
|
|
||||||
func (a *artistsPageAdapter) SortOrders() ([]string, int) {
|
func (a *artistsPageAdapter) SortOrders() ([]string, int) {
|
||||||
orders := a.mp.ArtistSortOrders()
|
orders := a.mp.ArtistSortOrders()
|
||||||
sortOrder := slices.Index(orders, a.cfg.SortOrder)
|
sortOrder := max(slices.Index(orders, a.cfg.SortOrder), 0)
|
||||||
if sortOrder < 0 {
|
|
||||||
sortOrder = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return util.LocalizeSlice(orders), sortOrder
|
return util.LocalizeSlice(orders), sortOrder
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -204,7 +204,8 @@ func NewGenreList(sorting widgets.ListHeaderSort) *GenreList {
|
|||||||
a.hdr = widgets.NewListHeader([]widgets.ListColumn{
|
a.hdr = widgets.NewListHeader([]widgets.ListColumn{
|
||||||
{Text: lang.L("Name"), Alignment: fyne.TextAlignLeading, CanToggleVisible: false},
|
{Text: lang.L("Name"), Alignment: fyne.TextAlignLeading, CanToggleVisible: false},
|
||||||
{Text: albumCount, Alignment: fyne.TextAlignTrailing, CanToggleVisible: false},
|
{Text: albumCount, Alignment: fyne.TextAlignTrailing, CanToggleVisible: false},
|
||||||
{Text: trackCount, Alignment: fyne.TextAlignTrailing, CanToggleVisible: false}},
|
{Text: trackCount, Alignment: fyne.TextAlignTrailing, CanToggleVisible: false},
|
||||||
|
},
|
||||||
a.columnsLayout)
|
a.columnsLayout)
|
||||||
a.hdr.SetSorting(sorting)
|
a.hdr.SetSorting(sorting)
|
||||||
a.hdr.OnColumnSortChanged = a.onSorted
|
a.hdr.OnColumnSortChanged = a.onSorted
|
||||||
@@ -261,7 +262,7 @@ func (g *GenreList) doSortGenres() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch g.sorting.ColNumber {
|
switch g.sorting.ColNumber {
|
||||||
case 0: //Name
|
case 0: // Name
|
||||||
g.stringSort(func(g *mediaprovider.Genre) string { return g.Name })
|
g.stringSort(func(g *mediaprovider.Genre) string { return g.Name })
|
||||||
case 1: // Album Count
|
case 1: // Album Count
|
||||||
g.intSort(func(g *mediaprovider.Genre) int { return g.AlbumCount })
|
g.intSort(func(g *mediaprovider.Genre) int { return g.AlbumCount })
|
||||||
|
|||||||
@@ -360,8 +360,10 @@ func (a *NowPlayingPage) updateLyrics() {
|
|||||||
// set the widget to an empty (not nil) lyric during fetch
|
// set the widget to an empty (not nil) lyric during fetch
|
||||||
// to keep it from showing "Lyrics not available"
|
// to keep it from showing "Lyrics not available"
|
||||||
a.lyricsViewer.DisableTapToSeek()
|
a.lyricsViewer.DisableTapToSeek()
|
||||||
a.lyricsViewer.SetLyrics(&mediaprovider.Lyrics{Synced: true,
|
a.lyricsViewer.SetLyrics(&mediaprovider.Lyrics{
|
||||||
Lines: []mediaprovider.LyricLine{{Text: ""}}})
|
Synced: true,
|
||||||
|
Lines: []mediaprovider.LyricLine{{Text: ""}},
|
||||||
|
})
|
||||||
tr, _ := a.nowPlaying.(*mediaprovider.Track)
|
tr, _ := a.nowPlaying.(*mediaprovider.Track)
|
||||||
go a.fetchLyrics(ctx, tr)
|
go a.fetchLyrics(ctx, tr)
|
||||||
}
|
}
|
||||||
@@ -482,7 +484,6 @@ func (a *NowPlayingPage) OnPlayTimeUpdate(curTime, _ float64, seeked bool) {
|
|||||||
|
|
||||||
func (a *NowPlayingPage) currentTracklistOrNil() *widgets.PlayQueueList {
|
func (a *NowPlayingPage) currentTracklistOrNil() *widgets.PlayQueueList {
|
||||||
if a.tabs != nil {
|
if a.tabs != nil {
|
||||||
|
|
||||||
switch a.tabs.SelectedIndex() {
|
switch a.tabs.SelectedIndex() {
|
||||||
case 0: /*queue*/
|
case 0: /*queue*/
|
||||||
return a.queueList
|
return a.queueList
|
||||||
|
|||||||
@@ -400,7 +400,8 @@ func (p *PlaylistList) buildHeaderAndLayout() {
|
|||||||
{Text: lang.L("Name"), Alignment: fyne.TextAlignLeading, CanToggleVisible: false},
|
{Text: lang.L("Name"), Alignment: fyne.TextAlignLeading, CanToggleVisible: false},
|
||||||
{Text: lang.L("_Description"), Alignment: fyne.TextAlignLeading, CanToggleVisible: false},
|
{Text: lang.L("_Description"), Alignment: fyne.TextAlignLeading, CanToggleVisible: false},
|
||||||
{Text: lang.L("Owner"), Alignment: fyne.TextAlignLeading, CanToggleVisible: false},
|
{Text: lang.L("Owner"), Alignment: fyne.TextAlignLeading, CanToggleVisible: false},
|
||||||
{Text: trackCount, Alignment: fyne.TextAlignTrailing, CanToggleVisible: false}}, p.columnsLayout)
|
{Text: trackCount, Alignment: fyne.TextAlignTrailing, CanToggleVisible: false},
|
||||||
|
}, p.columnsLayout)
|
||||||
p.header.SetSorting(p.sorting)
|
p.header.SetSorting(p.sorting)
|
||||||
p.header.OnColumnSortChanged = p.onSorted
|
p.header.OnColumnSortChanged = p.onSorted
|
||||||
}
|
}
|
||||||
@@ -433,7 +434,7 @@ func (p *PlaylistList) doSortPlaylists() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch p.sorting.ColNumber {
|
switch p.sorting.ColNumber {
|
||||||
case 0: //Name
|
case 0: // Name
|
||||||
p.stringSort(func(p *mediaprovider.Playlist) string { return p.Name })
|
p.stringSort(func(p *mediaprovider.Playlist) string { return p.Name })
|
||||||
case 1: // Description
|
case 1: // Description
|
||||||
p.stringSort(func(p *mediaprovider.Playlist) string { return p.Description })
|
p.stringSort(func(p *mediaprovider.Playlist) string { return p.Description })
|
||||||
|
|||||||
@@ -253,7 +253,8 @@ func NewRadioList(nowPlayingIDPtr *string) *RadioList {
|
|||||||
a.ExtendBaseWidget(a)
|
a.ExtendBaseWidget(a)
|
||||||
a.hdr = widgets.NewListHeader([]widgets.ListColumn{
|
a.hdr = widgets.NewListHeader([]widgets.ListColumn{
|
||||||
{Text: lang.L("Name"), Alignment: fyne.TextAlignLeading, CanToggleVisible: false},
|
{Text: lang.L("Name"), Alignment: fyne.TextAlignLeading, CanToggleVisible: false},
|
||||||
{Text: lang.L("Home Page"), Alignment: fyne.TextAlignLeading, CanToggleVisible: false}},
|
{Text: lang.L("Home Page"), Alignment: fyne.TextAlignLeading, CanToggleVisible: false},
|
||||||
|
},
|
||||||
a.columnsLayout)
|
a.columnsLayout)
|
||||||
a.hdr.DisableSorting = true
|
a.hdr.DisableSorting = true
|
||||||
a.list = widgets.NewFocusList(
|
a.list = widgets.NewFocusList(
|
||||||
@@ -299,9 +300,8 @@ func NewRadioList(nowPlayingIDPtr *string) *RadioList {
|
|||||||
row.IsPlaying = isPlaying
|
row.IsPlaying = isPlaying
|
||||||
row.nameLabel.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = isPlaying
|
row.nameLabel.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = isPlaying
|
||||||
if isPlaying {
|
if isPlaying {
|
||||||
row.Content.(*fyne.Container).Objects[0] =
|
row.Content.(*fyne.Container).Objects[0] = container.NewBorder(nil, nil, a.playingIcon, nil,
|
||||||
container.NewBorder(nil, nil, a.playingIcon, nil,
|
container.New(layout.NewCustomPaddedLayout(0, 0, -5, 0), row.nameLabel))
|
||||||
container.New(layout.NewCustomPaddedLayout(0, 0, -5, 0), row.nameLabel))
|
|
||||||
} else {
|
} else {
|
||||||
row.Content.(*fyne.Container).Objects[0] = row.nameLabel
|
row.Content.(*fyne.Container).Objects[0] = row.nameLabel
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
@@ -309,7 +310,6 @@ func (c *Controller) ShowAboutDialog() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) ShowSettingsDialog(themeUpdateCallbk func(), themeFiles map[string]string) {
|
func (c *Controller) ShowSettingsDialog(themeUpdateCallbk func(), themeFiles map[string]string) {
|
||||||
|
|
||||||
devs, err := c.App.LocalPlayer.ListAudioDevices()
|
devs, err := c.App.LocalPlayer.ListAudioDevices()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error listing audio devices: %v", err)
|
log.Printf("error listing audio devices: %v", err)
|
||||||
@@ -358,7 +358,6 @@ func (c *Controller) ShowSettingsDialog(themeUpdateCallbk func(), themeFiles map
|
|||||||
c.ClosePopUpOnEscape(pop)
|
c.ClosePopUpOnEscape(pop)
|
||||||
c.haveModal = true
|
c.haveModal = true
|
||||||
pop.Show()
|
pop.Show()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) doModalClosed() {
|
func (c *Controller) doModalClosed() {
|
||||||
@@ -467,7 +466,6 @@ func (c *Controller) ShowDownloadDialog(tracks []*mediaprovider.Track, downloadN
|
|||||||
} else {
|
} else {
|
||||||
go c.downloadTracks(tracks, file.URI().Path(), downloadName)
|
go c.downloadTracks(tracks, file.URI().Path(), downloadName)
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
c.MainWindow)
|
c.MainWindow)
|
||||||
dg.SetFileName(fileName)
|
dg.SetFileName(fileName)
|
||||||
@@ -549,7 +547,7 @@ func (c *Controller) sendNotification(title, content string) {
|
|||||||
|
|
||||||
func (c *Controller) showError(content string) {
|
func (c *Controller) showError(content string) {
|
||||||
// TODO: display an in-app toast message instead of a dialog.
|
// TODO: display an in-app toast message instead of a dialog.
|
||||||
dialog.ShowError(fmt.Errorf(content), c.MainWindow)
|
dialog.ShowError(errors.New(content), c.MainWindow)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) ShowAlbumInfoDialog(albumID, albumName string, albumCover image.Image) {
|
func (c *Controller) ShowAlbumInfoDialog(albumID, albumName string, albumCover image.Image) {
|
||||||
|
|||||||
@@ -88,7 +88,6 @@ func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
m.ClosePopUpOnEscape(pop)
|
m.ClosePopUpOnEscape(pop)
|
||||||
m.haveModal = true
|
m.haveModal = true
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ func GenresRoute() Route {
|
|||||||
func PlaylistRoute(id string) Route {
|
func PlaylistRoute(id string) Route {
|
||||||
return Route{Page: Playlist, Arg: id}
|
return Route{Page: Playlist, Arg: id}
|
||||||
}
|
}
|
||||||
|
|
||||||
func PlaylistsRoute() Route {
|
func PlaylistsRoute() Route {
|
||||||
return Route{Page: Playlists}
|
return Route{Page: Playlists}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,7 +200,6 @@ func (m *Controller) PromptForLoginAndConnect() {
|
|||||||
pop.Show()
|
pop.Show()
|
||||||
}
|
}
|
||||||
}, m.MainWindow)
|
}, m.MainWindow)
|
||||||
|
|
||||||
}
|
}
|
||||||
m.haveModal = true
|
m.haveModal = true
|
||||||
pop.Show()
|
pop.Show()
|
||||||
|
|||||||
@@ -374,5 +374,4 @@ func (q *searchEntry) TypedKey(e *fyne.KeyEvent) {
|
|||||||
default:
|
default:
|
||||||
q.SearchEntry.TypedKey(e)
|
q.SearchEntry.TypedKey(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,10 +106,7 @@ func (s *SettingsDialog) createGeneralTab(canSaveQueueToServer bool) *container.
|
|||||||
startupPage = widget.NewSelect(pages, func(_ string) {
|
startupPage = widget.NewSelect(pages, func(_ string) {
|
||||||
s.config.Application.StartupPage = backend.SupportedStartupPages[startupPage.SelectedIndex()]
|
s.config.Application.StartupPage = backend.SupportedStartupPages[startupPage.SelectedIndex()]
|
||||||
})
|
})
|
||||||
initialIdx := slices.Index(backend.SupportedStartupPages, s.config.Application.StartupPage)
|
initialIdx := max(slices.Index(backend.SupportedStartupPages, s.config.Application.StartupPage), 0)
|
||||||
if initialIdx < 0 {
|
|
||||||
initialIdx = 0
|
|
||||||
}
|
|
||||||
startupPage.SetSelectedIndex(initialIdx)
|
startupPage.SetSelectedIndex(initialIdx)
|
||||||
if startupPage.Selected == "" {
|
if startupPage.Selected == "" {
|
||||||
startupPage.SetSelectedIndex(0)
|
startupPage.SetSelectedIndex(0)
|
||||||
@@ -430,12 +427,19 @@ func (s *SettingsDialog) createPlaybackTab(isLocalPlayer, isReplayGainPlayer boo
|
|||||||
disableTranscode,
|
disableTranscode,
|
||||||
container.NewHBox(transcode, transcodeCodec, transcodeBitRate),
|
container.NewHBox(transcode, transcodeCodec, transcodeBitRate),
|
||||||
s.newSectionSeparator(),
|
s.newSectionSeparator(),
|
||||||
widget.NewRichText(&widget.TextSegment{Text: "ReplayGain", Style: util.BoldRichTextStyle}),
|
widget.NewLabelWithStyle("ReplayGain", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||||
container.New(layout.NewFormLayout(),
|
container.New(layout.NewFormLayout(),
|
||||||
widget.NewLabel(lang.L("ReplayGain mode")), container.NewGridWithColumns(2, replayGainSelect),
|
widget.NewLabel(lang.L("ReplayGain mode")), container.NewGridWithColumns(2, replayGainSelect),
|
||||||
widget.NewLabel(lang.L("ReplayGain preamp")), container.NewHBox(preampGain, widget.NewLabel("dB")),
|
widget.NewLabel(lang.L("ReplayGain preamp")), container.NewHBox(preampGain, widget.NewLabel("dB")),
|
||||||
widget.NewLabel(lang.L("Prevent clipping")), preventClipping,
|
widget.NewLabel(lang.L("Prevent clipping")), preventClipping,
|
||||||
),
|
),
|
||||||
|
s.newSectionSeparator(),
|
||||||
|
widget.NewLabelWithStyle(lang.L("When enqueuing random"), fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||||
|
widget.NewCheckWithData(lang.L("Skip one-star tracks"), binding.BindBool(&s.config.Playback.SkipOneStarWhenShuffling)),
|
||||||
|
container.NewBorder(nil, nil,
|
||||||
|
widget.NewLabel(lang.L("Skip tracks with keyword")), nil,
|
||||||
|
widget.NewEntryWithData(binding.BindString(&s.config.Playback.SkipKeywordWhenShuffling)),
|
||||||
|
),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,7 +495,8 @@ func (s *SettingsDialog) createAppearanceTab(window fyne.Window) *container.TabI
|
|||||||
themeModeSelect := widget.NewSelect([]string{
|
themeModeSelect := widget.NewSelect([]string{
|
||||||
string(myTheme.AppearanceDark),
|
string(myTheme.AppearanceDark),
|
||||||
string(myTheme.AppearanceLight),
|
string(myTheme.AppearanceLight),
|
||||||
string(myTheme.AppearanceAuto)}, nil)
|
string(myTheme.AppearanceAuto),
|
||||||
|
}, nil)
|
||||||
themeModeSelect.OnChanged = func(_ string) {
|
themeModeSelect.OnChanged = func(_ string) {
|
||||||
s.config.Theme.Appearance = themeModeSelect.Options[themeModeSelect.SelectedIndex()]
|
s.config.Theme.Appearance = themeModeSelect.Options[themeModeSelect.SelectedIndex()]
|
||||||
if s.OnThemeSettingChanged != nil {
|
if s.OnThemeSettingChanged != nil {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func NewColumnsLayout(widths []float32) *ColumnsLayout {
|
|||||||
func (c *ColumnsLayout) MinSize(objects []fyne.CanvasObject) fyne.Size {
|
func (c *ColumnsLayout) MinSize(objects []fyne.CanvasObject) fyne.Size {
|
||||||
var width float32
|
var width float32
|
||||||
var height float32
|
var height float32
|
||||||
for i := 0; i < len(objects); i++ {
|
for i := range objects {
|
||||||
if !objects[i].Visible() {
|
if !objects[i].Visible() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -56,7 +56,7 @@ func (c *ColumnsLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {
|
|||||||
expandObjW := extraW / float32(expandObjCount)
|
expandObjW := extraW / float32(expandObjCount)
|
||||||
|
|
||||||
var x float32
|
var x float32
|
||||||
for i := 0; i < len(objects); i++ {
|
for i := range objects {
|
||||||
if !objects[i].Visible() {
|
if !objects[i].Visible() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ var (
|
|||||||
ShortcutNavSeven = desktop.CustomShortcut{KeyName: fyne.Key7, Modifier: fyne.KeyModifierShortcutDefault}
|
ShortcutNavSeven = desktop.CustomShortcut{KeyName: fyne.Key7, Modifier: fyne.KeyModifierShortcutDefault}
|
||||||
ShortcutNavEight = desktop.CustomShortcut{KeyName: fyne.Key8, Modifier: fyne.KeyModifierShortcutDefault}
|
ShortcutNavEight = desktop.CustomShortcut{KeyName: fyne.Key8, Modifier: fyne.KeyModifierShortcutDefault}
|
||||||
|
|
||||||
NavShortcuts = []desktop.CustomShortcut{ShortcutNavOne, ShortcutNavTwo, ShortcutNavThree,
|
NavShortcuts = []desktop.CustomShortcut{
|
||||||
ShortcutNavFour, ShortcutNavFive, ShortcutNavSix, ShortcutNavSeven, ShortcutNavEight}
|
ShortcutNavOne, ShortcutNavTwo, ShortcutNavThree,
|
||||||
|
ShortcutNavFour, ShortcutNavFive, ShortcutNavSix, ShortcutNavSeven, ShortcutNavEight,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-4
@@ -341,10 +341,7 @@ func darkenColor(c color.Color, fraction float64) color.Color {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func brightenComponent(component uint32, fraction float64) uint32 {
|
func brightenComponent(component uint32, fraction float64) uint32 {
|
||||||
brightened := component + uint32(float64(component)*fraction)
|
brightened := min(component+uint32(float64(component)*fraction), 0xffff)
|
||||||
if brightened > 0xffff {
|
|
||||||
brightened = 0xffff
|
|
||||||
}
|
|
||||||
return brightened
|
return brightened
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ func (t *ToastOverlay) makeToastAnimFunc(endPos fyne.Position, dismissal bool) f
|
|||||||
t.cancelPreviousToast()
|
t.cancelPreviousToast()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -225,7 +225,7 @@ func colorToHexAndOpacity(color color.Color) (hexStr, aStr string) {
|
|||||||
r, g, b, a := toNRGBA(color)
|
r, g, b, a := toNRGBA(color)
|
||||||
cBytes := []byte{byte(r), byte(g), byte(b)}
|
cBytes := []byte{byte(r), byte(g), byte(b)}
|
||||||
hexStr, aStr = "#"+hex.EncodeToString(cBytes), strconv.FormatFloat(float64(a)/0xff, 'f', 6, 64)
|
hexStr, aStr = "#"+hex.EncodeToString(cBytes), strconv.FormatFloat(float64(a)/0xff, 'f', 6, 64)
|
||||||
return
|
return hexStr, aStr
|
||||||
}
|
}
|
||||||
|
|
||||||
// toNRGBA converts a color to RGBA values which are not premultiplied, unlike color.RGBA().
|
// toNRGBA converts a color to RGBA values which are not premultiplied, unlike color.RGBA().
|
||||||
@@ -300,7 +300,7 @@ func toNRGBA(c color.Color) (r, g, b, a int) {
|
|||||||
default: // RGBA, RGBA64, and unknown implementations of Color
|
default: // RGBA, RGBA64, and unknown implementations of Color
|
||||||
r, g, b, a = unmultiplyAlpha(c)
|
r, g, b, a = unmultiplyAlpha(c)
|
||||||
}
|
}
|
||||||
return
|
return r, g, b, a
|
||||||
}
|
}
|
||||||
|
|
||||||
// unmultiplyAlpha returns a color's RGBA components as 8-bit integers by calling c.RGBA() and then removing the alpha premultiplication.
|
// unmultiplyAlpha returns a color's RGBA components as 8-bit integers by calling c.RGBA() and then removing the alpha premultiplication.
|
||||||
@@ -317,5 +317,5 @@ func unmultiplyAlpha(c color.Color) (r, g, b, a int) {
|
|||||||
g = int(green >> 8)
|
g = int(green >> 8)
|
||||||
b = int(blue >> 8)
|
b = int(blue >> 8)
|
||||||
a = int(alpha >> 8)
|
a = int(alpha >> 8)
|
||||||
return
|
return r, g, b, a
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-1
@@ -55,7 +55,7 @@ func dateFormatForLocale(locale string) DateFormat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func shortMonthName(month int) string {
|
func shortMonthName(month int) string {
|
||||||
var months = [12]string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec"}
|
months := [12]string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec"}
|
||||||
if month >= 1 && month <= 12 {
|
if month >= 1 && month <= 12 {
|
||||||
return lang.L(months[month-1])
|
return lang.L(months[month-1])
|
||||||
}
|
}
|
||||||
@@ -100,6 +100,39 @@ func FormatItemDate(date mediaprovider.ItemDate) string {
|
|||||||
return sb.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}
|
var BoldRichTextStyle = widget.RichTextStyle{TextStyle: fyne.TextStyle{Bold: true}, Inline: true}
|
||||||
|
|
||||||
func MakeOpaque(c color.Color) color.Color {
|
func MakeOpaque(c color.Color) color.Color {
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ func (l *peakMeterRenderer) Refresh() {
|
|||||||
|
|
||||||
func (l *peakMeterRenderer) Objects() []fyne.CanvasObject {
|
func (l *peakMeterRenderer) Objects() []fyne.CanvasObject {
|
||||||
if l.objects == nil {
|
if l.objects == nil {
|
||||||
l.objects = make([]fyne.CanvasObject, 0, 2*len(l.rulerLines) + 8)
|
l.objects = make([]fyne.CanvasObject, 0, 2*len(l.rulerLines)+8)
|
||||||
for i := range l.rulerLines {
|
for i := range l.rulerLines {
|
||||||
l.objects = append(l.objects, &l.rulerLines[i], &l.rulerLabels[i])
|
l.objects = append(l.objects, &l.rulerLines[i], &l.rulerLabels[i])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ type GenreFilterSubsection struct {
|
|||||||
genreList []string
|
genreList []string
|
||||||
onChanged func([]string)
|
onChanged func([]string)
|
||||||
|
|
||||||
selectedGenres map[string]interface{}
|
selectedGenres map[string]any
|
||||||
selectedGenresMutex sync.RWMutex
|
selectedGenresMutex sync.RWMutex
|
||||||
|
|
||||||
filterText *widget.Entry
|
filterText *widget.Entry
|
||||||
@@ -253,7 +253,7 @@ type GenreFilterSubsection struct {
|
|||||||
func NewGenreFilterSubsection(onChanged func([]string), initialSelectedGenres []string) *GenreFilterSubsection {
|
func NewGenreFilterSubsection(onChanged func([]string), initialSelectedGenres []string) *GenreFilterSubsection {
|
||||||
g := &GenreFilterSubsection{
|
g := &GenreFilterSubsection{
|
||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
selectedGenres: make(map[string]interface{}),
|
selectedGenres: make(map[string]any),
|
||||||
}
|
}
|
||||||
g.ExtendBaseWidget(g)
|
g.ExtendBaseWidget(g)
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ func NewAuxControls(initialVolume int, initialLoopMode backend.LoopMode, initial
|
|||||||
a.cast.SetToolTip(lang.L("Cast to device"))
|
a.cast.SetToolTip(lang.L("Cast to device"))
|
||||||
|
|
||||||
a.autoplay.Highlighted = initialAutoplay
|
a.autoplay.Highlighted = initialAutoplay
|
||||||
//a.autoplay.IconSize = IconButtonSizeSmaller
|
// a.autoplay.IconSize = IconButtonSizeSmaller
|
||||||
a.autoplay.SetToolTip(lang.L("Autoplay"))
|
a.autoplay.SetToolTip(lang.L("Autoplay"))
|
||||||
a.autoplay.OnTapped = func() {
|
a.autoplay.OnTapped = func() {
|
||||||
a.SetAutoplay(!a.autoplay.Highlighted)
|
a.SetAutoplay(!a.autoplay.Highlighted)
|
||||||
|
|||||||
@@ -71,10 +71,12 @@ func (g *FocusList) FocusNeighbor(curItem widget.ListItemID, up bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ fyne.Tappable = (*FocusListRowBase)(nil)
|
var (
|
||||||
var _ fyne.Widget = (*FocusListRowBase)(nil)
|
_ fyne.Tappable = (*FocusListRowBase)(nil)
|
||||||
var _ fyne.Focusable = (*FocusListRowBase)(nil)
|
_ fyne.Widget = (*FocusListRowBase)(nil)
|
||||||
var _ desktop.Hoverable = (*FocusListRowBase)(nil)
|
_ fyne.Focusable = (*FocusListRowBase)(nil)
|
||||||
|
_ desktop.Hoverable = (*FocusListRowBase)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
// Base type used for all list rows in widgets such as Tracklist, etc.
|
// Base type used for all list rows in widgets such as Tracklist, etc.
|
||||||
type FocusListRowBase struct {
|
type FocusListRowBase struct {
|
||||||
@@ -88,7 +90,7 @@ type FocusListRowBase struct {
|
|||||||
|
|
||||||
OnTapped func()
|
OnTapped func()
|
||||||
OnDoubleTapped func()
|
OnDoubleTapped func()
|
||||||
OnFocusNeighbor func(up bool) //TODO: func(up, selecting bool)
|
OnFocusNeighbor func(up bool) // TODO: func(up, selecting bool)
|
||||||
|
|
||||||
tappedAt int64 // unixMillis
|
tappedAt int64 // unixMillis
|
||||||
focusedRect canvas.Rectangle
|
focusedRect canvas.Rectangle
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ import (
|
|||||||
"github.com/dweymouth/supersonic/ui/util"
|
"github.com/dweymouth/supersonic/ui/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ fyne.Widget = (*GridViewItem)(nil)
|
var (
|
||||||
var _ fyne.Focusable = (*GridViewItem)(nil)
|
_ fyne.Widget = (*GridViewItem)(nil)
|
||||||
|
_ fyne.Focusable = (*GridViewItem)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
var _ fyne.Widget = (*coverImage)(nil)
|
var _ fyne.Widget = (*coverImage)(nil)
|
||||||
|
|
||||||
@@ -376,11 +378,10 @@ func setPlayBtnTranslucency(f float32) {
|
|||||||
|
|
||||||
// get theme Primary color as color.NRGBA
|
// get theme Primary color as color.NRGBA
|
||||||
var primary color.NRGBA
|
var primary color.NRGBA
|
||||||
switch pr := theme.Color(theme.ColorNamePrimary); pr.(type) {
|
switch pr := theme.Color(theme.ColorNamePrimary).(type) {
|
||||||
case color.NRGBA:
|
case color.NRGBA:
|
||||||
primary = pr.(color.NRGBA)
|
primary = pr
|
||||||
case color.RGBA:
|
case color.RGBA:
|
||||||
pr := pr.(color.RGBA)
|
|
||||||
primary = color.NRGBA{R: pr.R, G: pr.G, B: pr.B, A: pr.A}
|
primary = color.NRGBA{R: pr.R, G: pr.G, B: pr.B, A: pr.A}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,10 @@ type LoadingDots struct {
|
|||||||
func NewLoadingDots() *LoadingDots {
|
func NewLoadingDots() *LoadingDots {
|
||||||
l := &LoadingDots{}
|
l := &LoadingDots{}
|
||||||
for i := range l.dots {
|
for i := range l.dots {
|
||||||
l.dots[i] = minSizeCircle{Circle: canvas.Circle{
|
l.dots[i] = minSizeCircle{
|
||||||
FillColor: theme.DisabledColor()},
|
Circle: canvas.Circle{
|
||||||
|
FillColor: theme.DisabledColor(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
l.ExtendBaseWidget(l)
|
l.ExtendBaseWidget(l)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ type MultiHyperlink struct {
|
|||||||
|
|
||||||
// TODO: Once https://github.com/fyne-io/fyne/issues/4336 is resolved,
|
// TODO: Once https://github.com/fyne-io/fyne/issues/4336 is resolved,
|
||||||
// we can switch to the much cleaner RichText implementation
|
// we can switch to the much cleaner RichText implementation
|
||||||
//provider *widget.RichText
|
// provider *widget.RichText
|
||||||
|
|
||||||
objects []fyne.CanvasObject
|
objects []fyne.CanvasObject
|
||||||
suffixLabel *ttwidget.RichText
|
suffixLabel *ttwidget.RichText
|
||||||
@@ -45,11 +45,11 @@ type MultiHyperlinkSegment struct {
|
|||||||
|
|
||||||
func NewMultiHyperlink() *MultiHyperlink {
|
func NewMultiHyperlink() *MultiHyperlink {
|
||||||
c := &MultiHyperlink{
|
c := &MultiHyperlink{
|
||||||
//provider: widget.NewRichText(),
|
// provider: widget.NewRichText(),
|
||||||
content: container.NewWithoutLayout(),
|
content: container.NewWithoutLayout(),
|
||||||
}
|
}
|
||||||
c.ExtendBaseWidget(c)
|
c.ExtendBaseWidget(c)
|
||||||
//c.provider.Truncation = fyne.TextTruncateEllipsis
|
// c.provider.Truncation = fyne.TextTruncateEllipsis
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,12 +302,12 @@ func (c *MultiHyperlink) Resize(size fyne.Size) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *MultiHyperlink) Refresh() {
|
func (c *MultiHyperlink) Refresh() {
|
||||||
//c.syncSegments()
|
// c.syncSegments()
|
||||||
c.layoutObjects()
|
c.layoutObjects()
|
||||||
c.BaseWidget.Refresh()
|
c.BaseWidget.Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *MultiHyperlink) CreateRenderer() fyne.WidgetRenderer {
|
func (c *MultiHyperlink) CreateRenderer() fyne.WidgetRenderer {
|
||||||
return widget.NewSimpleRenderer(c.content)
|
return widget.NewSimpleRenderer(c.content)
|
||||||
//return widget.NewSimpleRenderer(c.provider)
|
// return widget.NewSimpleRenderer(c.provider)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -264,7 +264,6 @@ func (p *PlayQueueList) onShowContextMenu(e *fyne.PointEvent, trackIdx int) {
|
|||||||
p.ensureRadiosMenu()
|
p.ensureRadiosMenu()
|
||||||
p.radiosMenu.ShowAtPosition(e.AbsolutePosition)
|
p.radiosMenu.ShowAtPosition(e.AbsolutePosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PlayQueueList) ensureTracksMenu() {
|
func (p *PlayQueueList) ensureTracksMenu() {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func NewStarRating() *StarRating {
|
|||||||
func (s *StarRating) createContainer() {
|
func (s *StarRating) createContainer() {
|
||||||
s.container = container.New(layout.NewCustomPaddedHBoxLayout(0))
|
s.container = container.New(layout.NewCustomPaddedHBoxLayout(0))
|
||||||
var im *canvas.Image
|
var im *canvas.Image
|
||||||
for i := 0; i < 5; i++ {
|
for i := range 5 {
|
||||||
if s.IsDisabled {
|
if s.IsDisabled {
|
||||||
im = canvas.NewImageFromResource(themedDisabledStarOutline)
|
im = canvas.NewImageFromResource(themedDisabledStarOutline)
|
||||||
} else if s.Rating > i {
|
} else if s.Rating > i {
|
||||||
@@ -103,7 +103,7 @@ var _ fyne.Tappable = (*StarRating)(nil)
|
|||||||
|
|
||||||
func (s *StarRating) Tapped(*fyne.PointEvent) {
|
func (s *StarRating) Tapped(*fyne.PointEvent) {
|
||||||
if s.mouseHoverRating <= 0 {
|
if s.mouseHoverRating <= 0 {
|
||||||
return //shouldn't happen
|
return // shouldn't happen
|
||||||
}
|
}
|
||||||
if s.Rating == s.mouseHoverRating {
|
if s.Rating == s.mouseHoverRating {
|
||||||
s.Rating = 0
|
s.Rating = 0
|
||||||
@@ -126,7 +126,7 @@ func (s *StarRating) Refresh() {
|
|||||||
if !s.holdRating && s.mouseHoverRating > 0 {
|
if !s.holdRating && s.mouseHoverRating > 0 {
|
||||||
rating = s.mouseHoverRating
|
rating = s.mouseHoverRating
|
||||||
}
|
}
|
||||||
for i := 0; i < 5; i++ {
|
for i := range 5 {
|
||||||
im := s.container.Objects[i].(*canvas.Image)
|
im := s.container.Objects[i].(*canvas.Image)
|
||||||
im.SetMinSize(fyne.NewSize(s.StarSize, s.StarSize))
|
im.SetMinSize(fyne.NewSize(s.StarSize, s.StarSize))
|
||||||
if s.IsDisabled {
|
if s.IsDisabled {
|
||||||
|
|||||||
@@ -446,6 +446,8 @@ func (t *Tracklist) doSortTracks() {
|
|||||||
t.intSort(func(tr *util.TrackListModel) int64 { return tr.Track().Size })
|
t.intSort(func(tr *util.TrackListModel) int64 { return tr.Track().Size })
|
||||||
case ColumnPlays:
|
case ColumnPlays:
|
||||||
t.intSort(func(tr *util.TrackListModel) int64 { return int64(tr.Track().PlayCount) })
|
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:
|
case ColumnComment:
|
||||||
t.stringSort(func(tr *util.TrackListModel) string { return tr.Track().Comment })
|
t.stringSort(func(tr *util.TrackListModel) string { return tr.Track().Comment })
|
||||||
case ColumnBPM:
|
case ColumnBPM:
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ func (t *TracklistLoader) loadMoreTracks(num int) {
|
|||||||
t.trackBuffer = make([]*mediaprovider.Track, 0, num)
|
t.trackBuffer = make([]*mediaprovider.Track, 0, num)
|
||||||
}
|
}
|
||||||
t.trackBuffer = t.trackBuffer[:0]
|
t.trackBuffer = t.trackBuffer[:0]
|
||||||
for i := 0; i < num; i++ {
|
for range num {
|
||||||
tr := t.iter.Next()
|
tr := t.iter.Next()
|
||||||
if tr == nil {
|
if tr == nil {
|
||||||
t.done = true
|
t.done = true
|
||||||
|
|||||||
+39
-26
@@ -61,6 +61,7 @@ const (
|
|||||||
ColumnFavorite = "Favorite"
|
ColumnFavorite = "Favorite"
|
||||||
ColumnRating = "Rating"
|
ColumnRating = "Rating"
|
||||||
ColumnPlays = "Plays"
|
ColumnPlays = "Plays"
|
||||||
|
ColumnLastPlayed = "LastPlayed"
|
||||||
ColumnComment = "Comment"
|
ColumnComment = "Comment"
|
||||||
ColumnBPM = "BPM"
|
ColumnBPM = "BPM"
|
||||||
ColumnBitrate = "Bitrate"
|
ColumnBitrate = "Bitrate"
|
||||||
@@ -87,6 +88,7 @@ var (
|
|||||||
fav := lang.L("Fav.")
|
fav := lang.L("Fav.")
|
||||||
rating := lang.L("Rating")
|
rating := lang.L("Rating")
|
||||||
plays := lang.L("Plays")
|
plays := lang.L("Plays")
|
||||||
|
lastPlayed := lang.L("Last played")
|
||||||
comment := lang.L("Comment")
|
comment := lang.L("Comment")
|
||||||
bpm := lang.L("BPM")
|
bpm := lang.L("BPM")
|
||||||
bitrate := lang.L("Bit rate")
|
bitrate := lang.L("Bit rate")
|
||||||
@@ -104,6 +106,7 @@ var (
|
|||||||
{Name: ColumnFavorite, Col: ListColumn{Text: " " + fav, Alignment: fyne.TextAlignCenter, CanToggleVisible: true}},
|
{Name: ColumnFavorite, Col: ListColumn{Text: " " + fav, Alignment: fyne.TextAlignCenter, CanToggleVisible: true}},
|
||||||
{Name: ColumnRating, Col: ListColumn{Text: rating, Alignment: fyne.TextAlignLeading, 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: 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: ColumnComment, Col: ListColumn{Text: comment, Alignment: fyne.TextAlignLeading, CanToggleVisible: true}},
|
||||||
{Name: ColumnBPM, Col: ListColumn{Text: bpm, Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}},
|
{Name: ColumnBPM, Col: ListColumn{Text: bpm, Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}},
|
||||||
{Name: ColumnBitrate, Col: ListColumn{Text: bitrate, 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: ColumnFavorite, Col: ListColumn{Text: fav, Alignment: fyne.TextAlignCenter, CanToggleVisible: true}},
|
||||||
{Name: ColumnRating, Col: ListColumn{Text: rating, Alignment: fyne.TextAlignLeading, 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: 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: ColumnComment, Col: ListColumn{Text: comment, Alignment: fyne.TextAlignLeading, CanToggleVisible: true}},
|
||||||
{Name: ColumnBPM, Col: ListColumn{Text: bpm, Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}},
|
{Name: ColumnBPM, Col: ListColumn{Text: bpm, Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}},
|
||||||
{Name: ColumnBitrate, Col: ListColumn{Text: bitrate, Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}},
|
{Name: ColumnBitrate, Col: ListColumn{Text: bitrate, Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}},
|
||||||
@@ -144,6 +148,10 @@ var (
|
|||||||
playsColWidth := fyne.Max(
|
playsColWidth := fyne.Max(
|
||||||
widget.NewLabel("9999").MinSize().Width,
|
widget.NewLabel("9999").MinSize().Width,
|
||||||
widget.NewLabel(plays).MinSize().Width+sortIconWidth)
|
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(
|
bpmColWidth := fyne.Max(
|
||||||
widget.NewLabel(bpm+" ").MinSize().Width,
|
widget.NewLabel(bpm+" ").MinSize().Width,
|
||||||
widget.NewLabel("9999").MinSize().Width)
|
widget.NewLabel("9999").MinSize().Width)
|
||||||
@@ -154,10 +162,10 @@ var (
|
|||||||
widget.NewLabel("99.9 MB").MinSize().Width,
|
widget.NewLabel("99.9 MB").MinSize().Width,
|
||||||
widget.NewLabel(size).MinSize().Width+sortIconWidth)
|
widget.NewLabel(size).MinSize().Width+sortIconWidth)
|
||||||
|
|
||||||
// #, Title, Artist, Album, Composer, Time, Year, Favorite, Rating, Plays, Comment, BPM, Bitrate, Size, Path
|
// #, 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, -1, bpmColWidth, bitrateColWidth, sizeColWidth, -1}
|
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, Comment, BPM, Bitrate, Size, Path
|
// #, 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, -1, bpmColWidth, bitrateColWidth, sizeColWidth, -1}
|
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
|
nextUpdateModel *util.TrackListModel
|
||||||
nextUpdateRowNum int
|
nextUpdateRowNum int
|
||||||
|
|
||||||
num *widget.Label
|
num *widget.Label
|
||||||
name *ttwidget.RichText
|
name *ttwidget.RichText
|
||||||
artist *MultiHyperlink
|
artist *MultiHyperlink
|
||||||
album *MultiHyperlink // for disabled support, if albumID is ""
|
album *MultiHyperlink // for disabled support, if albumID is ""
|
||||||
composer *MultiHyperlink
|
composer *MultiHyperlink
|
||||||
dur *widget.Label
|
dur *widget.Label
|
||||||
year *widget.Label
|
year *widget.Label
|
||||||
favorite *fyne.Container
|
favorite *fyne.Container
|
||||||
rating *StarRating
|
rating *StarRating
|
||||||
bitrate *widget.Label
|
bitrate *widget.Label
|
||||||
plays *widget.Label
|
plays *widget.Label
|
||||||
comment *ttwidget.Label
|
lastPlayed *widget.Label
|
||||||
bpm *widget.Label
|
comment *ttwidget.Label
|
||||||
size *widget.Label
|
bpm *widget.Label
|
||||||
path *ttwidget.Label
|
size *widget.Label
|
||||||
|
path *ttwidget.Label
|
||||||
|
|
||||||
// must be injected by extending widget
|
// must be injected by extending widget
|
||||||
setColVisibility func(int, bool) bool
|
setColVisibility func(int, bool) bool
|
||||||
@@ -253,7 +262,7 @@ func NewExpandedTracklistRow(tracklist *Tracklist, im *backend.ImageManager, pla
|
|||||||
|
|
||||||
v := makeVerticallyCentered // func alias
|
v := makeVerticallyCentered // func alias
|
||||||
container := container.New(tracklist.colLayout,
|
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.Content = container
|
||||||
t.setColVisibility = func(colNum int, vis bool) bool {
|
t.setColVisibility = func(colNum int, vis bool) bool {
|
||||||
c := container.Objects[colNum].(*fyne.Container)
|
c := container.Objects[colNum].(*fyne.Container)
|
||||||
@@ -281,7 +290,7 @@ func NewCompactTracklistRow(tracklist *Tracklist, playingIcon fyne.CanvasObject)
|
|||||||
t.playingIcon = playingIcon
|
t.playingIcon = playingIcon
|
||||||
|
|
||||||
t.Content = container.New(tracklist.colLayout,
|
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{
|
colHiddenPtrMap := map[int]*bool{
|
||||||
2: &t.artist.Hidden,
|
2: &t.artist.Hidden,
|
||||||
@@ -292,11 +301,12 @@ func NewCompactTracklistRow(tracklist *Tracklist, playingIcon fyne.CanvasObject)
|
|||||||
7: &t.favorite.Hidden,
|
7: &t.favorite.Hidden,
|
||||||
8: &t.rating.Hidden,
|
8: &t.rating.Hidden,
|
||||||
9: &t.plays.Hidden,
|
9: &t.plays.Hidden,
|
||||||
10: &t.comment.Hidden,
|
10: &t.lastPlayed.Hidden,
|
||||||
11: &t.bpm.Hidden,
|
11: &t.comment.Hidden,
|
||||||
12: &t.bitrate.Hidden,
|
12: &t.bpm.Hidden,
|
||||||
13: &t.size.Hidden,
|
13: &t.bitrate.Hidden,
|
||||||
14: &t.path.Hidden,
|
14: &t.size.Hidden,
|
||||||
|
15: &t.path.Hidden,
|
||||||
}
|
}
|
||||||
t.setColVisibility = func(colNum int, vis bool) bool {
|
t.setColVisibility = func(colNum int, vis bool) bool {
|
||||||
ptr, ok := colHiddenPtrMap[colNum]
|
ptr, ok := colHiddenPtrMap[colNum]
|
||||||
@@ -338,6 +348,7 @@ func (t *tracklistRowBase) create(tracklist *Tracklist) {
|
|||||||
t.rating.StarSize = 16
|
t.rating.StarSize = 16
|
||||||
t.rating.OnRatingChanged = t.setTrackRating
|
t.rating.OnRatingChanged = t.setTrackRating
|
||||||
t.plays = util.NewTrailingAlignLabel()
|
t.plays = util.NewTrailingAlignLabel()
|
||||||
|
t.lastPlayed = util.NewTruncatingLabel()
|
||||||
t.comment = util.NewTruncatingTooltipLabel()
|
t.comment = util.NewTruncatingTooltipLabel()
|
||||||
t.comment.OnMouseIn = t.MouseIn
|
t.comment.OnMouseIn = t.MouseIn
|
||||||
t.comment.OnMouseOut = t.MouseOut
|
t.comment.OnMouseOut = t.MouseOut
|
||||||
@@ -352,6 +363,7 @@ func (t *tracklistRowBase) create(tracklist *Tracklist) {
|
|||||||
func (t *tracklistRowBase) SetOnTappedSecondary(f func(*fyne.PointEvent, int)) {
|
func (t *tracklistRowBase) SetOnTappedSecondary(f func(*fyne.PointEvent, int)) {
|
||||||
t.OnTappedSecondary = f
|
t.OnTappedSecondary = f
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tracklistRowBase) TrackID() string {
|
func (t *tracklistRowBase) TrackID() string {
|
||||||
return t.trackID
|
return t.trackID
|
||||||
}
|
}
|
||||||
@@ -404,6 +416,7 @@ func (t *tracklistRowBase) doUpdate(tm *util.TrackListModel, rowNum int) {
|
|||||||
t.dur.Text = util.SecondsToMMSS(tr.Duration.Seconds())
|
t.dur.Text = util.SecondsToMMSS(tr.Duration.Seconds())
|
||||||
t.year.Text = strconv.Itoa(tr.Year)
|
t.year.Text = strconv.Itoa(tr.Year)
|
||||||
t.plays.Text = strconv.Itoa(int(tr.PlayCount))
|
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.Text = strings.ReplaceAll(tr.Comment, "\n", " ")
|
||||||
t.comment.SetToolTip(tr.Comment)
|
t.comment.SetToolTip(tr.Comment)
|
||||||
t.bpm.Text = strconv.Itoa(tr.BPM)
|
t.bpm.Text = strconv.Itoa(tr.BPM)
|
||||||
|
|||||||
Reference in New Issue
Block a user