Merge pull request #427 from dweymouth/translations

Prepare for translations
This commit is contained in:
Drew Weymouth
2024-07-21 16:46:16 -07:00
committed by GitHub
41 changed files with 676 additions and 378 deletions
@@ -9,17 +9,11 @@ import (
"github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/sharedutil"
) )
const (
ArtistSortAlbumCount string = "Album Count"
ArtistSortNameAZ string = "Name (A-Z)"
ArtistSortRandom string = "Random"
)
func (j *jellyfinMediaProvider) ArtistSortOrders() []string { func (j *jellyfinMediaProvider) ArtistSortOrders() []string {
return []string{ return []string{
ArtistSortAlbumCount, mediaprovider.ArtistSortAlbumCount,
ArtistSortNameAZ, mediaprovider.ArtistSortNameAZ,
ArtistSortRandom, mediaprovider.ArtistSortRandom,
} }
} }
@@ -29,10 +23,10 @@ func (j *jellyfinMediaProvider) IterateArtists(sortOrder string, filter mediapro
var sortFn func([]*jellyfin.Artist) []*jellyfin.Artist var sortFn func([]*jellyfin.Artist) []*jellyfin.Artist
if sortOrder == "" { if sortOrder == "" {
sortOrder = ArtistSortNameAZ // default sortOrder = mediaprovider.ArtistSortNameAZ // default
} }
switch sortOrder { switch sortOrder {
case ArtistSortAlbumCount: case mediaprovider.ArtistSortAlbumCount:
// Pagination needs to be disabled, to retrieve all results in a single request, and correctly sort them. // Pagination needs to be disabled, to retrieve all results in a single request, and correctly sort them.
disablePagination = true disablePagination = true
sortFn = func(artists []*jellyfin.Artist) []*jellyfin.Artist { sortFn = func(artists []*jellyfin.Artist) []*jellyfin.Artist {
@@ -41,10 +35,10 @@ func (j *jellyfinMediaProvider) IterateArtists(sortOrder string, filter mediapro
}) })
return artists return artists
} }
case ArtistSortNameAZ: case mediaprovider.ArtistSortNameAZ:
jfSort.Field = jellyfin.SortByName jfSort.Field = jellyfin.SortByName
jfSort.Mode = jellyfin.SortAsc jfSort.Mode = jellyfin.SortAsc
case ArtistSortRandom: case mediaprovider.ArtistSortRandom:
jfSort.Field = jellyfin.SortByRandom jfSort.Field = jellyfin.SortByRandom
} }
+13 -22
View File
@@ -9,44 +9,35 @@ import (
"github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/sharedutil"
) )
const (
AlbumSortRecentlyAdded string = "Recently Added"
AlbumSortRandom string = "Random"
AlbumSortTitleAZ string = "Title (A-Z)"
AlbumSortArtistAZ string = "Artist (A-Z)"
AlbumSortYearAscending string = "Year (ascending)"
AlbumSortYearDescending string = "Year (descending)"
)
func (j *jellyfinMediaProvider) AlbumSortOrders() []string { func (j *jellyfinMediaProvider) AlbumSortOrders() []string {
return []string{ return []string{
AlbumSortRecentlyAdded, mediaprovider.AlbumSortRecentlyAdded,
AlbumSortRandom, mediaprovider.AlbumSortRandom,
AlbumSortTitleAZ, mediaprovider.AlbumSortTitleAZ,
AlbumSortArtistAZ, mediaprovider.AlbumSortArtistAZ,
AlbumSortYearAscending, mediaprovider.AlbumSortYearAscending,
AlbumSortYearDescending, mediaprovider.AlbumSortYearDescending,
} }
} }
func (j *jellyfinMediaProvider) IterateAlbums(sortOrder string, filter mediaprovider.AlbumFilter) mediaprovider.AlbumIterator { func (j *jellyfinMediaProvider) IterateAlbums(sortOrder string, filter mediaprovider.AlbumFilter) mediaprovider.AlbumIterator {
var jfSort jellyfin.Sort var jfSort jellyfin.Sort
switch sortOrder { switch sortOrder {
case AlbumSortRecentlyAdded: case mediaprovider.AlbumSortRecentlyAdded:
jfSort.Field = jellyfin.SortByDateCreated jfSort.Field = jellyfin.SortByDateCreated
jfSort.Mode = jellyfin.SortDesc jfSort.Mode = jellyfin.SortDesc
case AlbumSortRandom: case mediaprovider.AlbumSortRandom:
jfSort.Field = jellyfin.SortByRandom jfSort.Field = jellyfin.SortByRandom
case AlbumSortArtistAZ: case mediaprovider.AlbumSortArtistAZ:
jfSort.Field = jellyfin.SortByArtist jfSort.Field = jellyfin.SortByArtist
jfSort.Mode = jellyfin.SortAsc jfSort.Mode = jellyfin.SortAsc
case AlbumSortTitleAZ: case mediaprovider.AlbumSortTitleAZ:
jfSort.Field = jellyfin.SortByName jfSort.Field = jellyfin.SortByName
jfSort.Mode = jellyfin.SortAsc jfSort.Mode = jellyfin.SortAsc
case AlbumSortYearAscending: case mediaprovider.AlbumSortYearAscending:
jfSort.Field = jellyfin.SortByYear jfSort.Field = jellyfin.SortByYear
jfSort.Mode = jellyfin.SortAsc jfSort.Mode = jellyfin.SortAsc
case AlbumSortYearDescending: case mediaprovider.AlbumSortYearDescending:
jfSort.Field = jellyfin.SortByYear jfSort.Field = jellyfin.SortByYear
jfSort.Mode = jellyfin.SortDesc jfSort.Mode = jellyfin.SortDesc
} }
@@ -64,7 +55,7 @@ func (j *jellyfinMediaProvider) IterateAlbums(sortOrder string, filter mediaprov
return sharedutil.MapSlice(al, toAlbum), nil return sharedutil.MapSlice(al, toAlbum), nil
} }
if sortOrder == AlbumSortRandom { if sortOrder == mediaprovider.AlbumSortRandom {
determFetcher := func(offs, limit int) ([]*mediaprovider.Album, error) { determFetcher := func(offs, limit int) ([]*mediaprovider.Album, error) {
al, err := j.client.GetAlbums(jellyfin.QueryOpts{ al, err := j.client.GetAlbums(jellyfin.QueryOpts{
Sort: jellyfin.Sort{Field: "SortName", Mode: jellyfin.SortAsc}, Sort: jellyfin.Sort{Field: "SortName", Mode: jellyfin.SortAsc},
+19
View File
@@ -9,6 +9,25 @@ import (
"github.com/deluan/sanitize" "github.com/deluan/sanitize"
) )
const (
// set of all supported album sorts across all media providers
// these strings may be translated
AlbumSortRecentlyAdded string = "Recently Added"
AlbumSortRecentlyPlayed string = "Recently Played"
AlbumSortFrequentlyPlayed string = "Frequently Played"
AlbumSortRandom string = "Random"
AlbumSortTitleAZ string = "Title (A-Z)"
AlbumSortArtistAZ string = "Artist (A-Z)"
AlbumSortYearAscending string = "Year (ascending)"
AlbumSortYearDescending string = "Year (descending)"
// set of all supported artist sorts across all media providers
// these strings may be translated
ArtistSortAlbumCount string = "Album Count"
ArtistSortNameAZ string = "Name (A-Z)"
ArtistSortRandom string = "Random"
)
type MediaIterator[M any] interface { type MediaIterator[M any] interface {
Next() *M Next() *M
} }
+17 -28
View File
@@ -11,27 +11,16 @@ import (
"github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/sharedutil"
) )
const (
AlbumSortRecentlyAdded string = "Recently Added"
AlbumSortRecentlyPlayed string = "Recently Played"
AlbumSortFrequentlyPlayed string = "Frequently Played"
AlbumSortRandom string = "Random"
AlbumSortTitleAZ string = "Title (A-Z)"
AlbumSortArtistAZ string = "Artist (A-Z)"
AlbumSortYearAscending string = "Year (ascending)"
AlbumSortYearDescending string = "Year (descending)"
)
func (s *subsonicMediaProvider) AlbumSortOrders() []string { func (s *subsonicMediaProvider) AlbumSortOrders() []string {
return []string{ return []string{
AlbumSortRecentlyAdded, mediaprovider.AlbumSortRecentlyAdded,
AlbumSortRecentlyPlayed, mediaprovider.AlbumSortRecentlyPlayed,
AlbumSortFrequentlyPlayed, mediaprovider.AlbumSortFrequentlyPlayed,
AlbumSortRandom, mediaprovider.AlbumSortRandom,
AlbumSortTitleAZ, mediaprovider.AlbumSortTitleAZ,
AlbumSortArtistAZ, mediaprovider.AlbumSortArtistAZ,
AlbumSortYearAscending, mediaprovider.AlbumSortYearAscending,
AlbumSortYearDescending, mediaprovider.AlbumSortYearDescending,
} }
} }
@@ -86,28 +75,28 @@ func (s *subsonicMediaProvider) IterateAlbums(sortOrder string, filter mediaprov
return s.baseIterFromSimpleSortOrder("starred", modifiedFilter) return s.baseIterFromSimpleSortOrder("starred", modifiedFilter)
} }
if sortOrder == "" { if sortOrder == "" {
sortOrder = AlbumSortRecentlyAdded // default sortOrder = mediaprovider.AlbumSortRecentlyAdded // default
} }
switch sortOrder { switch sortOrder {
case AlbumSortRecentlyAdded: case mediaprovider.AlbumSortRecentlyAdded:
return s.baseIterFromSimpleSortOrder("newest", filter) return s.baseIterFromSimpleSortOrder("newest", filter)
case AlbumSortRecentlyPlayed: case mediaprovider.AlbumSortRecentlyPlayed:
return s.baseIterFromSimpleSortOrder("recent", filter) return s.baseIterFromSimpleSortOrder("recent", filter)
case AlbumSortFrequentlyPlayed: case mediaprovider.AlbumSortFrequentlyPlayed:
return s.baseIterFromSimpleSortOrder("frequent", filter) return s.baseIterFromSimpleSortOrder("frequent", filter)
case AlbumSortRandom: case mediaprovider.AlbumSortRandom:
return s.newRandomIter(filter, s.prefetchCoverCB) return s.newRandomIter(filter, s.prefetchCoverCB)
case AlbumSortTitleAZ: case mediaprovider.AlbumSortTitleAZ:
return s.baseIterFromSimpleSortOrder("alphabeticalByName", filter) return s.baseIterFromSimpleSortOrder("alphabeticalByName", filter)
case AlbumSortArtistAZ: case mediaprovider.AlbumSortArtistAZ:
return s.baseIterFromSimpleSortOrder("alphabeticalByArtist", filter) return s.baseIterFromSimpleSortOrder("alphabeticalByArtist", filter)
case AlbumSortYearAscending: case mediaprovider.AlbumSortYearAscending:
fetchFn := func(offset, limit int) ([]*subsonic.AlbumID3, error) { fetchFn := func(offset, limit int) ([]*subsonic.AlbumID3, error) {
return s.client.GetAlbumList2("byYear", return s.client.GetAlbumList2("byYear",
map[string]string{"fromYear": "0", "toYear": "3000", "offset": strconv.Itoa(offset), "limit": strconv.Itoa(limit)}) map[string]string{"fromYear": "0", "toYear": "3000", "offset": strconv.Itoa(offset), "limit": strconv.Itoa(limit)})
} }
return helpers.NewAlbumIterator(makeFetchFn(fetchFn), filter, s.prefetchCoverCB) return helpers.NewAlbumIterator(makeFetchFn(fetchFn), filter, s.prefetchCoverCB)
case AlbumSortYearDescending: case mediaprovider.AlbumSortYearDescending:
fetchFn := func(offset, limit int) ([]*subsonic.AlbumID3, error) { fetchFn := func(offset, limit int) ([]*subsonic.AlbumID3, error) {
return s.client.GetAlbumList2("byYear", return s.client.GetAlbumList2("byYear",
map[string]string{"fromYear": "3000", "toYear": "0", "offset": strconv.Itoa(offset), "limit": strconv.Itoa(limit)}) map[string]string{"fromYear": "3000", "toYear": "0", "offset": strconv.Itoa(offset), "limit": strconv.Itoa(limit)})
@@ -14,17 +14,11 @@ import (
"github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/sharedutil"
) )
const (
ArtistSortAlbumCount string = "Album Count"
ArtistSortNameAZ string = "Name (A-Z)"
ArtistSortRandom string = "Random"
)
func (s *subsonicMediaProvider) ArtistSortOrders() []string { func (s *subsonicMediaProvider) ArtistSortOrders() []string {
return []string{ return []string{
ArtistSortAlbumCount, mediaprovider.ArtistSortAlbumCount,
ArtistSortNameAZ, mediaprovider.ArtistSortNameAZ,
ArtistSortRandom, mediaprovider.ArtistSortRandom,
} }
} }
@@ -37,10 +31,10 @@ func filterArtistMatches(f mediaprovider.ArtistFilter, artist *subsonic.ArtistID
func (s *subsonicMediaProvider) IterateArtists(sortOrder string, filter mediaprovider.ArtistFilter) mediaprovider.ArtistIterator { func (s *subsonicMediaProvider) IterateArtists(sortOrder string, filter mediaprovider.ArtistFilter) mediaprovider.ArtistIterator {
if sortOrder == "" { if sortOrder == "" {
sortOrder = ArtistSortNameAZ // default sortOrder = mediaprovider.ArtistSortNameAZ // default
} }
switch sortOrder { switch sortOrder {
case ArtistSortAlbumCount: case mediaprovider.ArtistSortAlbumCount:
return s.baseArtistIterFromSimpleSortOrder( return s.baseArtistIterFromSimpleSortOrder(
func(artists []*subsonic.ArtistID3) []*subsonic.ArtistID3 { func(artists []*subsonic.ArtistID3) []*subsonic.ArtistID3 {
slices.SortStableFunc(artists, func(a, b *subsonic.ArtistID3) int { slices.SortStableFunc(artists, func(a, b *subsonic.ArtistID3) int {
@@ -50,7 +44,7 @@ func (s *subsonicMediaProvider) IterateArtists(sortOrder string, filter mediapro
}, },
filter, filter,
) )
case ArtistSortNameAZ: case mediaprovider.ArtistSortNameAZ:
return s.baseArtistIterFromSimpleSortOrder( return s.baseArtistIterFromSimpleSortOrder(
func(artists []*subsonic.ArtistID3) []*subsonic.ArtistID3 { func(artists []*subsonic.ArtistID3) []*subsonic.ArtistID3 {
c := collate.New(language.English, collate.Loose) c := collate.New(language.English, collate.Loose)
@@ -61,7 +55,7 @@ func (s *subsonicMediaProvider) IterateArtists(sortOrder string, filter mediapro
}, },
filter, filter,
) )
case ArtistSortRandom: case mediaprovider.ArtistSortRandom:
return s.baseArtistIterFromSimpleSortOrder( return s.baseArtistIterFromSimpleSortOrder(
func(artists []*subsonic.ArtistID3) []*subsonic.ArtistID3 { func(artists []*subsonic.ArtistID3) []*subsonic.ArtistID3 {
newArtists := make([]*subsonic.ArtistID3, len(artists)) newArtists := make([]*subsonic.ArtistID3, len(artists))
@@ -12,7 +12,7 @@ func (s *subsonicMediaProvider) IterateTracks(searchQuery string) mediaprovider.
return &allTracksIterator{ return &allTracksIterator{
s: s, s: s,
albumIter: s.IterateAlbums( albumIter: s.IterateAlbums(
AlbumSortArtistAZ, mediaprovider.AlbumSortArtistAZ,
mediaprovider.NewAlbumFilter(mediaprovider.AlbumFilterOptions{}), mediaprovider.NewAlbumFilter(mediaprovider.AlbumFilterOptions{}),
), ),
} }
+1 -1
View File
@@ -53,4 +53,4 @@ require (
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )
replace fyne.io/fyne/v2 v2.5.0 => github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20240701152141-03bb69ca60ab replace fyne.io/fyne/v2 v2.5.0 => github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20240721175043-f7b0391c76a6
+2 -2
View File
@@ -79,8 +79,8 @@ github.com/dweymouth/fyne-advanced-list v0.0.0-20240623145729-9c6b8f99bcfe h1:ow
github.com/dweymouth/fyne-advanced-list v0.0.0-20240623145729-9c6b8f99bcfe/go.mod h1:sbOhla4VcfFb4OjXiUFTLXMPTnhRUlVrDMhB8HtWR4o= github.com/dweymouth/fyne-advanced-list v0.0.0-20240623145729-9c6b8f99bcfe/go.mod h1:sbOhla4VcfFb4OjXiUFTLXMPTnhRUlVrDMhB8HtWR4o=
github.com/dweymouth/fyne-lyrics v0.0.0-20240528234907-15eee7ce5e64 h1:RUIrnGY034rDMlcOui/daurwX5b+52KdUKhH9aXaDSg= github.com/dweymouth/fyne-lyrics v0.0.0-20240528234907-15eee7ce5e64 h1:RUIrnGY034rDMlcOui/daurwX5b+52KdUKhH9aXaDSg=
github.com/dweymouth/fyne-lyrics v0.0.0-20240528234907-15eee7ce5e64/go.mod h1:3YrjFDHMlhCsSZ/OvmJCxWm9QHSgOVWZBxnraZz9Z7c= github.com/dweymouth/fyne-lyrics v0.0.0-20240528234907-15eee7ce5e64/go.mod h1:3YrjFDHMlhCsSZ/OvmJCxWm9QHSgOVWZBxnraZz9Z7c=
github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20240701152141-03bb69ca60ab h1:ShhYp3PTlbBGMo2Q2E2bmK9IlXuh0e1675VawRz6ps4= github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20240721175043-f7b0391c76a6 h1:4NuEVS2zgD14ptLoMU21j6Fd23Bjoject4LjfGnCPv0=
github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20240701152141-03bb69ca60ab/go.mod h1:9D4oT3NWeG+MLi/lP7ItZZyujHC/qqMJpoGTAYX5Uqc= github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20240721175043-f7b0391c76a6/go.mod h1:9D4oT3NWeG+MLi/lP7ItZZyujHC/qqMJpoGTAYX5Uqc=
github.com/dweymouth/go-jellyfin v0.0.0-20240517151952-5ceca61cb645 h1:KzqSaQwG3HsTZQlEtkp0BeUy9vmYZ0rq0B15qIPSiBs= github.com/dweymouth/go-jellyfin v0.0.0-20240517151952-5ceca61cb645 h1:KzqSaQwG3HsTZQlEtkp0BeUy9vmYZ0rq0B15qIPSiBs=
github.com/dweymouth/go-jellyfin v0.0.0-20240517151952-5ceca61cb645/go.mod h1:fcUagHBaQnt06GmBAllNE0J4O/7064zXRWdqnTTtVjI= github.com/dweymouth/go-jellyfin v0.0.0-20240517151952-5ceca61cb645/go.mod h1:fcUagHBaQnt06GmBAllNE0J4O/7064zXRWdqnTTtVjI=
github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee h1:ZGyJ6wp7CAfT31BugypcF/TPKEy2RrGR9JFq1JOjOpY= github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee h1:ZGyJ6wp7CAfT31BugypcF/TPKEy2RrGR9JFq1JOjOpY=
+2
View File
@@ -14,6 +14,7 @@ import (
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/app" "fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/lang"
) )
func main() { func main() {
@@ -43,6 +44,7 @@ func main() {
os.Setenv("FYNE_SCALE", "1.1") os.Setenv("FYNE_SCALE", "1.1")
} }
lang.AddTranslationsFS(res.Translations, "translations")
fyneApp := app.New() fyneApp := app.New()
fyneApp.SetIcon(res.ResAppicon256Png) fyneApp.SetIcon(res.ResAppicon256Png)
+6
View File
@@ -0,0 +1,6 @@
package res
import "embed"
//go:embed translations
var Translations embed.FS
+190
View File
@@ -0,0 +1,190 @@
{
"Add Server": "Add Server",
"Add to playlist": "Add to playlist",
"Add to queue": "Add to queue",
"Album": "Album",
"album": "album",
"Album count": "Album count",
"Album Count": "Album Count",
"Album filters": "Album filters",
"Album gain": "Album gain",
"Album info not available": "Album info not available",
"Album peak": "Album peak",
"Albums": "Albums",
"albums": "albums",
"All": "All",
"All Tracks": "All Tracks",
"Alt. URL": "Alt. URL",
"Are you sure you want to delete the server": "Are you sure you want to delete the server",
"Artist": "Artist",
"Artist (A-Z)": "Artist (A-Z)",
"Artists": "Artists",
"Artist biography not available.": "Artist biography not available.",
"Audio device": "Audio device",
"Audio Drama": "Audio Drama",
"Audiobook": "Audiobook",
"Authentication failed": "Authentication failed",
"Auto": "Auto",
"Autoselect device": "Autoselect device",
"Bit rate": "Bit rate",
"BPM": "BPM",
"Broadcast": "Broadcast",
"Cancel": "Cancel",
"Close": "Close",
"Close to system tray": "Close to system tray",
"Comment": "Comment",
"Compilation": "Compilation",
"Composer": "Composer",
"Configure your music server to add radio stations": "Configure your music server to add radio stations",
"Confirm Delete Playlist": "Confirm Delete Playlist",
"Confirm Delete Server": "Confirm Delete Server",
"Connect to Server": "Connect to Server",
"Connecting": "Connecting",
"Connecting to": "Connecting to",
"Content type": "Content type",
"Could not reach server": "Could not reach server",
"Create new playlist": "Create new playlist",
"day": "day",
"days": "days",
"Delete Playlist": "Delete Playlist",
"Demo": "Demo",
"Description": "Description",
"Disable server transcoding": "Disable server transcoding",
"Disc number": "Disc number",
"Discography": "Discography",
"discs": "discs",
"DJ-Mix": "DJ-Mix",
"Download": "Download",
"Download completed": "Download completed",
"Duration": "Duration",
"Edit": "Edit",
"Edit Playlist": "Edit Playlist",
"Edit server": "Edit server",
"Enable system tray": "Enable system tray",
"Enabled": "Enabled",
"Enter": "Enter",
"EP": "EP",
"Equalizer": "Equalizer",
"Exclusive mode": "Exclusive mode",
"Favorites": "Favorites",
"Field Recording": "Field Recording",
"File path": "File path",
"File size": "File size",
"Filter genres": "Filter genres",
"Frequently Played": "Frequently Played",
"General": "General",
"Genre": "Genre",
"Genres": "Genres",
"Github page": "Github page",
"Home Page": "Home Page",
"hr": "hr",
"hrs": "hrs",
"Internet Radio Stations": "Internet Radio Stations",
"Interview": "Interview",
"Is favorite": "Is favorite",
"Is not favorite": "Is not favorite",
"Last played": "Last played",
"Live": "Live",
"Locally": "Locally",
"Login to Server": "Login to Server",
"Lyrics": "Lyrics",
"Lyrics not available": "Lyrics not available",
"min": "min",
"minutes of track have been played": "minutes of track have been played",
"Mixtape": "Mixtape",
"My Server": "My Server",
"Name": "Name",
"Name (A-Z)": "Name (A-Z)",
"Nickname": "Nickname",
"No radio stations available": "No radio stations available",
"None": "None",
"none": "none",
"none selected": "none selected",
"OK": "OK",
"optional": "optional",
"or when": "or when",
"Password": "Password",
"Paused": "Paused",
"percent of track is played": "percent of track is played",
"Play": "Play",
"Play Artist Radio": "Play Artist Radio",
"Play count": "Play count",
"Play Discography": "Play Discography",
"Play next": "Play next",
"Play Queue": "Play Queue",
"Play random": "Play random",
"Playback": "Playback",
"Playing": "Playing",
"Playlist": "Playlist",
"Playlists": "Playlists",
"playlist by": "playlist by",
"Plays": "Plays",
"Prevent clipping": "Prevent clipping",
"Private": "Private",
"Public": "Public",
"Random": "Random",
"Rating": "Rating",
"reissued": "reissued",
"Recently Added": "Recently Added",
"Recently Played": "Recently Played",
"Related": "Related",
"Remix": "Remix",
"Remove from playlist": "Remove from playlist",
"ReplayGain mode": "ReplayGain mode",
"ReplayGain preamp": "ReplayGain preamp",
"Restart required": "Restart required",
"Save play queue on exit": "Save play queue on exit",
"Saved at": "Saved at",
"Scrobble when": "Scrobble when",
"Search Everywhere": "Search Everywhere",
"Search page": "Search page",
"Search playlists or new playlist name": "Search playlists or new playlist name",
"sec": "sec",
"selected": "selected",
"Send playback statistics to server": "Send playback statistics to server",
"Server": "Server",
"Server Type": "Server Type",
"Server unreachable": "Server unreachable",
"Set rating": "Set rating",
"Share": "Share",
"Share content": "Share content",
"Show info": "Show info",
"Show notification on track change": "Show notification on track change",
"Show year in album grid cards": "Show year in album grid cards",
"Shuffle": "Shuffle",
"Shuffle albums": "Shuffle albums",
"Shuffle tracks": "Shuffle tracks",
"Similar artists": "Similar artists",
"Single": "Single",
"Skip duplicate tracks": "Skip duplicate tracks",
"Soundtrack": "Soundtrack",
"Spoken Word": "Spoken Word",
"Startup page": "Startup page",
"Stopped": "Stopped",
"Support the project": "Support the project",
"Testing connection": "Testing connection",
"Theme": "Theme",
"Title": "Title",
"Title (A-Z)": "Title (A-Z)",
"Top Tracks": "Top Tracks",
"Total time": "Total time",
"Track": "Track",
"track": "track",
"Track count": "Track count",
"Track gain": "Track gain",
"Track Info": "Track Info",
"Track number": "Track number",
"Track peak": "Track peak",
"tracks": "tracks",
"UI Scaling": "UI Scaling",
"URL": "URL",
"Use legacy authentication": "Use legacy authentication",
"Username": "Username",
"version": "version",
"wrong URL": "wrong URL",
"wrong username/password": "wrong username/password",
"Year": "Year",
"Year (ascending)": "Year (ascending)",
"Year (descending)": "Year (descending)",
"Year from": "Year from"
}
+14 -13
View File
@@ -15,6 +15,7 @@ import (
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
@@ -211,7 +212,7 @@ func NewAlbumPageHeader(page *AlbumPage) *AlbumPageHeader {
SizeName: theme.SizeNameHeadingText, SizeName: theme.SizeNameHeadingText,
} }
a.releaseTypeLabel = widget.NewRichText( a.releaseTypeLabel = widget.NewRichText(
&widget.TextSegment{Text: "Album", Style: util.BoldRichTextStyle}, &widget.TextSegment{Text: lang.L("Album"), Style: util.BoldRichTextStyle},
&widget.TextSegment{Text: " by", Style: widget.RichTextStyle{Inline: true}}, &widget.TextSegment{Text: " by", Style: widget.RichTextStyle{Inline: true}},
) )
a.artistLabel = widgets.NewMultiHyperlink() a.artistLabel = widgets.NewMultiHyperlink()
@@ -224,10 +225,10 @@ func NewAlbumPageHeader(page *AlbumPage) *AlbumPageHeader {
a.page.contr.NavigateTo(controller.GenreRoute(genre)) a.page.contr.NavigateTo(controller.GenreRoute(genre))
} }
a.miscLabel = widget.NewLabel("") a.miscLabel = widget.NewLabel("")
playButton := widget.NewButtonWithIcon("Play", theme.MediaPlayIcon(), func() { playButton := widget.NewButtonWithIcon(lang.L("Play"), theme.MediaPlayIcon(), func() {
go a.page.pm.PlayAlbum(a.page.albumID, 0, false) go a.page.pm.PlayAlbum(a.page.albumID, 0, false)
}) })
shuffleBtn := widget.NewButtonWithIcon("Shuffle", myTheme.ShuffleIcon, func() { shuffleBtn := widget.NewButtonWithIcon(lang.L("Shuffle"), myTheme.ShuffleIcon, func() {
a.page.pm.LoadTracks(a.page.tracklist.GetTracks(), backend.Replace, true) a.page.pm.LoadTracks(a.page.tracklist.GetTracks(), backend.Replace, true)
a.page.pm.PlayFromBeginning() a.page.pm.PlayFromBeginning()
}) })
@@ -235,28 +236,28 @@ func NewAlbumPageHeader(page *AlbumPage) *AlbumPageHeader {
menuBtn := widget.NewButtonWithIcon("", theme.MoreHorizontalIcon(), nil) menuBtn := widget.NewButtonWithIcon("", theme.MoreHorizontalIcon(), nil)
menuBtn.OnTapped = func() { menuBtn.OnTapped = func() {
if pop == nil { if pop == nil {
playNext := fyne.NewMenuItem("Play next", func() { playNext := fyne.NewMenuItem(lang.L("Play next"), func() {
go a.page.pm.LoadAlbum(a.albumID, backend.InsertNext, false /*shuffle*/) go a.page.pm.LoadAlbum(a.albumID, backend.InsertNext, false /*shuffle*/)
}) })
playNext.Icon = myTheme.PlayNextIcon playNext.Icon = myTheme.PlayNextIcon
queue := fyne.NewMenuItem("Add to queue", func() { queue := fyne.NewMenuItem(lang.L("Add to queue"), func() {
go a.page.pm.LoadAlbum(a.albumID, backend.Append, false /*shuffle*/) go a.page.pm.LoadAlbum(a.albumID, backend.Append, false /*shuffle*/)
}) })
queue.Icon = theme.ContentAddIcon() queue.Icon = theme.ContentAddIcon()
playlist := fyne.NewMenuItem("Add to playlist...", func() { playlist := fyne.NewMenuItem(lang.L("Add to playlist")+"...", func() {
a.page.contr.DoAddTracksToPlaylistWorkflow( a.page.contr.DoAddTracksToPlaylistWorkflow(
sharedutil.TracksToIDs(a.page.tracks)) sharedutil.TracksToIDs(a.page.tracks))
}) })
playlist.Icon = myTheme.PlaylistIcon playlist.Icon = myTheme.PlaylistIcon
download := fyne.NewMenuItem("Download...", func() { download := fyne.NewMenuItem(lang.L("Download")+"...", func() {
a.page.contr.ShowDownloadDialog(a.page.tracks, a.titleLabel.String()) a.page.contr.ShowDownloadDialog(a.page.tracks, a.titleLabel.String())
}) })
download.Icon = theme.DownloadIcon() download.Icon = theme.DownloadIcon()
info := fyne.NewMenuItem("Show Info...", func() { info := fyne.NewMenuItem(lang.L("Show info")+"...", func() {
a.page.contr.ShowAlbumInfoDialog(a.albumID, a.titleLabel.String(), a.cover.Image()) a.page.contr.ShowAlbumInfoDialog(a.albumID, a.titleLabel.String(), a.cover.Image())
}) })
info.Icon = theme.InfoIcon() info.Icon = theme.InfoIcon()
a.shareMenuItem = fyne.NewMenuItem("Share...", func() { a.shareMenuItem = fyne.NewMenuItem(lang.L("Share")+"...", func() {
a.page.contr.ShowShareDialog(a.albumID) a.page.contr.ShowShareDialog(a.albumID)
}) })
a.shareMenuItem.Icon = myTheme.ShareIcon a.shareMenuItem.Icon = myTheme.ShareIcon
@@ -360,16 +361,16 @@ func formatMiscLabelStr(a *mediaprovider.AlbumWithTracks) string {
var discs string var discs string
if len(a.Tracks) > 0 { if len(a.Tracks) > 0 {
if discCount := a.Tracks[len(a.Tracks)-1].DiscNumber; discCount > 1 { if discCount := a.Tracks[len(a.Tracks)-1].DiscNumber; discCount > 1 {
discs = fmt.Sprintf("%d discs · ", discCount) discs = fmt.Sprintf("%d %s · ", discCount, lang.L("discs"))
} }
} }
tracks := "tracks" tracks := lang.L("tracks")
if a.TrackCount == 1 { if a.TrackCount == 1 {
tracks = "track" tracks = lang.L("track")
} }
yearStr := strconv.Itoa(a.Year) yearStr := strconv.Itoa(a.Year)
if a.ReissueYear > a.Year { if a.ReissueYear > a.Year {
yearStr += fmt.Sprintf(" (reissued %d)", a.ReissueYear) yearStr += fmt.Sprintf(" (%s %d)", lang.L("reissued"), a.ReissueYear)
} }
return fmt.Sprintf("%s · %d %s · %s%s", yearStr, a.TrackCount, tracks, discs, util.SecondsToTimeString(float64(a.Duration))) return fmt.Sprintf("%s · %d %s · %s%s", yearStr, a.TrackCount, tracks, discs, util.SecondsToTimeString(float64(a.Duration)))
} }
+14 -9
View File
@@ -4,9 +4,11 @@ import (
"slices" "slices"
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
"github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/sharedutil"
"github.com/dweymouth/supersonic/ui/controller" "github.com/dweymouth/supersonic/ui/controller"
myTheme "github.com/dweymouth/supersonic/ui/theme" myTheme "github.com/dweymouth/supersonic/ui/theme"
"github.com/dweymouth/supersonic/ui/util" "github.com/dweymouth/supersonic/ui/util"
@@ -27,7 +29,7 @@ func NewAlbumsPage(cfg *backend.AlbumsPageConfig, pool *util.WidgetPool, contr *
return NewGridViewPage(adapter, pool, mp, im) return NewGridViewPage(adapter, pool, mp, im)
} }
func (a *albumsPageAdapter) Title() string { return "Albums" } func (a *albumsPageAdapter) Title() string { return lang.L("Albums") }
func (a *albumsPageAdapter) Filter() mediaprovider.AlbumFilter { func (a *albumsPageAdapter) Filter() mediaprovider.AlbumFilter {
if a.filter == nil { if a.filter == nil {
@@ -49,22 +51,25 @@ func (a *albumsPageAdapter) PlaceholderResource() fyne.Resource { return myTheme
func (a *albumsPageAdapter) Route() controller.Route { return controller.AlbumsRoute() } func (a *albumsPageAdapter) Route() controller.Route { return controller.AlbumsRoute() }
func (a *albumsPageAdapter) SortOrders() ([]string, string) { func (a *albumsPageAdapter) SortOrders() ([]string, int) {
orders := a.mp.AlbumSortOrders() orders := a.mp.AlbumSortOrders()
sortOrder := a.cfg.SortOrder sortOrder := slices.Index(orders, a.cfg.SortOrder)
if !slices.Contains(orders, sortOrder) { if sortOrder < 0 {
sortOrder = string(orders[0]) sortOrder = 0
} }
return orders, sortOrder
translatedOrders := sharedutil.MapSlice(orders, func(s string) string { return lang.L(s) })
return translatedOrders, sortOrder
} }
func (a *albumsPageAdapter) SaveSortOrder(order string) { func (a *albumsPageAdapter) SaveSortOrder(orderIdx int) {
a.cfg.SortOrder = order a.cfg.SortOrder = a.mp.AlbumSortOrders()[orderIdx]
} }
func (a *albumsPageAdapter) ActionButton() *widget.Button { return nil } func (a *albumsPageAdapter) ActionButton() *widget.Button { return nil }
func (a *albumsPageAdapter) Iter(sortOrder string, filter mediaprovider.AlbumFilter) widgets.GridViewIterator { func (a *albumsPageAdapter) Iter(sortOrderIdx int, filter mediaprovider.AlbumFilter) widgets.GridViewIterator {
sortOrder := a.mp.AlbumSortOrders()[sortOrderIdx]
return widgets.NewGridViewAlbumIterator(a.mp.IterateAlbums(sortOrder, filter)) return widgets.NewGridViewAlbumIterator(a.mp.IterateAlbums(sortOrder, filter))
} }
+18 -12
View File
@@ -15,6 +15,7 @@ import (
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
@@ -50,9 +51,14 @@ type ArtistPage struct {
container *fyne.Container container *fyne.Container
} }
const (
viewTopTracks = "Top Tracks"
viewDiscography = "Discography"
)
func NewArtistPage(artistID string, cfg *backend.ArtistPageConfig, pool *util.WidgetPool, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager, contr *controller.Controller) *ArtistPage { func NewArtistPage(artistID string, cfg *backend.ArtistPageConfig, pool *util.WidgetPool, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager, contr *controller.Controller) *ArtistPage {
activeView := 0 activeView := 0
if cfg.InitialView == "Top Tracks" { if cfg.InitialView == viewTopTracks {
activeView = 1 activeView = 1
} }
return newArtistPage(artistID, cfg, pool, pm, mp, im, contr, activeView, widgets.TracklistSort{}) return newArtistPage(artistID, cfg, pool, pm, mp, im, contr, activeView, widgets.TracklistSort{})
@@ -81,7 +87,7 @@ func newArtistPage(artistID string, cfg *backend.ArtistPageConfig, pool *util.Wi
if img, ok := im.GetCachedArtistImage(artistID); ok { if img, ok := im.GetCachedArtistImage(artistID); ok {
a.header.artistImage.SetImage(img, true /*tappable*/) a.header.artistImage.SetImage(img, true /*tappable*/)
} }
viewToggle := widgets.NewToggleText(0, []string{"Discography", "Top Tracks"}) viewToggle := widgets.NewToggleText(0, []string{lang.L("Discography"), lang.L("Top Tracks")})
viewToggle.SetActivatedLabel(a.activeView) viewToggle.SetActivatedLabel(a.activeView)
viewToggle.OnChanged = a.onViewChange viewToggle.OnChanged = a.onViewChange
//line := canvas.NewLine(theme.TextColor()) //line := canvas.NewLine(theme.TextColor())
@@ -270,9 +276,9 @@ func (a *ArtistPage) onViewChange(num int) {
} }
a.activeView = num a.activeView = num
if num == 1 { if num == 1 {
a.cfg.InitialView = "Top Tracks" a.cfg.InitialView = viewTopTracks
} else { } else {
a.cfg.InitialView = "Discography" a.cfg.InitialView = viewDiscography
} }
} }
@@ -285,7 +291,7 @@ func (s *artistPageState) Restore() Page {
return newArtistPage(s.artistID, s.cfg, s.pool, s.pm, s.mp, s.im, s.contr, s.activeView, s.trackSort) return newArtistPage(s.artistID, s.cfg, s.pool, s.pm, s.mp, s.im, s.contr, s.activeView, s.trackSort)
} }
const artistBioNotAvailableStr = "Artist biography not available." const artistBioNotAvailableKey = "Artist biography not available."
type ArtistPageHeader struct { type ArtistPageHeader struct {
widget.BaseWidget widget.BaseWidget
@@ -310,7 +316,7 @@ func NewArtistPageHeader(page *ArtistPage) *ArtistPageHeader {
a := &ArtistPageHeader{ a := &ArtistPageHeader{
artistPage: page, artistPage: page,
titleDisp: widget.NewRichTextWithText(""), titleDisp: widget.NewRichTextWithText(""),
biographyDisp: widgets.NewMaxRowsLabel(5, artistBioNotAvailableStr), biographyDisp: widgets.NewMaxRowsLabel(5, lang.L(artistBioNotAvailableKey)),
similarArtists: container.NewHBox(), similarArtists: container.NewHBox(),
} }
a.titleDisp.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{ a.titleDisp.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{
@@ -323,10 +329,10 @@ func NewArtistPageHeader(page *ArtistPage) *ArtistPageHeader {
} }
} }
a.favoriteBtn = widgets.NewFavoriteButton(func() { go a.toggleFavorited() }) a.favoriteBtn = widgets.NewFavoriteButton(func() { go a.toggleFavorited() })
a.playBtn = widget.NewButtonWithIcon("Play Discography", theme.MediaPlayIcon(), func() { a.playBtn = widget.NewButtonWithIcon(lang.L("Play Discography"), theme.MediaPlayIcon(), func() {
go a.artistPage.pm.PlayArtistDiscography(a.artistID, false /*shuffle*/) go a.artistPage.pm.PlayArtistDiscography(a.artistID, false /*shuffle*/)
}) })
a.playRadioBtn = widget.NewButtonWithIcon("Play Artist Radio", myTheme.ShuffleIcon, func() { a.playRadioBtn = widget.NewButtonWithIcon(lang.L("Play Artist Radio"), myTheme.ShuffleIcon, func() {
// must not pass playArtistRadio func directly to NewButton // must not pass playArtistRadio func directly to NewButton
// because the artistPage bound to this header can change when reused // because the artistPage bound to this header can change when reused
a.artistPage.playArtistRadio() a.artistPage.playArtistRadio()
@@ -336,11 +342,11 @@ func NewArtistPageHeader(page *ArtistPage) *ArtistPageHeader {
a.menuBtn = widget.NewButtonWithIcon("", theme.MoreHorizontalIcon(), nil) a.menuBtn = widget.NewButtonWithIcon("", theme.MoreHorizontalIcon(), nil)
a.menuBtn.OnTapped = func() { a.menuBtn.OnTapped = func() {
if pop == nil { if pop == nil {
shuffleTracks := fyne.NewMenuItem("Shuffle tracks", func() { shuffleTracks := fyne.NewMenuItem(lang.L("Shuffle tracks"), func() {
go a.artistPage.pm.PlayArtistDiscography(a.artistID, true /*shuffle*/) go a.artistPage.pm.PlayArtistDiscography(a.artistID, true /*shuffle*/)
}) })
shuffleTracks.Icon = myTheme.TracksIcon shuffleTracks.Icon = myTheme.TracksIcon
shuffleAlbums := fyne.NewMenuItem("Shuffle albums", func() { shuffleAlbums := fyne.NewMenuItem(lang.L("Shuffle albums"), func() {
go a.artistPage.pm.ShuffleArtistAlbums(a.artistID) go a.artistPage.pm.ShuffleArtistAlbums(a.artistID)
}) })
shuffleAlbums.Icon = myTheme.AlbumIcon shuffleAlbums.Icon = myTheme.AlbumIcon
@@ -374,7 +380,7 @@ func (a *ArtistPageHeader) Clear() {
a.artistID = "" a.artistID = ""
a.favoriteBtn.IsFavorited = false a.favoriteBtn.IsFavorited = false
a.titleDisp.Segments[0].(*widget.TextSegment).Text = "" a.titleDisp.Segments[0].(*widget.TextSegment).Text = ""
a.biographyDisp.Text = artistBioNotAvailableStr a.biographyDisp.Text = lang.L(artistBioNotAvailableKey)
for _, obj := range a.similarArtists.Objects { for _, obj := range a.similarArtists.Objects {
obj.Hide() obj.Hide()
} }
@@ -410,7 +416,7 @@ func (a *ArtistPageHeader) UpdateInfo(info *mediaprovider.ArtistInfo) {
} }
if len(a.similarArtists.Objects) == 0 { if len(a.similarArtists.Objects) == 0 {
a.similarArtists.Add(widget.NewLabel("Similar Artists:")) a.similarArtists.Add(widget.NewLabel(lang.L("Similar artists") + ":"))
} }
for _, obj := range a.similarArtists.Objects { for _, obj := range a.similarArtists.Objects {
obj.Hide() obj.Hide()
+14 -9
View File
@@ -4,9 +4,11 @@ import (
"slices" "slices"
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
"github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/sharedutil"
"github.com/dweymouth/supersonic/ui/controller" "github.com/dweymouth/supersonic/ui/controller"
myTheme "github.com/dweymouth/supersonic/ui/theme" myTheme "github.com/dweymouth/supersonic/ui/theme"
"github.com/dweymouth/supersonic/ui/util" "github.com/dweymouth/supersonic/ui/util"
@@ -26,7 +28,7 @@ func NewArtistsPage(cfg *backend.ArtistsPageConfig, pool *util.WidgetPool, contr
return NewGridViewPage(adapter, pool, mp, im) return NewGridViewPage(adapter, pool, mp, im)
} }
func (a *artistsPageAdapter) Title() string { return "Artists" } func (a *artistsPageAdapter) Title() string { return lang.L("Artists") }
func (a *artistsPageAdapter) Filter() mediaprovider.ArtistFilter { func (a *artistsPageAdapter) Filter() mediaprovider.ArtistFilter {
if a.filter == nil { if a.filter == nil {
@@ -45,22 +47,25 @@ func (a *artistsPageAdapter) PlaceholderResource() fyne.Resource { return myThem
func (a *artistsPageAdapter) Route() controller.Route { return controller.ArtistsRoute() } func (a *artistsPageAdapter) Route() controller.Route { return controller.ArtistsRoute() }
func (a *artistsPageAdapter) SortOrders() ([]string, string) { func (a *artistsPageAdapter) SortOrders() ([]string, int) {
orders := a.mp.ArtistSortOrders() orders := a.mp.ArtistSortOrders()
sortOrder := a.cfg.SortOrder sortOrder := slices.Index(orders, a.cfg.SortOrder)
if !slices.Contains(orders, sortOrder) { if sortOrder < 0 {
sortOrder = string(orders[0]) sortOrder = 0
} }
return orders, sortOrder
translatedOrders := sharedutil.MapSlice(orders, func(s string) string { return lang.L(s) })
return translatedOrders, sortOrder
} }
func (a *artistsPageAdapter) SaveSortOrder(order string) { func (a *artistsPageAdapter) SaveSortOrder(orderIdx int) {
a.cfg.SortOrder = order a.cfg.SortOrder = a.mp.ArtistSortOrders()[orderIdx]
} }
func (a *artistsPageAdapter) ActionButton() *widget.Button { return nil } func (a *artistsPageAdapter) ActionButton() *widget.Button { return nil }
func (a *artistsPageAdapter) Iter(sortOrder string, filter mediaprovider.ArtistFilter) widgets.GridViewIterator { func (a *artistsPageAdapter) Iter(sortOrderIdx int, filter mediaprovider.ArtistFilter) widgets.GridViewIterator {
sortOrder := a.mp.ArtistSortOrders()[sortOrderIdx]
return widgets.NewGridViewArtistIterator(a.mp.IterateArtists(sortOrder, filter)) return widgets.NewGridViewArtistIterator(a.mp.IterateArtists(sortOrder, filter))
} }
+5 -4
View File
@@ -14,6 +14,7 @@ import (
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
@@ -85,7 +86,7 @@ func NewFavoritesPage(cfg *backend.FavoritesPageConfig, pool *util.WidgetPool, c
} }
func (a *FavoritesPage) createHeader(activeBtnIdx int) { func (a *FavoritesPage) createHeader(activeBtnIdx int) {
a.titleDisp = widget.NewRichTextWithText("Favorites") a.titleDisp = widget.NewRichTextWithText(lang.L("Favorites"))
a.titleDisp.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{ a.titleDisp.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{
SizeName: theme.SizeNameHeadingText, SizeName: theme.SizeNameHeadingText,
} }
@@ -94,7 +95,7 @@ func (a *FavoritesPage) createHeader(activeBtnIdx int) {
widget.NewButtonWithIcon("", myTheme.ArtistIcon, a.onShowFavoriteArtists), widget.NewButtonWithIcon("", myTheme.ArtistIcon, a.onShowFavoriteArtists),
widget.NewButtonWithIcon("", myTheme.TracksIcon, a.onShowFavoriteSongs)) widget.NewButtonWithIcon("", myTheme.TracksIcon, a.onShowFavoriteSongs))
a.searcher = widgets.NewSearchEntry() a.searcher = widgets.NewSearchEntry()
a.searcher.PlaceHolder = "Search page" a.searcher.PlaceHolder = lang.L("Search page")
a.searcher.OnSearched = a.OnSearched a.searcher.OnSearched = a.OnSearched
a.searcher.Entry.Text = a.searchText a.searcher.Entry.Text = a.searchText
a.filterBtn = widgets.NewAlbumFilterButton(a.filter, a.mp.GetGenres) a.filterBtn = widgets.NewAlbumFilterButton(a.filter, a.mp.GetGenres)
@@ -364,9 +365,9 @@ func (a *FavoritesPage) onShowFavoriteArtists() {
func buildArtistGridViewModel(artists []*mediaprovider.Artist) []widgets.GridViewItemModel { func buildArtistGridViewModel(artists []*mediaprovider.Artist) []widgets.GridViewItemModel {
model := make([]widgets.GridViewItemModel, 0) model := make([]widgets.GridViewItemModel, 0)
for _, ar := range artists { for _, ar := range artists {
albums := "albums" albums := lang.L("albums")
if ar.AlbumCount == 1 { if ar.AlbumCount == 1 {
albums = "album" albums = lang.L("album")
} }
model = append(model, widgets.GridViewItemModel{ model = append(model, widgets.GridViewItemModel{
ID: ar.ID, ID: ar.ID,
+4 -3
View File
@@ -9,6 +9,7 @@ import (
"github.com/dweymouth/supersonic/ui/widgets" "github.com/dweymouth/supersonic/ui/widgets"
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
) )
@@ -58,11 +59,11 @@ func (g *genrePageAdapter) Route() controller.Route {
func (g *genrePageAdapter) ActionButton() *widget.Button { func (g *genrePageAdapter) ActionButton() *widget.Button {
fn := func() { go g.pm.PlayRandomSongs(g.genre) } fn := func() { go g.pm.PlayRandomSongs(g.genre) }
return widget.NewButtonWithIcon("Play random", myTheme.ShuffleIcon, fn) return widget.NewButtonWithIcon(lang.L("Play random"), myTheme.ShuffleIcon, fn)
} }
func (a *genrePageAdapter) Iter(sortOrder string, filter mediaprovider.AlbumFilter) widgets.GridViewIterator { func (a *genrePageAdapter) Iter(sortOrderIdx int, filter mediaprovider.AlbumFilter) widgets.GridViewIterator {
return widgets.NewGridViewAlbumIterator(a.mp.IterateAlbums(sortOrder, filter)) return widgets.NewGridViewAlbumIterator(a.mp.IterateAlbums("", filter))
} }
func (a *genrePageAdapter) SearchIter(query string, filter mediaprovider.AlbumFilter) widgets.GridViewIterator { func (a *genrePageAdapter) SearchIter(query string, filter mediaprovider.AlbumFilter) widgets.GridViewIterator {
+12 -6
View File
@@ -14,6 +14,7 @@ import (
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
@@ -42,14 +43,14 @@ func newGenresPage(contr *controller.Controller, mp mediaprovider.MediaProvider,
a := &GenresPage{ a := &GenresPage{
contr: contr, contr: contr,
mp: mp, mp: mp,
titleDisp: widget.NewRichTextWithText("Genres"), titleDisp: widget.NewRichTextWithText(lang.L("Genres")),
} }
a.ExtendBaseWidget(a) a.ExtendBaseWidget(a)
a.titleDisp.Segments[0].(*widget.TextSegment).Style.SizeName = theme.SizeNameHeadingText a.titleDisp.Segments[0].(*widget.TextSegment).Style.SizeName = theme.SizeNameHeadingText
a.list = NewGenreList(sorting) a.list = NewGenreList(sorting)
a.list.OnNavTo = func(id string) { a.contr.NavigateTo(controller.GenreRoute(id)) } a.list.OnNavTo = func(id string) { a.contr.NavigateTo(controller.GenreRoute(id)) }
a.searcher = widgets.NewSearchEntry() a.searcher = widgets.NewSearchEntry()
a.searcher.PlaceHolder = "Search page" a.searcher.PlaceHolder = lang.L("Search page")
a.searcher.OnSearched = a.onSearched a.searcher.OnSearched = a.onSearched
a.searcher.Entry.Text = searchText a.searcher.Entry.Text = searchText
a.buildContainer() a.buildContainer()
@@ -188,15 +189,20 @@ func NewGenreListRow(layout *layouts.ColumnsLayout) *GenreListRow {
} }
func NewGenreList(sorting widgets.ListHeaderSort) *GenreList { func NewGenreList(sorting widgets.ListHeaderSort) *GenreList {
albumCount := lang.L("Album count")
trackCount := lang.L("Track count")
albumW := widget.NewLabel(albumCount).MinSize().Width + 15 /*room for sort icon */
trackW := widget.NewLabel(trackCount).MinSize().Width + 15 /*room for sort icon */
a := &GenreList{ a := &GenreList{
sorting: sorting, sorting: sorting,
columnsLayout: layouts.NewColumnsLayout([]float32{-1, 125, 125}), columnsLayout: layouts.NewColumnsLayout([]float32{-1, albumW, trackW}),
} }
a.ExtendBaseWidget(a) a.ExtendBaseWidget(a)
a.hdr = widgets.NewListHeader([]widgets.ListColumn{ a.hdr = widgets.NewListHeader([]widgets.ListColumn{
{Text: "Name", Alignment: fyne.TextAlignLeading, CanToggleVisible: false}, {Text: lang.L("Name"), Alignment: fyne.TextAlignLeading, CanToggleVisible: false},
{Text: "Album Count", Alignment: fyne.TextAlignTrailing, CanToggleVisible: false}, {Text: albumCount, Alignment: fyne.TextAlignTrailing, CanToggleVisible: false},
{Text: "Track Count", 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
+23 -17
View File
@@ -9,6 +9,7 @@ import (
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
@@ -61,7 +62,7 @@ type GridViewPageAdapter[M, F any] interface {
// Returns the iterator for the given sortOrder and filter. // Returns the iterator for the given sortOrder and filter.
// (Non-media pages can ignore the filter argument) // (Non-media pages can ignore the filter argument)
Iter(sortOrder string, filter mediaprovider.MediaFilter[M, F]) widgets.GridViewIterator Iter(sortOrderIdx int, filter mediaprovider.MediaFilter[M, F]) widgets.GridViewIterator
// Returns the iterator for the given search query and filter. // Returns the iterator for the given search query and filter.
SearchIter(query string, filter mediaprovider.MediaFilter[M, F]) widgets.GridViewIterator SearchIter(query string, filter mediaprovider.MediaFilter[M, F]) widgets.GridViewIterator
@@ -76,11 +77,12 @@ type GridViewPageAdapter[M, F any] interface {
} }
type SortableGridViewPageAdapter interface { type SortableGridViewPageAdapter interface {
// Returns the list of sort orders and the initially selected sort order // Returns the list of sort orders
SortOrders() ([]string, string) // and the index of the initially selected sort order
SortOrders() ([]string, int)
// Saves the given sort order setting. // Saves the given sort order setting, the selected index is passed.
SaveSortOrder(string) SaveSortOrder(int)
} }
type sortOrderSelect struct { type sortOrderSelect struct {
@@ -120,7 +122,7 @@ func NewGridViewPage[M, F any](
gp.createTitleAndSort() gp.createTitleAndSort()
_, canShare := mp.(mediaprovider.SupportsSharing) _, canShare := mp.(mediaprovider.SupportsSharing)
iter := adapter.Iter(gp.getSortOrder(), gp.getFilter()) iter := adapter.Iter(gp.getSortOrderIdx(), gp.getFilter())
if g := pool.Obtain(util.WidgetTypeGridView); g != nil { if g := pool.Obtain(util.WidgetTypeGridView); g != nil {
gp.grid = g.(*widgets.GridView) gp.grid = g.(*widgets.GridView)
gp.grid.Placeholder = adapter.PlaceholderResource() gp.grid.Placeholder = adapter.PlaceholderResource()
@@ -143,13 +145,13 @@ func (g *GridViewPage[M, F]) createTitleAndSort() {
if s, ok := g.adapter.(SortableGridViewPageAdapter); ok { if s, ok := g.adapter.(SortableGridViewPageAdapter); ok {
sorts, selected := s.SortOrders() sorts, selected := s.SortOrders()
g.sortOrder = NewSortOrderSelect(sorts, g.onSortOrderChanged) g.sortOrder = NewSortOrderSelect(sorts, g.onSortOrderChanged)
g.sortOrder.Selected = selected g.sortOrder.SetSelectedIndex(selected)
} }
} }
func (g *GridViewPage[M, F]) createSearchAndFilter() { func (g *GridViewPage[M, F]) createSearchAndFilter() {
g.searcher = widgets.NewSearchEntry() g.searcher = widgets.NewSearchEntry()
g.searcher.PlaceHolder = "Search page" g.searcher.PlaceHolder = lang.L("Search page")
g.searcher.Text = g.searchText g.searcher.Text = g.searchText
g.searcher.OnSearched = g.OnSearched g.searcher.OnSearched = g.OnSearched
g.filterBtn = g.adapter.FilterButton() g.filterBtn = g.adapter.FilterButton()
@@ -179,7 +181,7 @@ func (g *GridViewPage[M, F]) Reload() {
if g.searchText != "" { if g.searchText != "" {
g.doSearch(g.searchText) g.doSearch(g.searchText)
} else { } else {
g.grid.Reset(g.adapter.Iter(g.getSortOrder(), g.getFilter())) g.grid.Reset(g.adapter.Iter(g.getSortOrderIdx(), g.getFilter()))
} }
} }
@@ -222,20 +224,24 @@ func (g *GridViewPage[M, F]) doSearch(query string) {
g.grid.Reset(g.adapter.SearchIter(query, g.getFilter())) g.grid.Reset(g.adapter.SearchIter(query, g.getFilter()))
} }
func (g *GridViewPage[M, F]) onSortOrderChanged(order string) { func (g *GridViewPage[M, F]) onSortOrderChanged(_ string) {
g.adapter.(SortableGridViewPageAdapter).SaveSortOrder(g.getSortOrder()) if g.grid == nil {
g.grid.Reset(g.adapter.Iter(g.getSortOrder(), g.getFilter())) return // callback from initializing
}
g.adapter.(SortableGridViewPageAdapter).SaveSortOrder(g.getSortOrderIdx())
g.grid.Reset(g.adapter.Iter(g.getSortOrderIdx(), g.getFilter()))
} }
func (g *GridViewPage[M, F]) getFilter() mediaprovider.MediaFilter[M, F] { func (g *GridViewPage[M, F]) getFilter() mediaprovider.MediaFilter[M, F] {
return g.filter return g.filter
} }
func (g *GridViewPage[M, F]) getSortOrder() string { func (g *GridViewPage[M, F]) getSortOrderIdx() int {
if g.sortOrder != nil { if g.sortOrder != nil {
return g.sortOrder.Selected return g.sortOrder.SelectedIndex()
} }
return "" return 0
} }
func (g *GridViewPage[M, F]) CreateRenderer() fyne.WidgetRenderer { func (g *GridViewPage[M, F]) CreateRenderer() fyne.WidgetRenderer {
@@ -249,7 +255,7 @@ type savedGridViewPage[M, F any] struct {
searchText string searchText string
filter mediaprovider.MediaFilter[M, F] filter mediaprovider.MediaFilter[M, F]
pool *util.WidgetPool pool *util.WidgetPool
sortOrder string sortOrderIdx int
gridState *widgets.GridViewState gridState *widgets.GridViewState
searchGridState *widgets.GridViewState searchGridState *widgets.GridViewState
} }
@@ -262,7 +268,7 @@ func (g *GridViewPage[M, F]) Save() SavedPage {
im: g.im, im: g.im,
searchText: g.searchText, searchText: g.searchText,
filter: g.filter, filter: g.filter,
sortOrder: g.getSortOrder(), sortOrderIdx: g.getSortOrderIdx(),
gridState: g.gridState, gridState: g.gridState,
searchGridState: g.searchGridState, searchGridState: g.searchGridState,
} }
+12 -10
View File
@@ -27,6 +27,7 @@ import (
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
@@ -181,7 +182,7 @@ func NewNowPlayingPage(
a.pm.LoadItems(items, backend.InsertNext, false) a.pm.LoadItems(items, backend.InsertNext, false)
} }
a.lyricsViewer = widgets.NewLyricsViewer() a.lyricsViewer = widgets.NewLyricsViewer()
a.statusLabel = widget.NewLabel("Stopped") a.statusLabel = widget.NewLabel(lang.L("Stopped"))
a.Reload() a.Reload()
return a return a
@@ -203,12 +204,12 @@ func (a *NowPlayingPage) CreateRenderer() fyne.WidgetRenderer {
a.lyricsLoading = widgets.NewLoadingDots() a.lyricsLoading = widgets.NewLoadingDots()
a.relatedLoading = widgets.NewLoadingDots() a.relatedLoading = widgets.NewLoadingDots()
a.tabs = container.NewAppTabs( a.tabs = container.NewAppTabs(
container.NewTabItem("Play Queue", container.NewTabItem(lang.L("Play Queue"),
container.NewBorder(layout.NewSpacer(), nil, nil, nil, a.queueList)), container.NewBorder(layout.NewSpacer(), nil, nil, nil, a.queueList)),
container.NewTabItem("Lyrics", container.NewStack( container.NewTabItem(lang.L("Lyrics"), container.NewStack(
a.lyricsViewer, a.lyricsViewer,
container.NewCenter(a.lyricsLoading))), container.NewCenter(a.lyricsLoading))),
container.NewTabItem("Related", container.NewStack( container.NewTabItem(lang.L("Related"), container.NewStack(
a.relatedList, a.relatedList,
container.NewCenter(a.relatedLoading))), container.NewCenter(a.relatedLoading))),
) )
@@ -537,12 +538,13 @@ func (a *NowPlayingPage) formatStatusLine() {
curPlayer := a.pm.CurrentPlayer() curPlayer := a.pm.CurrentPlayer()
playerStats := curPlayer.GetStatus() playerStats := curPlayer.GetStatus()
lastStatus := a.statusLabel.Text lastStatus := a.statusLabel.Text
state := "Stopped" stopped := lang.L("Stopped")
state := stopped
switch playerStats.State { switch playerStats.State {
case player.Paused: case player.Paused:
state = "Paused" state = lang.L("Paused")
case player.Playing: case player.Playing:
state = "Playing" state = lang.L("Playing")
} }
dur := 0.0 dur := 0.0
@@ -551,7 +553,7 @@ func (a *NowPlayingPage) formatStatusLine() {
} }
statusSuffix := "" statusSuffix := ""
trackNum := 0 trackNum := 0
if state != "Stopped" { if state != stopped {
trackNum = a.pm.NowPlayingIndex() + 1 trackNum = a.pm.NowPlayingIndex() + 1
statusSuffix = fmt.Sprintf(" %s/%s", statusSuffix = fmt.Sprintf(" %s/%s",
util.SecondsToMMSS(playerStats.TimePos), util.SecondsToMMSS(playerStats.TimePos),
@@ -561,14 +563,14 @@ func (a *NowPlayingPage) formatStatusLine() {
len(a.queue), statusSuffix) len(a.queue), statusSuffix)
mediaInfo := "" mediaInfo := ""
if state != "Stopped" { if state != stopped {
mediaInfo = a.formatMediaInfoStr(curPlayer) mediaInfo = a.formatMediaInfoStr(curPlayer)
} }
if mediaInfo != "" { if mediaInfo != "" {
mediaInfo = " · " + mediaInfo mediaInfo = " · " + mediaInfo
} }
a.statusLabel.Text = fmt.Sprintf("%s%s | Total time: %s", status, mediaInfo, util.SecondsToTimeString(a.totalTime)) a.statusLabel.Text = fmt.Sprintf("%s%s | %s: %s", status, mediaInfo, lang.L("Total time"), util.SecondsToTimeString(a.totalTime))
if lastStatus != a.statusLabel.Text { if lastStatus != a.statusLabel.Text {
a.statusLabel.Refresh() a.statusLabel.Refresh()
} }
+17 -13
View File
@@ -14,6 +14,7 @@ import (
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
@@ -89,7 +90,7 @@ func newPlaylistPage(
a.tracklist.OnReorderTracks = a.doSetNewTrackOrder a.tracklist.OnReorderTracks = a.doSetNewTrackOrder
_, canRate := a.sm.Server.(mediaprovider.SupportsRating) _, canRate := a.sm.Server.(mediaprovider.SupportsRating)
_, canShare := a.sm.Server.(mediaprovider.SupportsSharing) _, canShare := a.sm.Server.(mediaprovider.SupportsSharing)
remove := fyne.NewMenuItem("Remove from playlist", a.onRemoveSelectedFromPlaylist) remove := fyne.NewMenuItem(lang.L("Remove from playlist"), a.onRemoveSelectedFromPlaylist)
remove.Icon = theme.ContentClearIcon() remove.Icon = theme.ContentClearIcon()
a.tracklist.Options = widgets.TracklistOptions{ a.tracklist.Options = widgets.TracklistOptions{
Reorderable: true, Reorderable: true,
@@ -252,17 +253,17 @@ func NewPlaylistPageHeader(page *PlaylistPage) *PlaylistPageHeader {
a.ownerLabel = util.NewTruncatingLabel() a.ownerLabel = util.NewTruncatingLabel()
a.createdAtLabel = widget.NewLabel("") a.createdAtLabel = widget.NewLabel("")
a.trackTimeLabel = widget.NewLabel("") a.trackTimeLabel = widget.NewLabel("")
a.editButton = widget.NewButtonWithIcon("Edit", theme.DocumentCreateIcon(), func() { a.editButton = widget.NewButtonWithIcon(lang.L("Edit"), theme.DocumentCreateIcon(), func() {
if a.playlistInfo != nil { if a.playlistInfo != nil {
a.page.contr.DoEditPlaylistWorkflow(&a.playlistInfo.Playlist) a.page.contr.DoEditPlaylistWorkflow(&a.playlistInfo.Playlist)
} }
}) })
a.editButton.Hidden = true a.editButton.Hidden = true
playButton := widget.NewButtonWithIcon("Play", theme.MediaPlayIcon(), func() { playButton := widget.NewButtonWithIcon(lang.L("Play"), theme.MediaPlayIcon(), func() {
a.page.pm.LoadTracks(a.page.tracks, backend.Replace, false) a.page.pm.LoadTracks(a.page.tracks, backend.Replace, false)
a.page.pm.PlayFromBeginning() a.page.pm.PlayFromBeginning()
}) })
shuffleBtn := widget.NewButtonWithIcon("Shuffle", myTheme.ShuffleIcon, func() { shuffleBtn := widget.NewButtonWithIcon(lang.L("Shuffle"), myTheme.ShuffleIcon, func() {
a.page.pm.LoadTracks(a.page.tracks, backend.Replace, true) a.page.pm.LoadTracks(a.page.tracks, backend.Replace, true)
a.page.pm.PlayFromBeginning() a.page.pm.PlayFromBeginning()
}) })
@@ -270,20 +271,20 @@ func NewPlaylistPageHeader(page *PlaylistPage) *PlaylistPageHeader {
menuBtn := widget.NewButtonWithIcon("", theme.MoreHorizontalIcon(), nil) menuBtn := widget.NewButtonWithIcon("", theme.MoreHorizontalIcon(), nil)
menuBtn.OnTapped = func() { menuBtn.OnTapped = func() {
if pop == nil { if pop == nil {
playNext := fyne.NewMenuItem("Play next", func() { playNext := fyne.NewMenuItem(lang.L("Play next"), func() {
go a.page.pm.LoadPlaylist(a.page.playlistID, backend.InsertNext, false) go a.page.pm.LoadPlaylist(a.page.playlistID, backend.InsertNext, false)
}) })
playNext.Icon = myTheme.PlayNextIcon playNext.Icon = myTheme.PlayNextIcon
queue := fyne.NewMenuItem("Add to queue", func() { queue := fyne.NewMenuItem(lang.L("Add to queue"), func() {
go a.page.pm.LoadPlaylist(a.page.playlistID, backend.Append, false) go a.page.pm.LoadPlaylist(a.page.playlistID, backend.Append, false)
}) })
queue.Icon = theme.ContentAddIcon() queue.Icon = theme.ContentAddIcon()
playlist := fyne.NewMenuItem("Add to playlist...", func() { playlist := fyne.NewMenuItem(lang.L("Add to playlist")+"...", func() {
a.page.contr.DoAddTracksToPlaylistWorkflow( a.page.contr.DoAddTracksToPlaylistWorkflow(
sharedutil.TracksToIDs(a.page.tracks)) sharedutil.TracksToIDs(a.page.tracks))
}) })
playlist.Icon = myTheme.PlaylistIcon playlist.Icon = myTheme.PlaylistIcon
download := fyne.NewMenuItem("Download...", func() { download := fyne.NewMenuItem(lang.L("Download")+"...", func() {
a.page.contr.ShowDownloadDialog(a.page.tracks, a.titleLabel.String()) a.page.contr.ShowDownloadDialog(a.page.tracks, a.titleLabel.String())
}) })
download.Icon = theme.DownloadIcon() download.Icon = theme.DownloadIcon()
@@ -342,17 +343,20 @@ func (a *PlaylistPageHeader) Update(playlist *mediaprovider.PlaylistWithTracks)
} }
func (a *PlaylistPageHeader) formatPlaylistOwnerStr(p *mediaprovider.PlaylistWithTracks) string { func (a *PlaylistPageHeader) formatPlaylistOwnerStr(p *mediaprovider.PlaylistWithTracks) string {
pubPriv := "Public" pubPriv := lang.L("Public")
if !p.Public { if !p.Public {
pubPriv = "Private" pubPriv = lang.L("Private")
} }
return fmt.Sprintf("%s playlist by %s", pubPriv, p.Owner) playlistBy := lang.L("playlist by")
return fmt.Sprintf("%s %s %s", pubPriv, playlistBy, p.Owner)
} }
func (a *PlaylistPageHeader) formatPlaylistTrackTimeStr(p *mediaprovider.PlaylistWithTracks) string { func (a *PlaylistPageHeader) formatPlaylistTrackTimeStr(p *mediaprovider.PlaylistWithTracks) string {
tracks := "tracks" var tracks string
if p.TrackCount == 1 { if p.TrackCount == 1 {
tracks = "track" tracks = lang.L("track")
} else {
tracks = lang.L("tracks")
} }
return fmt.Sprintf("%d %s, %s", p.TrackCount, tracks, util.SecondsToTimeString(float64(p.Duration))) return fmt.Sprintf("%d %s, %s", p.TrackCount, tracks, util.SecondsToTimeString(float64(p.Duration)))
} }
+18 -12
View File
@@ -19,6 +19,7 @@ import (
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
@@ -71,14 +72,14 @@ func newPlaylistsPage(
mp: mp, mp: mp,
contr: contr, contr: contr,
listSort: listSort, listSort: listSort,
titleDisp: widget.NewRichTextWithText("Playlists"), titleDisp: widget.NewRichTextWithText(lang.L("Playlists")),
initialListScrollPos: listScrollPos, initialListScrollPos: listScrollPos,
initialGridScrollPos: gridScrollPos, initialGridScrollPos: gridScrollPos,
} }
a.ExtendBaseWidget(a) a.ExtendBaseWidget(a)
a.titleDisp.Segments[0].(*widget.TextSegment).Style.SizeName = theme.SizeNameHeadingText a.titleDisp.Segments[0].(*widget.TextSegment).Style.SizeName = theme.SizeNameHeadingText
a.searcher = widgets.NewSearchEntry() a.searcher = widgets.NewSearchEntry()
a.searcher.PlaceHolder = "Search page" a.searcher.PlaceHolder = lang.L("Search page")
a.searcher.OnSearched = a.onSearched a.searcher.OnSearched = a.onSearched
a.searcher.Entry.Text = searchText a.searcher.Entry.Text = searchText
a.viewToggle = widgets.NewToggleButtonGroup(0, a.viewToggle = widgets.NewToggleButtonGroup(0,
@@ -193,9 +194,11 @@ func (a *PlaylistsPage) showGridView() {
func createPlaylistGridViewModel(playlists []*mediaprovider.Playlist) []widgets.GridViewItemModel { func createPlaylistGridViewModel(playlists []*mediaprovider.Playlist) []widgets.GridViewItemModel {
return sharedutil.MapSlice(playlists, func(pl *mediaprovider.Playlist) widgets.GridViewItemModel { return sharedutil.MapSlice(playlists, func(pl *mediaprovider.Playlist) widgets.GridViewItemModel {
tracks := "tracks" var tracks string
if pl.TrackCount == 1 { if pl.TrackCount == 1 {
tracks = "track" tracks = lang.L("track")
} else {
tracks = lang.L("tracks")
} }
return widgets.GridViewItemModel{ return widgets.GridViewItemModel{
Name: pl.Name, Name: pl.Name,
@@ -334,10 +337,9 @@ type PlaylistList struct {
func NewPlaylistList(initialSort widgets.ListHeaderSort) *PlaylistList { func NewPlaylistList(initialSort widgets.ListHeaderSort) *PlaylistList {
a := &PlaylistList{ a := &PlaylistList{
sorting: initialSort, sorting: initialSort,
columnsLayout: layouts.NewColumnsLayout([]float32{-1, -1, 200, 125}),
} }
a.buildHeader() a.buildHeaderAndLayout()
a.list = widgets.NewFocusList( a.list = widgets.NewFocusList(
func() int { func() int {
return len(a.playlists) return len(a.playlists)
@@ -369,12 +371,16 @@ func NewPlaylistList(initialSort widgets.ListHeaderSort) *PlaylistList {
return a return a
} }
func (p *PlaylistList) buildHeader() { func (p *PlaylistList) buildHeaderAndLayout() {
trackCount := lang.L("Track count")
trackCountWidth := fyne.Max(125, widget.NewLabel(trackCount).MinSize().Width+15)
p.columnsLayout = layouts.NewColumnsLayout([]float32{-1, -1, 200, trackCountWidth})
p.header = widgets.NewListHeader([]widgets.ListColumn{ p.header = widgets.NewListHeader([]widgets.ListColumn{
{Text: "Name", Alignment: fyne.TextAlignLeading, CanToggleVisible: false}, {Text: lang.L("Name"), Alignment: fyne.TextAlignLeading, CanToggleVisible: false},
{Text: "Description", Alignment: fyne.TextAlignLeading, CanToggleVisible: false}, {Text: lang.L("Description"), Alignment: fyne.TextAlignLeading, CanToggleVisible: false},
{Text: "Owner", Alignment: fyne.TextAlignLeading, CanToggleVisible: false}, {Text: lang.L("Owner"), Alignment: fyne.TextAlignLeading, CanToggleVisible: false},
{Text: "Track Count", 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
} }
+10 -9
View File
@@ -15,6 +15,7 @@ import (
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
@@ -48,7 +49,7 @@ func newRadiosPage(contr *controller.Controller, rp mediaprovider.RadioProvider,
contr: contr, contr: contr,
rp: rp, rp: rp,
pm: pm, pm: pm,
titleDisp: widget.NewRichTextWithText("Internet Radio Stations"), titleDisp: widget.NewRichTextWithText(lang.L("Internet Radio Stations")),
} }
a.ExtendBaseWidget(a) a.ExtendBaseWidget(a)
a.titleDisp.Segments[0].(*widget.TextSegment).Style.SizeName = theme.SizeNameHeadingText a.titleDisp.Segments[0].(*widget.TextSegment).Style.SizeName = theme.SizeNameHeadingText
@@ -56,13 +57,13 @@ func newRadiosPage(contr *controller.Controller, rp mediaprovider.RadioProvider,
a.list.OnPlay = a.onPlay a.list.OnPlay = a.onPlay
a.list.OnQueue = a.onQueue a.list.OnQueue = a.onQueue
a.searcher = widgets.NewSearchEntry() a.searcher = widgets.NewSearchEntry()
a.searcher.PlaceHolder = "Search page" a.searcher.PlaceHolder = lang.L("Search page")
a.searcher.OnSearched = a.onSearched a.searcher.OnSearched = a.onSearched
a.searcher.Entry.Text = searchText a.searcher.Entry.Text = searchText
a.noRadiosMsg = container.NewCenter(widgets.NewInfoMessage( a.noRadiosMsg = container.NewCenter(widgets.NewInfoMessage(
"No radio stations available", lang.L("No radio stations available"),
"Configure your music server to add radio stations", lang.L("Configure your music server to add radio stations"),
)) ))
a.noRadiosMsg.Hide() a.noRadiosMsg.Hide()
@@ -248,8 +249,8 @@ func NewRadioList(nowPlayingIDPtr *string) *RadioList {
a.playingIcon = container.NewCenter(widget.NewIcon(playIcon)) a.playingIcon = container.NewCenter(widget.NewIcon(playIcon))
a.ExtendBaseWidget(a) a.ExtendBaseWidget(a)
a.hdr = widgets.NewListHeader([]widgets.ListColumn{ a.hdr = widgets.NewListHeader([]widgets.ListColumn{
{Text: "Name", Alignment: fyne.TextAlignLeading, CanToggleVisible: false}, {Text: lang.L("Name"), Alignment: fyne.TextAlignLeading, CanToggleVisible: false},
{Text: "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(
@@ -314,21 +315,21 @@ func NewRadioList(nowPlayingIDPtr *string) *RadioList {
func (a *RadioList) showMenu(pos fyne.Position) { func (a *RadioList) showMenu(pos fyne.Position) {
if a.menu == nil { if a.menu == nil {
play := fyne.NewMenuItem("Play", func() { play := fyne.NewMenuItem(lang.L("Play"), func() {
if a.OnPlay != nil { if a.OnPlay != nil {
a.OnPlay(a.selected.Item) a.OnPlay(a.selected.Item)
} }
}) })
play.Icon = theme.MediaPlayIcon() play.Icon = theme.MediaPlayIcon()
playNext := fyne.NewMenuItem("Play next", func() { playNext := fyne.NewMenuItem(lang.L("Play next"), func() {
if a.OnQueue != nil { if a.OnQueue != nil {
a.OnQueue(a.selected.Item, true) a.OnQueue(a.selected.Item, true)
} }
}) })
playNext.Icon = myTheme.PlayNextIcon playNext.Icon = myTheme.PlayNextIcon
append := fyne.NewMenuItem("Add to queue", func() { append := fyne.NewMenuItem(lang.L("Add to queue"), func() {
if a.OnQueue != nil { if a.OnQueue != nil {
a.OnQueue(a.selected.Item, false) a.OnQueue(a.selected.Item, false)
} }
+4 -3
View File
@@ -11,6 +11,7 @@ import (
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
) )
@@ -65,11 +66,11 @@ func NewTracksPage(contr *controller.Controller, conf *backend.TracksPageConfig,
} }
contr.ConnectTracklistActions(t.tracklist) contr.ConnectTracklistActions(t.tracklist)
t.title = widget.NewRichTextWithText("All Tracks") t.title = widget.NewRichTextWithText(lang.L("All Tracks"))
t.title.Segments[0].(*widget.TextSegment).Style.SizeName = widget.RichTextStyleHeading.SizeName t.title.Segments[0].(*widget.TextSegment).Style.SizeName = widget.RichTextStyleHeading.SizeName
t.playRandom = widget.NewButtonWithIcon("Play random", theme.ShuffleIcon, t.playRandomSongs) t.playRandom = widget.NewButtonWithIcon(lang.L("Play random"), theme.ShuffleIcon, t.playRandomSongs)
t.searcher = widgets.NewSearchEntry() t.searcher = widgets.NewSearchEntry()
t.searcher.PlaceHolder = "Search page" t.searcher.PlaceHolder = lang.L("Search page")
t.searcher.OnSearched = t.OnSearched t.searcher.OnSearched = t.OnSearched
t.createContainer() t.createContainer()
t.Reload() t.Reload()
+19 -18
View File
@@ -25,6 +25,7 @@ import (
"fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
@@ -246,7 +247,7 @@ func (m *Controller) GetArtistTracks(artistID string) []*mediaprovider.Track {
} }
func (m *Controller) PromptForFirstServer() { func (m *Controller) PromptForFirstServer() {
d := dialogs.NewAddEditServerDialog("Connect to Server", false, nil, m.MainWindow.Canvas().Focus) d := dialogs.NewAddEditServerDialog(lang.L("Connect to Server"), false, nil, m.MainWindow.Canvas().Focus)
pop := widget.NewModalPopUp(d, m.MainWindow.Canvas()) pop := widget.NewModalPopUp(d, m.MainWindow.Canvas())
d.OnSubmit = func() { d.OnSubmit = func() {
d.DisableSubmit() d.DisableSubmit()
@@ -333,7 +334,7 @@ func (m *Controller) DoEditPlaylistWorkflow(playlist *mediaprovider.Playlist) {
} }
dlg.OnDeletePlaylist = func() { dlg.OnDeletePlaylist = func() {
pop.Hide() pop.Hide()
dialog.ShowCustomConfirm("Confirm Delete Playlist", "OK", "Cancel", layout.NewSpacer(), /*custom content*/ dialog.ShowCustomConfirm(lang.L("Confirm Delete Playlist"), lang.L("OK"), lang.L("Cancel"), layout.NewSpacer(), /*custom content*/
func(ok bool) { func(ok bool) {
if !ok { if !ok {
pop.Show() pop.Show()
@@ -380,8 +381,8 @@ func (c *Controller) DoConnectToServerWorkflow(server *backend.ServerConfig) {
canceled := false canceled := false
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
dlg := dialog.NewCustom("Connecting", "Cancel", dlg := dialog.NewCustom(lang.L("Connecting"), lang.L("Cancel"),
widget.NewLabel(fmt.Sprintf("Connecting to %s", server.Nickname)), c.MainWindow) widget.NewLabel(fmt.Sprintf(lang.L("Connecting to")+" %s", server.Nickname)), c.MainWindow)
dlg.SetOnClosed(func() { dlg.SetOnClosed(func() {
canceled = true canceled = true
cancel() cancel()
@@ -414,16 +415,16 @@ func (m *Controller) PromptForLoginAndConnect() {
pop := widget.NewModalPopUp(d, m.MainWindow.Canvas()) pop := widget.NewModalPopUp(d, m.MainWindow.Canvas())
d.OnSubmit = func(server *backend.ServerConfig, password string) { d.OnSubmit = func(server *backend.ServerConfig, password string) {
d.DisableSubmit() d.DisableSubmit()
d.SetInfoText("Testing connection...") d.SetInfoText(lang.L("Testing connection") + "...")
go func() { go func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
err := m.App.ServerManager.TestConnectionAndAuth(ctx, server.ServerConnection, password) err := m.App.ServerManager.TestConnectionAndAuth(ctx, server.ServerConnection, password)
if err == backend.ErrUnreachable { if err == backend.ErrUnreachable {
d.SetErrorText("Server unreachable") d.SetErrorText(lang.L("Server unreachable"))
} else if err != nil { } else if err != nil {
d.SetErrorText("Authentication failed") d.SetErrorText(lang.L("Authentication failed"))
} else { } else {
pop.Hide() pop.Hide()
m.trySetPasswordAndConnectToServer(server, password) m.trySetPasswordAndConnectToServer(server, password)
@@ -434,7 +435,7 @@ func (m *Controller) PromptForLoginAndConnect() {
} }
d.OnEditServer = func(server *backend.ServerConfig) { d.OnEditServer = func(server *backend.ServerConfig) {
pop.Hide() pop.Hide()
editD := dialogs.NewAddEditServerDialog("Edit server", true, server, m.MainWindow.Canvas().Focus) editD := dialogs.NewAddEditServerDialog(lang.L("Edit server"), true, server, m.MainWindow.Canvas().Focus)
editPop := widget.NewModalPopUp(editD, m.MainWindow.Canvas()) editPop := widget.NewModalPopUp(editD, m.MainWindow.Canvas())
editD.OnSubmit = func() { editD.OnSubmit = func() {
d.DisableSubmit() d.DisableSubmit()
@@ -461,7 +462,7 @@ func (m *Controller) PromptForLoginAndConnect() {
} }
d.OnNewServer = func() { d.OnNewServer = func() {
pop.Hide() pop.Hide()
newD := dialogs.NewAddEditServerDialog("Add server", true, nil, m.MainWindow.Canvas().Focus) newD := dialogs.NewAddEditServerDialog(lang.L("Add Server"), true, nil, m.MainWindow.Canvas().Focus)
newPop := widget.NewModalPopUp(newD, m.MainWindow.Canvas()) newPop := widget.NewModalPopUp(newD, m.MainWindow.Canvas())
newD.OnSubmit = func() { newD.OnSubmit = func() {
d.DisableSubmit() d.DisableSubmit()
@@ -491,8 +492,8 @@ func (m *Controller) PromptForLoginAndConnect() {
} }
d.OnDeleteServer = func(server *backend.ServerConfig) { d.OnDeleteServer = func(server *backend.ServerConfig) {
pop.Hide() pop.Hide()
dialog.ShowConfirm("Confirm delete server", dialog.ShowConfirm(lang.L("Confirm Delete Server"),
fmt.Sprintf("Are you sure you want to delete the server %q?", server.Nickname), fmt.Sprintf(lang.L("Are you sure you want to delete the server")+" %q?", server.Nickname),
func(ok bool) { func(ok bool) {
if ok { if ok {
m.App.ServerManager.DeleteServer(server.ID) m.App.ServerManager.DeleteServer(server.ID)
@@ -527,7 +528,7 @@ func (c *Controller) ShowSettingsDialog(themeUpdateCallbk func(), themeFiles map
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)
devs = []mpv.AudioDevice{{Name: "auto", Description: "Autoselect device"}} devs = []mpv.AudioDevice{{Name: "auto", Description: lang.L("Autoselect device")}}
} }
curPlayer := c.App.PlaybackManager.CurrentPlayer() curPlayer := c.App.PlaybackManager.CurrentPlayer()
@@ -634,7 +635,7 @@ func (c *Controller) tryConnectToServer(ctx context.Context, server *backend.Ser
} }
func (c *Controller) testConnectionAndUpdateDialogText(dlg *dialogs.AddEditServerDialog) bool { func (c *Controller) testConnectionAndUpdateDialogText(dlg *dialogs.AddEditServerDialog) bool {
dlg.SetInfoText("Testing connection...") dlg.SetInfoText(lang.L("Testing connection") + "...")
conn := backend.ServerConnection{ conn := backend.ServerConnection{
ServerType: dlg.ServerType, ServerType: dlg.ServerType,
Hostname: dlg.Host, Hostname: dlg.Host,
@@ -646,10 +647,10 @@ func (c *Controller) testConnectionAndUpdateDialogText(dlg *dialogs.AddEditServe
defer cancel() defer cancel()
err := c.App.ServerManager.TestConnectionAndAuth(ctx, conn, dlg.Password) err := c.App.ServerManager.TestConnectionAndAuth(ctx, conn, dlg.Password)
if err == backend.ErrUnreachable { if err == backend.ErrUnreachable {
dlg.SetErrorText("Could not reach server (wrong hostname?)") dlg.SetErrorText(lang.L("Could not reach server") + fmt.Sprintf(" (%s?)", lang.L("wrong URL")))
return false return false
} else if err != nil { } else if err != nil {
dlg.SetErrorText("Authentication failed (wrong username/password)") dlg.SetErrorText(lang.L("Authentication failed") + fmt.Sprintf(" (%s)", lang.L("wrong username/password")))
return false return false
} }
return true return true
@@ -697,7 +698,7 @@ func (c *Controller) ShowShareDialog(id string) {
} }
hyperlink := widget.NewHyperlink(shareUrl.String(), shareUrl) hyperlink := widget.NewHyperlink(shareUrl.String(), shareUrl)
dlg := dialog.NewCustom("Share content", "OK", dlg := dialog.NewCustom(lang.L("Share content"), lang.L("OK"),
container.NewHBox( container.NewHBox(
hyperlink, hyperlink,
widget.NewButtonWithIcon("", theme.ContentCopyIcon(), func() { widget.NewButtonWithIcon("", theme.ContentCopyIcon(), func() {
@@ -787,7 +788,7 @@ func (c *Controller) downloadTrack(track *mediaprovider.Track, filePath string)
} }
log.Printf("Saved song %s to: %s\n", track.Title, filePath) log.Printf("Saved song %s to: %s\n", track.Title, filePath)
c.sendNotification(fmt.Sprintf("Download completed: %s", track.Title), fmt.Sprintf("Saved at: %s", filePath)) c.sendNotification(fmt.Sprintf(lang.L("Download completed")+": %s", track.Title), fmt.Sprintf(lang.L("Saved at")+": %s", filePath))
} }
func (c *Controller) downloadTracks(tracks []*mediaprovider.Track, filePath, downloadName string) { func (c *Controller) downloadTracks(tracks []*mediaprovider.Track, filePath, downloadName string) {
@@ -825,7 +826,7 @@ func (c *Controller) downloadTracks(tracks []*mediaprovider.Track, filePath, dow
log.Printf("Saved song %s to: %s\n", track.Title, filePath) log.Printf("Saved song %s to: %s\n", track.Title, filePath)
} }
c.sendNotification(fmt.Sprintf("Download completed: %s", downloadName), fmt.Sprintf("Saved at: %s", filePath)) c.sendNotification(fmt.Sprintf(lang.L("Download completed")+": %s", downloadName), fmt.Sprintf("Saved at: %s", filePath))
} }
func (c *Controller) sendNotification(title, content string) { func (c *Controller) sendNotification(title, content string) {
+5 -4
View File
@@ -9,6 +9,7 @@ import (
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
@@ -37,7 +38,7 @@ func NewAboutDialog(version string) *AboutDialog {
widget.NewSeparator(), widget.NewSeparator(),
container.NewHBox( container.NewHBox(
layout.NewSpacer(), layout.NewSpacer(),
widget.NewButton("Close", func() { widget.NewButton(lang.L("Close"), func() {
if a.OnDismiss != nil { if a.OnDismiss != nil {
a.OnDismiss() a.OnDismiss()
} }
@@ -59,7 +60,7 @@ func (a *AboutDialog) buildMainTabContainer(version string) *fyne.Container {
title.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = true title.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = true
title.Segments[0].(*widget.TextSegment).Style.SizeName = theme.SizeNameSubHeadingText title.Segments[0].(*widget.TextSegment).Style.SizeName = theme.SizeNameSubHeadingText
title.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignCenter title.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignCenter
versionLbl := newCenterAlignLabel(fmt.Sprintf("version %s", version)) versionLbl := newCenterAlignLabel(fmt.Sprintf("%s %s", lang.L("version"), version))
copyright := newCenterAlignLabel(res.Copyright) copyright := newCenterAlignLabel(res.Copyright)
license := widget.NewRichTextWithText("GNU General Public License version 3 (GPL v3)") license := widget.NewRichTextWithText("GNU General Public License version 3 (GPL v3)")
ts := license.Segments[0].(*widget.TextSegment) ts := license.Segments[0].(*widget.TextSegment)
@@ -69,9 +70,9 @@ func (a *AboutDialog) buildMainTabContainer(version string) *fyne.Container {
kofiUrl, _ := url.Parse(res.KofiURL) kofiUrl, _ := url.Parse(res.KofiURL)
githubKofi := container.NewCenter( githubKofi := container.NewCenter(
container.New(layout.NewCustomPaddedHBoxLayout(-10), container.New(layout.NewCustomPaddedHBoxLayout(-10),
widget.NewHyperlink("Github page", ghUrl), widget.NewHyperlink(lang.L("Github page"), ghUrl),
widget.NewLabel("·"), widget.NewLabel("·"),
widget.NewHyperlink("Support the project", kofiUrl)), widget.NewHyperlink(lang.L("Support the project"), kofiUrl)),
) )
return container.New(&layout.CustomPaddedLayout{TopPadding: 10, BottomPadding: 10}, return container.New(&layout.CustomPaddedLayout{TopPadding: 10, BottomPadding: 10},
+14 -11
View File
@@ -1,11 +1,14 @@
package dialogs package dialogs
import ( import (
"fmt"
"github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend"
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/binding" "fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
@@ -46,7 +49,7 @@ func NewAddEditServerDialog(title string, cancelable bool, prefillServer *backen
titleLabel := widget.NewLabel(title) titleLabel := widget.NewLabel(title)
titleLabel.TextStyle.Bold = true titleLabel.TextStyle.Bold = true
legacyAuthCheck := widget.NewCheckWithData("Use legacy authentication", binding.BindBool(&a.LegacyAuth)) legacyAuthCheck := widget.NewCheckWithData(lang.L("Use legacy authentication"), binding.BindBool(&a.LegacyAuth))
serverTypeChoice := widget.NewRadioGroup([]string{"Subsonic", "Jellyfin"}, func(s string) { serverTypeChoice := widget.NewRadioGroup([]string{"Subsonic", "Jellyfin"}, func(s string) {
a.ServerType = backend.ServerType(s) a.ServerType = backend.ServerType(s)
if s == string(backend.ServerTypeSubsonic) { if s == string(backend.ServerTypeSubsonic) {
@@ -67,15 +70,15 @@ func NewAddEditServerDialog(title string, cancelable bool, prefillServer *backen
userField := widget.NewEntryWithData(binding.BindString(&a.Username)) userField := widget.NewEntryWithData(binding.BindString(&a.Username))
userField.OnSubmitted = func(_ string) { focusHandler(a.passField) } userField.OnSubmitted = func(_ string) { focusHandler(a.passField) }
altHostField := widget.NewEntryWithData(binding.BindString(&a.AltHost)) altHostField := widget.NewEntryWithData(binding.BindString(&a.AltHost))
altHostField.SetPlaceHolder("(optional) https://my-external-domain.net/music") altHostField.SetPlaceHolder(fmt.Sprintf("(%s)", lang.L("optional")) + " https://my-external-domain.net/music")
altHostField.OnSubmitted = func(_ string) { focusHandler(userField) } altHostField.OnSubmitted = func(_ string) { focusHandler(userField) }
hostField := widget.NewEntryWithData(binding.BindString(&a.Host)) hostField := widget.NewEntryWithData(binding.BindString(&a.Host))
hostField.SetPlaceHolder("http://localhost:4533") hostField.SetPlaceHolder("http://localhost:4533")
hostField.OnSubmitted = func(_ string) { focusHandler(altHostField) } hostField.OnSubmitted = func(_ string) { focusHandler(altHostField) }
nickField := widget.NewEntryWithData(binding.BindString(&a.Nickname)) nickField := widget.NewEntryWithData(binding.BindString(&a.Nickname))
nickField.SetPlaceHolder("My Server") nickField.SetPlaceHolder(lang.L("My Server"))
nickField.OnSubmitted = func(_ string) { focusHandler(hostField) } nickField.OnSubmitted = func(_ string) { focusHandler(hostField) }
a.submitBtn = widget.NewButton("Enter", a.doSubmit) a.submitBtn = widget.NewButton(lang.L("Enter"), a.doSubmit)
a.submitBtn.Importance = widget.HighImportance a.submitBtn.Importance = widget.HighImportance
a.promptText = widget.NewRichTextWithText("") a.promptText = widget.NewRichTextWithText("")
a.promptText.Hidden = true a.promptText.Hidden = true
@@ -85,7 +88,7 @@ func NewAddEditServerDialog(title string, cancelable bool, prefillServer *backen
bottomRow = container.NewHBox( bottomRow = container.NewHBox(
a.promptText, a.promptText,
layout.NewSpacer(), layout.NewSpacer(),
widget.NewButton("Cancel", a.onCancel), widget.NewButton(lang.L("Cancel"), a.onCancel),
a.submitBtn) a.submitBtn)
} else { } else {
bottomRow = container.NewHBox( bottomRow = container.NewHBox(
@@ -97,17 +100,17 @@ func NewAddEditServerDialog(title string, cancelable bool, prefillServer *backen
a.container = container.NewVBox( a.container = container.NewVBox(
container.NewHBox(layout.NewSpacer(), titleLabel, layout.NewSpacer()), container.NewHBox(layout.NewSpacer(), titleLabel, layout.NewSpacer()),
container.New(layout.NewFormLayout(), container.New(layout.NewFormLayout(),
widget.NewLabel("Type"), widget.NewLabel(lang.L("Server Type")),
serverTypeChoice, serverTypeChoice,
widget.NewLabel("Nickname"), widget.NewLabel(lang.L("Nickname")),
nickField, nickField,
widget.NewLabel("URL"), widget.NewLabel(lang.L("URL")),
hostField, hostField,
widget.NewLabel("Alt. URL"), widget.NewLabel(lang.L("Alt. URL")),
altHostField, altHostField,
widget.NewLabel("Username"), widget.NewLabel(lang.L("Username")),
userField, userField,
widget.NewLabel("Password"), widget.NewLabel(lang.L("Password")),
a.passField, a.passField,
), ),
container.NewHBox(layout.NewSpacer(), legacyAuthCheck), container.NewHBox(layout.NewSpacer(), legacyAuthCheck),
+3 -2
View File
@@ -11,6 +11,7 @@ import (
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
@@ -36,7 +37,7 @@ func NewAlbumInfoDialog(albumInfo *mediaprovider.AlbumInfo, albumName string, al
widget.NewSeparator(), widget.NewSeparator(),
container.NewHBox( container.NewHBox(
layout.NewSpacer(), layout.NewSpacer(),
widget.NewButton("Close", func() { widget.NewButton(lang.L("Close"), func() {
if a.OnDismiss != nil { if a.OnDismiss != nil {
a.OnDismiss() a.OnDismiss()
} }
@@ -61,7 +62,7 @@ func (a *AlbumInfoDialog) buildMainContainer(albumInfo *mediaprovider.AlbumInfo,
title.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignCenter title.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignCenter
title.Truncation = fyne.TextTruncateEllipsis title.Truncation = fyne.TextTruncateEllipsis
infoContent := widget.NewLabel("Album info not available") infoContent := widget.NewLabel(lang.L("Album info not available"))
if albumInfo.Notes != "" { if albumInfo.Notes != "" {
infoContent = a.infoLabel(albumInfo.Notes) infoContent = a.infoLabel(albumInfo.Notes)
+8 -7
View File
@@ -4,6 +4,7 @@ import (
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/binding" "fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
"github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/backend/mediaprovider"
@@ -31,33 +32,33 @@ func NewEditPlaylistDialog(playlist *mediaprovider.Playlist, showPublicCheck boo
} }
e.ExtendBaseWidget(e) e.ExtendBaseWidget(e)
isPublicCheck := widget.NewCheckWithData("Public", binding.BindBool(&e.IsPublic)) isPublicCheck := widget.NewCheckWithData(lang.L("Public"), binding.BindBool(&e.IsPublic))
isPublicCheck.Hidden = !showPublicCheck isPublicCheck.Hidden = !showPublicCheck
nameEntry := widget.NewEntryWithData(binding.BindString(&e.Name)) nameEntry := widget.NewEntryWithData(binding.BindString(&e.Name))
descriptionEntry := widget.NewEntryWithData(binding.BindString(&e.Description)) descriptionEntry := widget.NewEntryWithData(binding.BindString(&e.Description))
deleteBtn := widget.NewButton("Delete Playlist", func() { deleteBtn := widget.NewButton(lang.L("Delete Playlist"), func() {
if e.OnDeletePlaylist != nil { if e.OnDeletePlaylist != nil {
e.OnDeletePlaylist() e.OnDeletePlaylist()
} }
}) })
submitBtn := widget.NewButton("OK", func() { submitBtn := widget.NewButton(lang.L("OK"), func() {
if e.OnUpdateMetadata != nil { if e.OnUpdateMetadata != nil {
e.OnUpdateMetadata() e.OnUpdateMetadata()
} }
}) })
submitBtn.Importance = widget.HighImportance submitBtn.Importance = widget.HighImportance
cancelBtn := widget.NewButton("Cancel", func() { cancelBtn := widget.NewButton(lang.L("Cancel"), func() {
if e.OnCanceled != nil { if e.OnCanceled != nil {
e.OnCanceled() e.OnCanceled()
} }
}) })
e.container = container.NewVBox( e.container = container.NewVBox(
container.NewHBox(layout.NewSpacer(), widget.NewLabel("Edit Playlist"), layout.NewSpacer()), container.NewHBox(layout.NewSpacer(), widget.NewLabel(lang.L("Edit Playlist")), layout.NewSpacer()),
container.New(layout.NewFormLayout(), container.New(layout.NewFormLayout(),
widget.NewLabel("Name"), widget.NewLabel(lang.L("Name")),
nameEntry, nameEntry,
widget.NewLabel("Description"), widget.NewLabel(lang.L("Description")),
descriptionEntry, descriptionEntry,
), ),
container.NewHBox(isPublicCheck, layout.NewSpacer(), deleteBtn), container.NewHBox(isPublicCheck, layout.NewSpacer(), deleteBtn),
+5 -4
View File
@@ -7,6 +7,7 @@ import (
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
@@ -37,7 +38,7 @@ var _ fyne.Widget = (*LoginDialog)(nil)
func NewLoginDialog(servers []*backend.ServerConfig, pwFetch PasswordFetchFunc) *LoginDialog { func NewLoginDialog(servers []*backend.ServerConfig, pwFetch PasswordFetchFunc) *LoginDialog {
l := &LoginDialog{servers: servers} l := &LoginDialog{servers: servers}
l.ExtendBaseWidget(l) l.ExtendBaseWidget(l)
titleLabel := widget.NewLabel("Login to Server") titleLabel := widget.NewLabel(lang.L("Login to Server"))
titleLabel.TextStyle.Bold = true titleLabel.TextStyle.Bold = true
l.passField = widget.NewPasswordEntry() l.passField = widget.NewPasswordEntry()
l.passField.OnSubmitted = func(_ string) { l.onSubmit() } l.passField.OnSubmitted = func(_ string) { l.onSubmit() }
@@ -57,7 +58,7 @@ func NewLoginDialog(servers []*backend.ServerConfig, pwFetch PasswordFetchFunc)
editBtn := widget.NewButtonWithIcon("", theme.DocumentCreateIcon(), l.onEditServer) editBtn := widget.NewButtonWithIcon("", theme.DocumentCreateIcon(), l.onEditServer)
newBtn := widget.NewButtonWithIcon("", theme.ContentAddIcon(), l.onNewServer) newBtn := widget.NewButtonWithIcon("", theme.ContentAddIcon(), l.onNewServer)
deleteBtn := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() { l.onDeleteServer(l.serverSelect.SelectedIndex()) }) deleteBtn := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() { l.onDeleteServer(l.serverSelect.SelectedIndex()) })
l.submitBtn = widget.NewButton("OK", l.onSubmit) l.submitBtn = widget.NewButton(lang.L("OK"), l.onSubmit)
l.submitBtn.Importance = widget.HighImportance l.submitBtn.Importance = widget.HighImportance
l.promptText = widget.NewRichTextWithText("") l.promptText = widget.NewRichTextWithText("")
l.promptText.Segments[0].(*widget.TextSegment).Style.ColorName = theme.ColorNameError l.promptText.Segments[0].(*widget.TextSegment).Style.ColorName = theme.ColorNameError
@@ -66,9 +67,9 @@ func NewLoginDialog(servers []*backend.ServerConfig, pwFetch PasswordFetchFunc)
l.container = container.NewVBox( l.container = container.NewVBox(
container.NewHBox(layout.NewSpacer(), titleLabel, layout.NewSpacer()), container.NewHBox(layout.NewSpacer(), titleLabel, layout.NewSpacer()),
container.New(layout.NewFormLayout(), container.New(layout.NewFormLayout(),
widget.NewLabel("Server"), widget.NewLabel(lang.L("Server")),
container.NewBorder(nil, nil, nil, container.NewHBox(editBtn, newBtn, deleteBtn), l.serverSelect), container.NewBorder(nil, nil, nil, container.NewHBox(editBtn, newBtn, deleteBtn), l.serverSelect),
widget.NewLabel("Password"), widget.NewLabel(lang.L("Password")),
l.passField), l.passField),
widget.NewSeparator(), widget.NewSeparator(),
container.NewHBox(l.promptText, layout.NewSpacer(), l.submitBtn), container.NewHBox(l.promptText, layout.NewSpacer(), l.submitBtn),
+2 -1
View File
@@ -4,6 +4,7 @@ import (
"log" "log"
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/lang"
"github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/ui/util" "github.com/dweymouth/supersonic/ui/util"
) )
@@ -15,7 +16,7 @@ type QuickSearch struct {
func NewQuickSearch(mp mediaprovider.MediaProvider, im util.ImageFetcher) *QuickSearch { func NewQuickSearch(mp mediaprovider.MediaProvider, im util.ImageFetcher) *QuickSearch {
q := &QuickSearch{mp: mp} q := &QuickSearch{mp: mp}
q.SearchDialog = NewSearchDialog(im, "Quick Search", "Close", q.onSearched) q.SearchDialog = NewSearchDialog(im, lang.L("Search Everywhere"), lang.L("Close"), q.onSearched)
return q return q
} }
+4 -3
View File
@@ -8,6 +8,7 @@ import (
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
@@ -270,11 +271,11 @@ func (s *searchResult) Update(result *mediaprovider.SearchResult) {
s.imageLoader.Load(result.CoverID) s.imageLoader.Load(result.CoverID)
s.title.SetText(result.Name) s.title.SetText(result.Name)
maybePluralize := func(s string, size int) string { maybePluralize := func(key string, size int) string {
if size != 1 { if size != 1 {
return s + "s" return lang.L(key + "s")
} }
return s return lang.L(key)
} }
var secondaryText string var secondaryText string
switch result.Type { switch result.Type {
+6 -5
View File
@@ -8,6 +8,7 @@ import (
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/data/binding" "fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
"github.com/deluan/sanitize" "github.com/deluan/sanitize"
"github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/backend/mediaprovider"
@@ -31,12 +32,12 @@ func NewSelectPlaylistDialog(mp mediaprovider.MediaProvider, im util.ImageFetche
} }
sd := NewSearchDialog( sd := NewSearchDialog(
im, im,
"Add to playlist", lang.L("Add to playlist"),
"Cancel", lang.L("Cancel"),
sp.onSearched, sp.onSearched,
) )
sd.ActionItem = widget.NewCheckWithData("Skip duplicate tracks", binding.BindBool(&sp.SkipDuplicates)) sd.ActionItem = widget.NewCheckWithData(lang.L("Skip duplicate tracks"), binding.BindBool(&sp.SkipDuplicates))
sd.PlaceholderText = "Search playlists or new playlist name" sd.PlaceholderText = lang.L("Search playlists or new playlist name")
sp.SearchDialog = sd sp.SearchDialog = sd
return sp return sp
} }
@@ -82,7 +83,7 @@ func (sp *SelectPlaylist) onSearched(query string) []*mediaprovider.SearchResult
) )
}) })
results = append(results, &mediaprovider.SearchResult{ results = append(results, &mediaprovider.SearchResult{
Name: fmt.Sprintf("Create new playlist: %s", query), Name: fmt.Sprintf("%s: %s", lang.L("Create new playlist"), query),
Type: mediaprovider.ContentTypePlaylist, Type: mediaprovider.ContentTypePlaylist,
}) })
} }
+32 -29
View File
@@ -19,6 +19,7 @@ import (
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/binding" "fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/storage" "fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
@@ -138,12 +139,12 @@ func (s *SettingsDialog) createGeneralTab(canSaveQueueToServer bool) *container.
if startupPage.Selected == "" { if startupPage.Selected == "" {
startupPage.SetSelectedIndex(0) startupPage.SetSelectedIndex(0)
} }
closeToTray := widget.NewCheckWithData("Close to system tray", closeToTray := widget.NewCheckWithData(lang.L("Close to system tray"),
binding.BindBool(&s.config.Application.CloseToSystemTray)) binding.BindBool(&s.config.Application.CloseToSystemTray))
if !s.config.Application.EnableSystemTray { if !s.config.Application.EnableSystemTray {
closeToTray.Disable() closeToTray.Disable()
} }
systemTrayEnable := widget.NewCheck("Enable system tray", func(val bool) { systemTrayEnable := widget.NewCheck(lang.L("Enable system tray"), func(val bool) {
s.config.Application.EnableSystemTray = val s.config.Application.EnableSystemTray = val
// TODO: see https://github.com/fyne-io/fyne/issues/3788 // TODO: see https://github.com/fyne-io/fyne/issues/3788
// Once Fyne supports removing/hiding an existing system tray menu, // Once Fyne supports removing/hiding an existing system tray menu,
@@ -159,18 +160,20 @@ func (s *SettingsDialog) createGeneralTab(canSaveQueueToServer bool) *container.
systemTrayEnable.Checked = s.config.Application.EnableSystemTray systemTrayEnable.Checked = s.config.Application.EnableSystemTray
// save play queue settings // save play queue settings
saveToServer := widget.NewRadioGroup([]string{"Locally", "To server"}, func(choice string) { locally := lang.L("Locally")
s.config.Application.SaveQueueToServer = choice == "To server" toServer := lang.L("To server")
saveToServer := widget.NewRadioGroup([]string{locally, toServer}, func(choice string) {
s.config.Application.SaveQueueToServer = choice == toServer
}) })
saveToServer.Horizontal = true saveToServer.Horizontal = true
if !s.config.Application.SavePlayQueue { if !s.config.Application.SavePlayQueue {
saveToServer.Disable() saveToServer.Disable()
} }
saveToServer.Selected = "Locally" saveToServer.Selected = locally
if s.config.Application.SaveQueueToServer { if s.config.Application.SaveQueueToServer {
saveToServer.Selected = "To server" saveToServer.Selected = toServer
} }
saveQueue := widget.NewCheck("Save play queue on exit", func(save bool) { saveQueue := widget.NewCheck(lang.L("Save play queue on exit"), func(save bool) {
s.config.Application.SavePlayQueue = save s.config.Application.SavePlayQueue = save
if save && canSaveQueueToServer { if save && canSaveQueueToServer {
saveToServer.Enable() saveToServer.Enable()
@@ -184,9 +187,9 @@ func (s *SettingsDialog) createGeneralTab(canSaveQueueToServer bool) *container.
saveQueueHBox.Add(saveToServer) saveQueueHBox.Add(saveToServer)
} }
trackNotif := widget.NewCheckWithData("Show notification on track change", trackNotif := widget.NewCheckWithData(lang.L("Show notification on track change"),
binding.BindBool(&s.config.Application.ShowTrackChangeNotification)) binding.BindBool(&s.config.Application.ShowTrackChangeNotification))
albumGridYears := widget.NewCheck("Show year in album grid cards", func(b bool) { albumGridYears := widget.NewCheck(lang.L("Show year in album grid cards"), func(b bool) {
s.config.AlbumsPage.ShowYears = b s.config.AlbumsPage.ShowYears = b
s.config.FavoritesPage.ShowAlbumYears = b s.config.FavoritesPage.ShowAlbumYears = b
if s.OnPageNeedsRefresh != nil { if s.OnPageNeedsRefresh != nil {
@@ -232,7 +235,7 @@ func (s *SettingsDialog) createGeneralTab(canSaveQueueToServer bool) *container.
if lastScrobbleText == "" { if lastScrobbleText == "" {
lastScrobbleText = "4" // default scrobble minutes lastScrobbleText = "4" // default scrobble minutes
} }
durationEnabled := widget.NewCheck("or when", func(checked bool) { durationEnabled := widget.NewCheck(lang.L("or when"), func(checked bool) {
if !checked { if !checked {
s.config.Scrobbling.ThresholdTimeSeconds = -1 s.config.Scrobbling.ThresholdTimeSeconds = -1
lastScrobbleText = durationEntry.Text lastScrobbleText = durationEntry.Text
@@ -256,7 +259,7 @@ func (s *SettingsDialog) createGeneralTab(canSaveQueueToServer bool) *container.
durationEnabled.Disable() durationEnabled.Disable()
} }
scrobbleEnabled := widget.NewCheck("Send playback statistics to server", func(checked bool) { scrobbleEnabled := widget.NewCheck(lang.L("Send playback statistics to server"), func(checked bool) {
s.config.Scrobbling.Enabled = checked s.config.Scrobbling.Enabled = checked
if !checked { if !checked {
percentEntry.Disable() percentEntry.Disable()
@@ -274,13 +277,13 @@ func (s *SettingsDialog) createGeneralTab(canSaveQueueToServer bool) *container.
}) })
scrobbleEnabled.Checked = s.config.Scrobbling.Enabled scrobbleEnabled.Checked = s.config.Scrobbling.Enabled
return container.NewTabItem("General", container.NewVBox( return container.NewTabItem(lang.L("General"), container.NewVBox(
container.NewBorder(nil, nil, widget.NewLabel("Theme"), /*left*/ container.NewBorder(nil, nil, widget.NewLabel(lang.L("Theme")), /*left*/
container.NewHBox(widget.NewLabel("Mode"), themeModeSelect, util.NewHSpace(5)), // right container.NewHBox(widget.NewLabel("Mode"), themeModeSelect, util.NewHSpace(5)), // right
themeFileSelect, // center themeFileSelect, // center
), ),
container.NewHBox( container.NewHBox(
widget.NewLabel("Startup page"), container.NewGridWithColumns(2, startupPage), widget.NewLabel(lang.L("Startup page")), container.NewGridWithColumns(2, startupPage),
), ),
container.NewHBox(systemTrayEnable, closeToTray), container.NewHBox(systemTrayEnable, closeToTray),
saveQueueHBox, saveQueueHBox,
@@ -291,20 +294,20 @@ func (s *SettingsDialog) createGeneralTab(canSaveQueueToServer bool) *container.
widget.NewRichText(&widget.TextSegment{Text: "Scrobbling", Style: util.BoldRichTextStyle}), widget.NewRichText(&widget.TextSegment{Text: "Scrobbling", Style: util.BoldRichTextStyle}),
scrobbleEnabled, scrobbleEnabled,
container.NewHBox( container.NewHBox(
widget.NewLabel("Scrobble when"), widget.NewLabel(lang.L("Scrobble when")),
percentEntry, percentEntry,
widget.NewLabel("percent of track is played"), widget.NewLabel(lang.L("percent of track is played")),
), ),
container.NewHBox( container.NewHBox(
durationEnabled, durationEnabled,
durationEntry, durationEntry,
widget.NewLabel("minutes of track have been played"), widget.NewLabel(lang.L("minutes of track have been played")),
), ),
)) ))
} }
func (s *SettingsDialog) createPlaybackTab(isLocalPlayer, isReplayGainPlayer bool) *container.TabItem { func (s *SettingsDialog) createPlaybackTab(isLocalPlayer, isReplayGainPlayer bool) *container.TabItem {
disableTranscode := widget.NewCheckWithData("Disable server transcoding", binding.BindBool(&s.config.Transcoding.ForceRawFile)) disableTranscode := widget.NewCheckWithData(lang.L("Disable server transcoding"), binding.BindBool(&s.config.Transcoding.ForceRawFile))
deviceList := make([]string, len(s.audioDevices)) deviceList := make([]string, len(s.audioDevices))
var selIndex int var selIndex int
for i, dev := range s.audioDevices { for i, dev := range s.audioDevices {
@@ -323,7 +326,7 @@ func (s *SettingsDialog) createPlaybackTab(isLocalPlayer, isReplayGainPlayer boo
} }
} }
replayGainSelect := widget.NewSelect([]string{"None", "Album", "Track", "Auto"}, nil) replayGainSelect := widget.NewSelect([]string{lang.L("None"), lang.L("Album"), lang.L("Track"), lang.L("Auto")}, nil)
replayGainSelect.OnChanged = func(_ string) { replayGainSelect.OnChanged = func(_ string) {
switch replayGainSelect.SelectedIndex() { switch replayGainSelect.SelectedIndex() {
case 0: case 0:
@@ -376,7 +379,7 @@ func (s *SettingsDialog) createPlaybackTab(isLocalPlayer, isReplayGainPlayer boo
}) })
preventClipping.Checked = s.config.ReplayGain.PreventClipping preventClipping.Checked = s.config.ReplayGain.PreventClipping
audioExclusive := widget.NewCheck("Audio exclusive mode", func(checked bool) { audioExclusive := widget.NewCheck(lang.L("Exclusive mode"), func(checked bool) {
s.config.LocalPlayback.AudioExclusive = checked s.config.LocalPlayback.AudioExclusive = checked
s.onAudioExclusiveSettingsChanged() s.onAudioExclusiveSettingsChanged()
}) })
@@ -392,26 +395,26 @@ func (s *SettingsDialog) createPlaybackTab(isLocalPlayer, isReplayGainPlayer boo
preampGain.Disable() preampGain.Disable()
} }
return container.NewTabItem("Playback", container.NewVBox( return container.NewTabItem(lang.L("Playback"), container.NewVBox(
disableTranscode, disableTranscode,
container.New(&layout.CustomPaddedLayout{TopPadding: 5}, container.New(&layout.CustomPaddedLayout{TopPadding: 5},
container.New(layout.NewFormLayout(), container.New(layout.NewFormLayout(),
widget.NewLabel("Audio device"), container.NewBorder(nil, nil, nil, util.NewHSpace(70), deviceSelect), widget.NewLabel(lang.L("Audio device")), container.NewBorder(nil, nil, nil, util.NewHSpace(70), deviceSelect),
layout.NewSpacer(), audioExclusive, layout.NewSpacer(), audioExclusive,
)), )),
s.newSectionSeparator(), s.newSectionSeparator(),
widget.NewRichText(&widget.TextSegment{Text: "ReplayGain", Style: util.BoldRichTextStyle}), widget.NewRichText(&widget.TextSegment{Text: "ReplayGain", Style: util.BoldRichTextStyle}),
container.New(layout.NewFormLayout(), container.New(layout.NewFormLayout(),
widget.NewLabel("ReplayGain mode"), container.NewGridWithColumns(2, replayGainSelect), widget.NewLabel(lang.L("ReplayGain mode")), container.NewGridWithColumns(2, replayGainSelect),
widget.NewLabel("ReplayGain preamp"), container.NewHBox(preampGain, widget.NewLabel("dB")), widget.NewLabel(lang.L("ReplayGain preamp")), container.NewHBox(preampGain, widget.NewLabel("dB")),
widget.NewLabel("Prevent clipping"), preventClipping, widget.NewLabel(lang.L("Prevent clipping")), preventClipping,
), ),
)) ))
} }
func (s *SettingsDialog) createEqualizerTab(eqBands []string) *container.TabItem { func (s *SettingsDialog) createEqualizerTab(eqBands []string) *container.TabItem {
enabled := widget.NewCheck("Enabled", func(b bool) { enabled := widget.NewCheck(lang.L("Enabled"), func(b bool) {
s.config.LocalPlayback.EqualizerEnabled = b s.config.LocalPlayback.EqualizerEnabled = b
if s.OnEqualizerSettingsChanged != nil { if s.OnEqualizerSettingsChanged != nil {
s.OnEqualizerSettingsChanged() s.OnEqualizerSettingsChanged()
@@ -435,7 +438,7 @@ func (s *SettingsDialog) createEqualizerTab(eqBands []string) *container.TabItem
debouncer() debouncer()
} }
cont := container.NewBorder(enabled, nil, nil, nil, geq) cont := container.NewBorder(enabled, nil, nil, nil, geq)
return container.NewTabItem("Equalizer", cont) return container.NewTabItem(lang.L("Equalizer"), cont)
} }
func (s *SettingsDialog) createExperimentalTab(window fyne.Window) *container.TabItem { func (s *SettingsDialog) createExperimentalTab(window fyne.Window) *container.TabItem {
@@ -486,7 +489,7 @@ func (s *SettingsDialog) createExperimentalTab(window fyne.Window) *container.Ta
return container.NewTabItem("Experimental", container.NewVBox( return container.NewTabItem("Experimental", container.NewVBox(
warningLabel, warningLabel,
s.newSectionSeparator(), s.newSectionSeparator(),
widget.NewRichText(&widget.TextSegment{Text: "UI Scaling", Style: util.BoldRichTextStyle}), widget.NewRichText(&widget.TextSegment{Text: lang.L("UI Scaling"), Style: util.BoldRichTextStyle}),
uiScaleRadio, uiScaleRadio,
s.newSectionSeparator(), s.newSectionSeparator(),
widget.NewRichText(&widget.TextSegment{Text: "Application Font", Style: util.BoldRichTextStyle}), widget.NewRichText(&widget.TextSegment{Text: "Application Font", Style: util.BoldRichTextStyle}),
@@ -524,7 +527,7 @@ func (s *SettingsDialog) setRestartRequired() {
if ts.Text != "" { if ts.Text != "" {
return return
} }
ts.Text = "Restart required" ts.Text = lang.L("Restart required")
ts.Style.ColorName = theme.ColorNameError ts.Style.ColorName = theme.ColorNameError
s.promptText.Refresh() s.promptText.Refresh()
} }
+23 -22
View File
@@ -7,6 +7,7 @@ import (
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
@@ -36,7 +37,7 @@ func NewTrackInfoDialog(track *mediaprovider.Track) *TrackInfoDialog {
func (t *TrackInfoDialog) CreateRenderer() fyne.WidgetRenderer { func (t *TrackInfoDialog) CreateRenderer() fyne.WidgetRenderer {
c := container.New(layout.NewFormLayout()) c := container.New(layout.NewFormLayout())
addFormRow(c, "Title", t.track.Title) addFormRow(c, lang.L("Title"), t.track.Title)
c.Add(newFormText("Album", true)) c.Add(newFormText("Album", true))
album := widget.NewHyperlink(t.track.Album, nil) album := widget.NewHyperlink(t.track.Album, nil)
@@ -47,7 +48,7 @@ func (t *TrackInfoDialog) CreateRenderer() fyne.WidgetRenderer {
} }
c.Add(album) c.Add(album)
c.Add(newFormText("Artists", true)) c.Add(newFormText(lang.L("Artists"), true))
artists := widgets.NewMultiHyperlink() artists := widgets.NewMultiHyperlink()
artists.BuildSegments(t.track.ArtistNames, t.track.ArtistIDs) artists.BuildSegments(t.track.ArtistNames, t.track.ArtistIDs)
artists.OnTapped = func(id string) { artists.OnTapped = func(id string) {
@@ -58,7 +59,7 @@ func (t *TrackInfoDialog) CreateRenderer() fyne.WidgetRenderer {
c.Add(artists) c.Add(artists)
if len(t.track.ComposerNames) > 0 { if len(t.track.ComposerNames) > 0 {
c.Add(newFormText("Composers", true)) c.Add(newFormText(lang.L("Composers"), true))
composers := widgets.NewMultiHyperlink() composers := widgets.NewMultiHyperlink()
composers.BuildSegments(t.track.ComposerNames, t.track.ComposerIDs) composers.BuildSegments(t.track.ComposerNames, t.track.ComposerIDs)
artists.OnTapped = func(id string) { artists.OnTapped = func(id string) {
@@ -70,7 +71,7 @@ func (t *TrackInfoDialog) CreateRenderer() fyne.WidgetRenderer {
} }
if len(t.track.Genres) > 0 { if len(t.track.Genres) > 0 {
c.Add(newFormText("Genres", true)) c.Add(newFormText(lang.L("Genres"), true))
genres := widgets.NewMultiHyperlink() genres := widgets.NewMultiHyperlink()
genres.BuildSegments(t.track.Genres, t.track.Genres) genres.BuildSegments(t.track.Genres, t.track.Genres)
genres.OnTapped = func(g string) { genres.OnTapped = func(g string) {
@@ -81,7 +82,7 @@ func (t *TrackInfoDialog) CreateRenderer() fyne.WidgetRenderer {
c.Add(genres) c.Add(genres)
} }
addFormRow(c, "Duration", util.SecondsToTimeString(float64(t.track.Duration))) addFormRow(c, lang.L("Duration"), util.SecondsToTimeString(float64(t.track.Duration)))
copyBtn := widgets.NewIconButton(theme.ContentCopyIcon(), func() { copyBtn := widgets.NewIconButton(theme.ContentCopyIcon(), func() {
if t.OnCopyFilePath != nil { if t.OnCopyFilePath != nil {
@@ -91,39 +92,39 @@ func (t *TrackInfoDialog) CreateRenderer() fyne.WidgetRenderer {
copyBtn.IconSize = widgets.IconButtonSizeSmaller copyBtn.IconSize = widgets.IconButtonSizeSmaller
btnCtr := container.New(layout.NewCustomPaddedLayout(8, 0, 10, 0), btnCtr := container.New(layout.NewCustomPaddedLayout(8, 0, 10, 0),
container.NewVBox(copyBtn, layout.NewSpacer())) container.NewVBox(copyBtn, layout.NewSpacer()))
c.Add(container.NewHBox(btnCtr, newFormText("File path", true))) c.Add(container.NewHBox(btnCtr, newFormText(lang.L("File path"), true)))
c.Add(newFormText(t.track.FilePath, false)) c.Add(newFormText(t.track.FilePath, false))
addFormRow(c, "Comment", t.track.Comment) addFormRow(c, lang.L("Comment"), t.track.Comment)
addFormRow(c, "Year", strconv.Itoa(t.track.Year)) addFormRow(c, lang.L("Year"), strconv.Itoa(t.track.Year))
addFormRow(c, "Track number", strconv.Itoa(t.track.TrackNumber)) addFormRow(c, lang.L("Track number"), strconv.Itoa(t.track.TrackNumber))
addFormRow(c, "Disc number", strconv.Itoa(t.track.DiscNumber)) addFormRow(c, lang.L("Disc number"), strconv.Itoa(t.track.DiscNumber))
if t.track.BPM > 0 { if t.track.BPM > 0 {
addFormRow(c, "BPM", strconv.Itoa(t.track.BPM)) addFormRow(c, lang.L("BPM"), strconv.Itoa(t.track.BPM))
} }
addFormRow(c, "Content type", t.track.ContentType) addFormRow(c, lang.L("Content type"), t.track.ContentType)
addFormRow(c, "Bit rate", fmt.Sprintf("%d kbps", t.track.BitRate)) addFormRow(c, lang.L("Bit rate"), fmt.Sprintf("%d kbps", t.track.BitRate))
addFormRow(c, "File size", util.BytesToSizeString(t.track.Size)) addFormRow(c, lang.L("File size"), util.BytesToSizeString(t.track.Size))
addFormRow(c, "Play count", strconv.Itoa(t.track.PlayCount)) addFormRow(c, lang.L("Play count"), strconv.Itoa(t.track.PlayCount))
if !t.track.LastPlayed.IsZero() { if !t.track.LastPlayed.IsZero() {
addFormRow(c, "Last played", t.track.LastPlayed.Format(time.RFC1123)) addFormRow(c, lang.L("Last played"), t.track.LastPlayed.Format(time.RFC1123))
} }
if t.track.ReplayGain.TrackPeak > 0 { if t.track.ReplayGain.TrackPeak > 0 {
addFormRow(c, "Track gain", fmt.Sprintf("%0.2f dB", t.track.ReplayGain.TrackGain)) addFormRow(c, lang.L("Track gain"), fmt.Sprintf("%0.2f dB", t.track.ReplayGain.TrackGain))
addFormRow(c, "Track peak", fmt.Sprintf("%0.6f", t.track.ReplayGain.TrackPeak)) addFormRow(c, lang.L("Track peak"), fmt.Sprintf("%0.6f", t.track.ReplayGain.TrackPeak))
} }
if t.track.ReplayGain.AlbumPeak > 0 { if t.track.ReplayGain.AlbumPeak > 0 {
addFormRow(c, "Album gain", fmt.Sprintf("%0.2f dB", t.track.ReplayGain.AlbumGain)) addFormRow(c, lang.L("Album gain"), fmt.Sprintf("%0.2f dB", t.track.ReplayGain.AlbumGain))
addFormRow(c, "Album peak", fmt.Sprintf("%0.6f", t.track.ReplayGain.AlbumPeak)) addFormRow(c, lang.L("Album peak"), fmt.Sprintf("%0.6f", t.track.ReplayGain.AlbumPeak))
} }
title := widget.NewRichTextWithText("Track Info") title := widget.NewRichTextWithText(lang.L("Track Info"))
title.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = true title.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = true
dismissBtn := widget.NewButton("Close", func() { dismissBtn := widget.NewButton(lang.L("Close"), func() {
if t.OnDismiss != nil { if t.OnDismiss != nil {
t.OnDismiss() t.OnDismiss()
} }
+25 -24
View File
@@ -12,6 +12,7 @@ import (
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
@@ -56,24 +57,24 @@ func SecondsToTimeString(s float64) string {
var str string var str string
if days > 0 { if days > 0 {
daysStr := "days" daysStr := lang.L("days")
if days == 1 { if days == 1 {
daysStr = "day" daysStr = lang.L("day")
} }
str = fmt.Sprintf("%d %s ", days, daysStr) str = fmt.Sprintf("%d %s ", days, daysStr)
} }
if hr > 0 { if hr > 0 {
hrStr := "hrs" hrStr := lang.L("hrs")
if hr == 1 { if hr == 1 {
hrStr = "hr" hrStr = lang.L("hr")
} }
str += fmt.Sprintf("%d %s ", hr, hrStr) str += fmt.Sprintf("%d %s ", hr, hrStr)
} }
if min > 0 { if min > 0 {
str += fmt.Sprintf("%d min ", min) str += fmt.Sprintf("%d %s ", min, lang.L("min"))
} }
if sec > 0 { if sec > 0 {
str += fmt.Sprintf("%d sec ", sec) str += fmt.Sprintf("%d %s ", sec, lang.L("sec"))
} }
return str[:len(str)-1] return str[:len(str)-1]
} }
@@ -158,45 +159,45 @@ func PlaintextFromHTMLString(s string) string {
} }
func DisplayReleaseType(releaseTypes mediaprovider.ReleaseTypes) string { func DisplayReleaseType(releaseTypes mediaprovider.ReleaseTypes) string {
baseType := "Album" baseType := lang.L("Album")
switch { switch {
case releaseTypes&mediaprovider.ReleaseTypeAudiobook > 0: case releaseTypes&mediaprovider.ReleaseTypeAudiobook > 0:
baseType = "Audiobook" baseType = lang.L("Audiobook")
case releaseTypes&mediaprovider.ReleaseTypeAudioDrama > 0: case releaseTypes&mediaprovider.ReleaseTypeAudioDrama > 0:
baseType = "Audio Drama" baseType = lang.L("Audio Drama")
case releaseTypes&mediaprovider.ReleaseTypeBroadcast > 0: case releaseTypes&mediaprovider.ReleaseTypeBroadcast > 0:
baseType = "Broadcast" baseType = lang.L("Broadcast")
case releaseTypes&mediaprovider.ReleaseTypeDJMix > 0: case releaseTypes&mediaprovider.ReleaseTypeDJMix > 0:
baseType = "DJ-Mix" baseType = lang.L("DJ-Mix")
case releaseTypes&mediaprovider.ReleaseTypeEP > 0: case releaseTypes&mediaprovider.ReleaseTypeEP > 0:
baseType = "EP" baseType = lang.L("EP")
case releaseTypes&mediaprovider.ReleaseTypeFieldRecording > 0: case releaseTypes&mediaprovider.ReleaseTypeFieldRecording > 0:
baseType = "Field Recording" baseType = lang.L("Field Recording")
case releaseTypes&mediaprovider.ReleaseTypeInterview > 0: case releaseTypes&mediaprovider.ReleaseTypeInterview > 0:
baseType = "Interview" baseType = lang.L("Interview")
case releaseTypes&mediaprovider.ReleaseTypeMixtape > 0: case releaseTypes&mediaprovider.ReleaseTypeMixtape > 0:
baseType = "Mixtape" baseType = lang.L("Mixtape")
case releaseTypes&mediaprovider.ReleaseTypeSingle > 0: case releaseTypes&mediaprovider.ReleaseTypeSingle > 0:
baseType = "Single" baseType = lang.L("Single")
case releaseTypes&mediaprovider.ReleaseTypeSoundtrack > 0: case releaseTypes&mediaprovider.ReleaseTypeSoundtrack > 0:
baseType = "Soundtrack" baseType = lang.L("Soundtrack")
} }
var modifiers []string var modifiers []string
if releaseTypes&mediaprovider.ReleaseTypeLive > 0 { if releaseTypes&mediaprovider.ReleaseTypeLive > 0 {
modifiers = append(modifiers, "Live") modifiers = append(modifiers, lang.L("Live"))
} }
if releaseTypes&mediaprovider.ReleaseTypeDemo > 0 { if releaseTypes&mediaprovider.ReleaseTypeDemo > 0 {
modifiers = append(modifiers, "Demo") modifiers = append(modifiers, lang.L("Demo"))
} }
if releaseTypes&mediaprovider.ReleaseTypeRemix > 0 { if releaseTypes&mediaprovider.ReleaseTypeRemix > 0 {
modifiers = append(modifiers, "Remix") modifiers = append(modifiers, lang.L("Remix"))
} }
if releaseTypes&mediaprovider.ReleaseTypeSpokenWord > 0 { if releaseTypes&mediaprovider.ReleaseTypeSpokenWord > 0 {
modifiers = append(modifiers, "Spoken Word") modifiers = append(modifiers, lang.L("Spoken Word"))
} }
if releaseTypes&mediaprovider.ReleaseTypeCompilation > 0 { if releaseTypes&mediaprovider.ReleaseTypeCompilation > 0 {
modifiers = append(modifiers, "Compilation") modifiers = append(modifiers, lang.L("Compilation"))
} }
modifiers = append(modifiers, baseType) modifiers = append(modifiers, baseType)
@@ -205,7 +206,7 @@ func DisplayReleaseType(releaseTypes mediaprovider.ReleaseTypes) string {
func NewRatingSubmenu(onSetRating func(int)) *fyne.MenuItem { func NewRatingSubmenu(onSetRating func(int)) *fyne.MenuItem {
newRatingMenuItem := func(rating int) *fyne.MenuItem { newRatingMenuItem := func(rating int) *fyne.MenuItem {
label := "(none)" label := fmt.Sprintf("(%s)", lang.L("none"))
if rating > 0 { if rating > 0 {
label = strconv.Itoa(rating) label = strconv.Itoa(rating)
} }
@@ -213,7 +214,7 @@ func NewRatingSubmenu(onSetRating func(int)) *fyne.MenuItem {
onSetRating(rating) onSetRating(rating)
}) })
} }
ratingMenu := fyne.NewMenuItem("Set rating", nil) ratingMenu := fyne.NewMenuItem(lang.L("Set rating"), nil)
ratingMenu.Icon = theme.NewThemedResource(res.ResStarOutlineSvg) ratingMenu.Icon = theme.NewThemedResource(res.ResStarOutlineSvg)
ratingMenu.ChildMenu = fyne.NewMenu("", []*fyne.MenuItem{ ratingMenu.ChildMenu = fyne.NewMenu("", []*fyne.MenuItem{
newRatingMenuItem(0), newRatingMenuItem(0),
+12 -11
View File
@@ -11,6 +11,7 @@ import (
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
@@ -153,7 +154,7 @@ func NewAlbumFilterPopup(filter *AlbumFilterButton) *AlbumFilterPopup {
} }
// setup is favorite/not favorite filters // setup is favorite/not favorite filters
a.isFavorite = widget.NewCheck("Is favorite", func(fav bool) { a.isFavorite = widget.NewCheck(lang.L("Is favorite"), func(fav bool) {
filterOptions := a.filterBtn.filter.Options() filterOptions := a.filterBtn.filter.Options()
if fav { if fav {
a.isNotFavorite.SetChecked(false) a.isNotFavorite.SetChecked(false)
@@ -163,7 +164,7 @@ func NewAlbumFilterPopup(filter *AlbumFilterButton) *AlbumFilterPopup {
debounceOnChanged() debounceOnChanged()
}) })
a.isFavorite.Hidden = a.filterBtn.FavoriteDisabled a.isFavorite.Hidden = a.filterBtn.FavoriteDisabled
a.isNotFavorite = widget.NewCheck("Is not favorite", func(fav bool) { a.isNotFavorite = widget.NewCheck(lang.L("Is not favorite"), func(fav bool) {
filterOptions := a.filterBtn.filter.Options() filterOptions := a.filterBtn.filter.Options()
if fav { if fav {
a.isFavorite.SetChecked(false) a.isFavorite.SetChecked(false)
@@ -184,11 +185,11 @@ func NewAlbumFilterPopup(filter *AlbumFilterButton) *AlbumFilterPopup {
a.genreFilter.Hidden = a.filterBtn.GenreDisabled a.genreFilter.Hidden = a.filterBtn.GenreDisabled
// setup container // setup container
title := widget.NewLabel("Album filters") title := widget.NewLabel(lang.L("Album filters"))
title.TextStyle.Bold = true title.TextStyle.Bold = true
a.container = container.NewVBox( a.container = container.NewVBox(
container.NewHBox(layout.NewSpacer(), title, layout.NewSpacer()), container.NewHBox(layout.NewSpacer(), title, layout.NewSpacer()),
container.NewHBox(widget.NewLabel("Year from"), minYear, widget.NewLabel("to"), maxYear), container.NewHBox(widget.NewLabel(lang.L("Year from")), minYear, widget.NewLabel("to"), maxYear),
container.NewHBox(a.isFavorite, a.isNotFavorite), container.NewHBox(a.isFavorite, a.isNotFavorite),
a.genreFilter, a.genreFilter,
) )
@@ -281,7 +282,7 @@ func NewGenreFilterSubsection(onChanged func([]string), initialSelectedGenres []
}, },
) )
g.filterText = widget.NewEntry() g.filterText = widget.NewEntry()
g.filterText.SetPlaceHolder("Filter genres") g.filterText.SetPlaceHolder(lang.L("Filter genres"))
i := NewTappableIcon(theme.ContentClearIcon()) i := NewTappableIcon(theme.ContentClearIcon())
i.NoPointerCursor = true i.NoPointerCursor = true
i.OnTapped = func() { g.filterText.SetText("") } i.OnTapped = func() { g.filterText.SetText("") }
@@ -290,18 +291,18 @@ func NewGenreFilterSubsection(onChanged func([]string), initialSelectedGenres []
g.filterText.OnChanged = func(_ string) { g.filterText.OnChanged = func(_ string) {
debouncer() debouncer()
} }
g.allBtn = widget.NewButton("All", func() { g.selectAllOrNoneInView(false) }) g.allBtn = widget.NewButton(lang.L("All"), func() { g.selectAllOrNoneInView(false) })
g.noneBtn = widget.NewButton("None", func() { g.selectAllOrNoneInView(true) }) g.noneBtn = widget.NewButton(lang.L("None"), func() { g.selectAllOrNoneInView(true) })
g.genresTitleLine = widget.NewRichText( g.genresTitleLine = widget.NewRichText(
&widget.TextSegment{ &widget.TextSegment{
Text: "Genres ", Text: lang.L("Genres") + " ",
Style: widget.RichTextStyle{ Style: widget.RichTextStyle{
Inline: true, Inline: true,
TextStyle: fyne.TextStyle{Bold: true}, TextStyle: fyne.TextStyle{Bold: true},
}, },
}, },
&widget.TextSegment{Text: "(none selected)"}, &widget.TextSegment{Text: fmt.Sprintf("(%s)", lang.L("none selected"))},
) )
g.updateNumSelectedText() g.updateNumSelectedText()
@@ -378,11 +379,11 @@ func (g *GenreFilterSubsection) invokeOnChanged() {
} }
func (g *GenreFilterSubsection) updateNumSelectedText() { func (g *GenreFilterSubsection) updateNumSelectedText() {
numText := "none" numText := lang.L("none")
if l := len(g.selectedGenres); l > 0 { if l := len(g.selectedGenres); l > 0 {
numText = strconv.Itoa(l) numText = strconv.Itoa(l)
} }
t := fmt.Sprintf("(%s selected)", numText) t := fmt.Sprintf("(%s %s)", numText, lang.L("selected"))
g.genresTitleLine.Segments[1].(*widget.TextSegment).Text = t g.genresTitleLine.Segments[1].(*widget.TextSegment).Text = t
g.genresTitleLine.Refresh() g.genresTitleLine.Refresh()
} }
+2 -1
View File
@@ -3,6 +3,7 @@ package widgets
import ( import (
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
fynelyrics "github.com/dweymouth/fyne-lyrics" fynelyrics "github.com/dweymouth/fyne-lyrics"
"github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/backend/mediaprovider"
@@ -29,7 +30,7 @@ type LyricsViewer struct {
func NewLyricsViewer() *LyricsViewer { func NewLyricsViewer() *LyricsViewer {
l := &LyricsViewer{ l := &LyricsViewer{
noLyricsMsg: container.NewCenter(NewInfoMessage( noLyricsMsg: container.NewCenter(NewInfoMessage(
"Lyrics not available", "")), lang.L("Lyrics not available"), "")),
isEmpty: true, isEmpty: true,
} }
l.ExtendBaseWidget(l) l.ExtendBaseWidget(l)
+2 -1
View File
@@ -4,6 +4,7 @@ import (
"time" "time"
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
"github.com/dweymouth/supersonic/ui/util" "github.com/dweymouth/supersonic/ui/util"
@@ -27,7 +28,7 @@ func NewSearchEntry() *SearchEntry {
// For use only by extending widgets // For use only by extending widgets
func (sf *SearchEntry) Init() { func (sf *SearchEntry) Init() {
sf.PlaceHolder = "Search" sf.PlaceHolder = lang.L("Search")
sf.ActionItem = NewClearTextButton(func() { sf.ActionItem = NewClearTextButton(func() {
sf.SetText("") sf.SetText("")
}) })
+1
View File
@@ -107,6 +107,7 @@ type Tracklist struct {
} }
func NewTracklist(tracks []*mediaprovider.Track, im *backend.ImageManager, useCompactRows bool) *Tracklist { func NewTracklist(tracks []*mediaprovider.Track, im *backend.ImageManager, useCompactRows bool) *Tracklist {
initTracklistColumns()
playIcon := theme.NewThemedResource(theme.MediaPlayIcon()) playIcon := theme.NewThemedResource(theme.MediaPlayIcon())
playIcon.ColorName = theme.ColorNamePrimary playIcon.ColorName = theme.ColorNamePrimary
+83 -35
View File
@@ -4,10 +4,12 @@ import (
"fmt" "fmt"
"image" "image"
"strconv" "strconv"
"sync"
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
@@ -42,44 +44,90 @@ const (
) )
var ( var (
ExpandedTracklistRowColumns = []TracklistColumn{ ExpandedTracklistRowColumns []TracklistColumn
{Name: ColumnNum, Col: ListColumn{Text: "#", Alignment: fyne.TextAlignTrailing, CanToggleVisible: false}},
{Name: ColumnTitleArtist, Col: ListColumn{Text: "Title / Artist", Alignment: fyne.TextAlignLeading, CanToggleVisible: false}},
{Name: ColumnAlbum, Col: ListColumn{Text: "Album", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}},
{Name: ColumnComposer, Col: ListColumn{Text: "Composer", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}},
{Name: ColumnTime, Col: ListColumn{Text: "Time", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}},
{Name: ColumnYear, Col: ListColumn{Text: "Year", Alignment: fyne.TextAlignTrailing, 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: ColumnPlays, Col: ListColumn{Text: "Plays", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}},
{Name: ColumnComment, Col: ListColumn{Text: "Comment", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}},
{Name: ColumnBitrate, Col: ListColumn{Text: "Bitrate", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}},
{Name: ColumnSize, Col: ListColumn{Text: "Size", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}},
{Name: ColumnPath, Col: ListColumn{Text: "File Path", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}},
}
// #, Title/Artist, Album, Composer, Time, Year, Favorite, Rating, Plays, Comment, Bitrate, Size, Path ExpandedTracklistRowColumnWidths []float32
ExpandedTracklistRowColumnWidths = []float32{40, -1, -1, -1, 60, 60, 55, 100, 65, -1, 75, 75, -1}
CompactTracklistRowColumns = []TracklistColumn{ CompactTracklistRowColumns []TracklistColumn
{Name: ColumnNum, Col: ListColumn{Text: "#", Alignment: fyne.TextAlignTrailing, CanToggleVisible: false}},
{Name: ColumnTitle, Col: ListColumn{Text: "Title", Alignment: fyne.TextAlignLeading, CanToggleVisible: false}},
{Name: ColumnArtist, Col: ListColumn{Text: "Artist", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}},
{Name: ColumnAlbum, Col: ListColumn{Text: "Album", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}},
{Name: ColumnComposer, Col: ListColumn{Text: "Composer", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}},
{Name: ColumnTime, Col: ListColumn{Text: "Time", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}},
{Name: ColumnYear, Col: ListColumn{Text: "Year", Alignment: fyne.TextAlignTrailing, 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: ColumnPlays, Col: ListColumn{Text: "Plays", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}},
{Name: ColumnComment, Col: ListColumn{Text: "Comment", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}},
{Name: ColumnBitrate, Col: ListColumn{Text: "Bitrate", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}},
{Name: ColumnSize, Col: ListColumn{Text: "Size", Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}},
{Name: ColumnPath, Col: ListColumn{Text: "File Path", Alignment: fyne.TextAlignLeading, CanToggleVisible: true}},
}
// #, Title, Artist, Album, Composer, Time, Year, Favorite, Rating, Plays, Comment, Bitrate, Size, Path CompactTracklistRowColumnWidths []float32
CompactTracklistRowColumnWidths = []float32{40, -1, -1, -1, -1, 60, 60, 55, 100, 65, -1, 75, 75, -1}
initTracklistColumns = sync.OnceFunc(func() {
title := lang.L("Title")
artist := lang.L("Artist")
album := lang.L("Album")
composer := lang.L("Composer")
time := lang.L("Time")
year := lang.L("Year")
fav := lang.L("Fav.")
rating := lang.L("Rating")
plays := lang.L("Plays")
comment := lang.L("Comment")
bitrate := lang.L("Bit rate")
size := lang.L("Size")
filepath := lang.L("File path")
CompactTracklistRowColumns = []TracklistColumn{
{Name: ColumnNum, Col: ListColumn{Text: "#", Alignment: fyne.TextAlignTrailing, CanToggleVisible: false}},
{Name: ColumnTitle, Col: ListColumn{Text: title, Alignment: fyne.TextAlignLeading, CanToggleVisible: false}},
{Name: ColumnArtist, Col: ListColumn{Text: artist, Alignment: fyne.TextAlignLeading, CanToggleVisible: true}},
{Name: ColumnAlbum, Col: ListColumn{Text: album, Alignment: fyne.TextAlignLeading, CanToggleVisible: true}},
{Name: ColumnComposer, Col: ListColumn{Text: composer, Alignment: fyne.TextAlignLeading, CanToggleVisible: true}},
{Name: ColumnTime, Col: ListColumn{Text: time, Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}},
{Name: ColumnYear, Col: ListColumn{Text: year, Alignment: fyne.TextAlignTrailing, 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: ColumnPlays, Col: ListColumn{Text: plays, Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}},
{Name: ColumnComment, Col: ListColumn{Text: comment, Alignment: fyne.TextAlignLeading, CanToggleVisible: true}},
{Name: ColumnBitrate, Col: ListColumn{Text: bitrate, Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}},
{Name: ColumnSize, Col: ListColumn{Text: size, Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}},
{Name: ColumnPath, Col: ListColumn{Text: filepath, Alignment: fyne.TextAlignLeading, CanToggleVisible: true}},
}
ExpandedTracklistRowColumns = []TracklistColumn{
{Name: ColumnNum, Col: ListColumn{Text: "#", Alignment: fyne.TextAlignTrailing, CanToggleVisible: false}},
{Name: ColumnTitleArtist, Col: ListColumn{Text: title + " / " + artist, Alignment: fyne.TextAlignLeading, CanToggleVisible: false}},
{Name: ColumnAlbum, Col: ListColumn{Text: album, Alignment: fyne.TextAlignLeading, CanToggleVisible: true}},
{Name: ColumnComposer, Col: ListColumn{Text: composer, Alignment: fyne.TextAlignLeading, CanToggleVisible: true}},
{Name: ColumnTime, Col: ListColumn{Text: time, Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}},
{Name: ColumnYear, Col: ListColumn{Text: year, Alignment: fyne.TextAlignTrailing, 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: ColumnPlays, Col: ListColumn{Text: plays, Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}},
{Name: ColumnComment, Col: ListColumn{Text: comment, Alignment: fyne.TextAlignLeading, CanToggleVisible: true}},
{Name: ColumnBitrate, Col: ListColumn{Text: bitrate, Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}},
{Name: ColumnSize, Col: ListColumn{Text: size, Alignment: fyne.TextAlignTrailing, CanToggleVisible: true}},
{Name: ColumnPath, Col: ListColumn{Text: filepath, Alignment: fyne.TextAlignLeading, CanToggleVisible: true}},
}
var sortIconWidth float32 = 15
numColWidth := widget.NewLabel("9999").MinSize().Width
timeColWidth := fyne.Max(
widget.NewLabel(time).MinSize().Width+sortIconWidth,
widget.NewLabel("99:99").MinSize().Width)
yearColWidth := fyne.Max(
widget.NewLabel(year).MinSize().Width+sortIconWidth,
widget.NewLabel("2000").MinSize().Width)
favColWidth := fyne.Max(55,
widget.NewLabel(fav).MinSize().Width+sortIconWidth)
ratingColWidth := fyne.Max(100,
widget.NewLabel(rating).MinSize().Width+sortIconWidth)
playsColWidth := fyne.Max(
widget.NewLabel("9999").MinSize().Width,
widget.NewLabel(plays).MinSize().Width+sortIconWidth)
bitrateColWidth := fyne.Max(
widget.NewLabel("1000").MinSize().Width,
widget.NewLabel(bitrate).MinSize().Width+sortIconWidth)
sizeColWidth := fyne.Max(
widget.NewLabel("99.9 MB").MinSize().Width,
widget.NewLabel(size).MinSize().Width+sortIconWidth)
// #, Title, Artist, Album, Composer, Time, Year, Favorite, Rating, Plays, Comment, Bitrate, Size, Path
CompactTracklistRowColumnWidths = []float32{numColWidth, -1, -1, -1, -1, timeColWidth, yearColWidth, favColWidth, ratingColWidth, playsColWidth, -1, bitrateColWidth, sizeColWidth, -1}
// #, Title/Artist, Album, Composer, Time, Year, Favorite, Rating, Plays, Comment, Bitrate, Size, Path
ExpandedTracklistRowColumnWidths = []float32{numColWidth, -1, -1, -1, timeColWidth, yearColWidth, favColWidth, ratingColWidth, playsColWidth, -1, bitrateColWidth, sizeColWidth, -1}
})
) )
type tracklistRowBase struct { type tracklistRowBase struct {