Merge pull request #111 from dweymouth/feature/settingsdialog
Add settings dialog
This commit is contained in:
+2
-1
@@ -68,6 +68,7 @@ func StartupApp(appName, appVersionTag, configFile, latestReleaseURL string) (*A
|
|||||||
PreventClipping: a.Config.ReplayGain.PreventClipping,
|
PreventClipping: a.Config.ReplayGain.PreventClipping,
|
||||||
PreampGain: a.Config.ReplayGain.PreampGainDB,
|
PreampGain: a.Config.ReplayGain.PreampGainDB,
|
||||||
})
|
})
|
||||||
|
a.Player.SetAudioExclusive(a.Config.LocalPlayback.AudioExclusive)
|
||||||
|
|
||||||
a.ServerManager = NewServerManager(appName)
|
a.ServerManager = NewServerManager(appName)
|
||||||
a.PlaybackManager = NewPlaybackManager(a.bgrndCtx, a.ServerManager, a.Player, &a.Config.Scrobbling)
|
a.PlaybackManager = NewPlaybackManager(a.bgrndCtx, a.ServerManager, a.Player, &a.Config.Scrobbling)
|
||||||
@@ -100,7 +101,7 @@ func (a *App) initMPV() error {
|
|||||||
p := player.NewWithClientName(a.appName)
|
p := player.NewWithClientName(a.appName)
|
||||||
c := a.Config.LocalPlayback
|
c := a.Config.LocalPlayback
|
||||||
c.InMemoryCacheSizeMB = clamp(c.InMemoryCacheSizeMB, 10, 500)
|
c.InMemoryCacheSizeMB = clamp(c.InMemoryCacheSizeMB, 10, 500)
|
||||||
if err := p.Init(c.AudioExclusive, c.InMemoryCacheSizeMB); err != nil {
|
if err := p.Init(c.InMemoryCacheSizeMB); err != nil {
|
||||||
return fmt.Errorf("failed to initialize mpv player: %s", err.Error())
|
return fmt.Errorf("failed to initialize mpv player: %s", err.Error())
|
||||||
}
|
}
|
||||||
a.Player = p
|
a.Player = p
|
||||||
|
|||||||
@@ -270,6 +270,14 @@ func (p *PlaybackManager) StopAndClearPlayQueue() {
|
|||||||
p.playQueue = nil
|
p.playQueue = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *PlaybackManager) SetReplayGainOptions(config ReplayGainConfig) {
|
||||||
|
p.player.SetReplayGainOptions(player.ReplayGainOptions{
|
||||||
|
Mode: player.ReplayGainMode(config.Mode),
|
||||||
|
PreventClipping: config.PreventClipping,
|
||||||
|
PreampGain: config.PreampGainDB,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// call BEFORE updating p.nowPlayingIdx
|
// call BEFORE updating p.nowPlayingIdx
|
||||||
func (p *PlaybackManager) checkScrobble(playDur time.Duration) {
|
func (p *PlaybackManager) checkScrobble(playDur time.Duration) {
|
||||||
if !p.scrobbleCfg.Enabled || len(p.playQueue) == 0 || p.nowPlayingIdx < 0 {
|
if !p.scrobbleCfg.Enabled || len(p.playQueue) == 0 || p.nowPlayingIdx < 0 {
|
||||||
|
|||||||
+18
-5
@@ -63,6 +63,7 @@ type Player struct {
|
|||||||
vol int
|
vol int
|
||||||
replayGainOpts ReplayGainOptions
|
replayGainOpts ReplayGainOptions
|
||||||
haveRGainOpts bool
|
haveRGainOpts bool
|
||||||
|
audioExclusive bool
|
||||||
status Status
|
status Status
|
||||||
seeking bool
|
seeking bool
|
||||||
curPlaylistPos int64
|
curPlaylistPos int64
|
||||||
@@ -96,7 +97,7 @@ func NewWithClientName(c string) *Player {
|
|||||||
|
|
||||||
// Initializes the Player and makes it ready for playback.
|
// Initializes the Player and makes it ready for playback.
|
||||||
// Most Player functions will return ErrUnitialized if called before Init.
|
// Most Player functions will return ErrUnitialized if called before Init.
|
||||||
func (p *Player) Init(audioExclusive bool, maxCacheMB int) error {
|
func (p *Player) Init(maxCacheMB int) error {
|
||||||
if !p.initialized {
|
if !p.initialized {
|
||||||
m, err := CreateMPV()
|
m, err := CreateMPV()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -118,9 +119,7 @@ func (p *Player) Init(audioExclusive bool, maxCacheMB int) error {
|
|||||||
}
|
}
|
||||||
m.SetOption("volume", MPVFormatInt64, p.vol)
|
m.SetOption("volume", MPVFormatInt64, p.vol)
|
||||||
|
|
||||||
if audioExclusive {
|
p.SetAudioExclusive(p.audioExclusive)
|
||||||
m.SetOptionString("audio-exclusive", "yes")
|
|
||||||
}
|
|
||||||
if p.haveRGainOpts {
|
if p.haveRGainOpts {
|
||||||
p.SetReplayGainOptions(p.replayGainOpts)
|
p.SetReplayGainOptions(p.replayGainOpts)
|
||||||
}
|
}
|
||||||
@@ -259,7 +258,7 @@ func (p *Player) SetVolume(vol int) error {
|
|||||||
|
|
||||||
// Sets the ReplayGain options of the player.
|
// Sets the ReplayGain options of the player.
|
||||||
// Unlike most Player functions, SetReplayGainOptions can be called
|
// Unlike most Player functions, SetReplayGainOptions can be called
|
||||||
// before Init, to set the initial volume of the player on startup.
|
// before Init, to set the initial replaygain options of the player on startup.
|
||||||
func (p *Player) SetReplayGainOptions(options ReplayGainOptions) error {
|
func (p *Player) SetReplayGainOptions(options ReplayGainOptions) error {
|
||||||
p.replayGainOpts = options
|
p.replayGainOpts = options
|
||||||
p.haveRGainOpts = true
|
p.haveRGainOpts = true
|
||||||
@@ -281,6 +280,20 @@ func (p *Player) SetReplayGainOptions(options ReplayGainOptions) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sets the audio exclusive option of the player.
|
||||||
|
// Unlike most Player functions, SetAudioExclusive can be called
|
||||||
|
// before Init, to set the initial option of the player on startup.
|
||||||
|
func (p *Player) SetAudioExclusive(tf bool) {
|
||||||
|
p.audioExclusive = tf
|
||||||
|
if p.initialized {
|
||||||
|
val := "no"
|
||||||
|
if tf {
|
||||||
|
val = "yes"
|
||||||
|
}
|
||||||
|
p.mpv.SetOptionString("audio-exclusive", val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Gets the current volume of the player.
|
// Gets the current volume of the player.
|
||||||
func (p *Player) GetVolume() int {
|
func (p *Player) GetVolume() int {
|
||||||
return p.vol
|
return p.vol
|
||||||
|
|||||||
@@ -304,6 +304,24 @@ func (c *Controller) ShowAboutDialog() {
|
|||||||
pop.Show()
|
pop.Show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Controller) ShowSettingsDialog() {
|
||||||
|
dlg := dialogs.NewSettingsDialog(c.App.Config)
|
||||||
|
dlg.OnReplayGainSettingsChanged = func() {
|
||||||
|
c.App.PlaybackManager.SetReplayGainOptions(c.App.Config.ReplayGain)
|
||||||
|
}
|
||||||
|
dlg.OnAudioExclusiveSettingChanged = func() {
|
||||||
|
c.App.Player.SetAudioExclusive(c.App.Config.LocalPlayback.AudioExclusive)
|
||||||
|
}
|
||||||
|
pop := widget.NewModalPopUp(dlg, c.MainWindow.Canvas())
|
||||||
|
dlg.OnDismiss = func() {
|
||||||
|
pop.Hide()
|
||||||
|
c.doModalClosed()
|
||||||
|
}
|
||||||
|
c.ClosePopUpOnEscape(pop)
|
||||||
|
c.haveModal = true
|
||||||
|
pop.Show()
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Controller) trySetPasswordAndConnectToServer(server *backend.ServerConfig, password string) error {
|
func (c *Controller) trySetPasswordAndConnectToServer(server *backend.ServerConfig, password string) error {
|
||||||
if err := c.App.ServerManager.SetServerPassword(server, password); err != nil {
|
if err := c.App.ServerManager.SetServerPassword(server, password); err != nil {
|
||||||
log.Printf("error setting keyring credentials: %v", err)
|
log.Printf("error setting keyring credentials: %v", err)
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
package dialogs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"supersonic/backend"
|
||||||
|
"supersonic/ui/layouts"
|
||||||
|
"supersonic/ui/widgets"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/container"
|
||||||
|
"fyne.io/fyne/v2/layout"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
)
|
||||||
|
|
||||||
|
var boldStyle = widget.RichTextStyle{TextStyle: fyne.TextStyle{Bold: true}}
|
||||||
|
|
||||||
|
type SettingsDialog struct {
|
||||||
|
widget.BaseWidget
|
||||||
|
|
||||||
|
OnReplayGainSettingsChanged func()
|
||||||
|
OnAudioExclusiveSettingChanged func()
|
||||||
|
OnDismiss func()
|
||||||
|
|
||||||
|
config *backend.Config
|
||||||
|
|
||||||
|
content fyne.CanvasObject
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSettingsDialog(config *backend.Config) *SettingsDialog {
|
||||||
|
s := &SettingsDialog{config: config}
|
||||||
|
s.ExtendBaseWidget(s)
|
||||||
|
|
||||||
|
tabs := container.NewAppTabs(
|
||||||
|
s.createGeneralTab(),
|
||||||
|
s.createPlaybackTab(),
|
||||||
|
)
|
||||||
|
s.content = container.NewVBox(tabs, widget.NewSeparator(),
|
||||||
|
container.NewHBox(layout.NewSpacer(), widget.NewButton("Close", func() {
|
||||||
|
if s.OnDismiss != nil {
|
||||||
|
s.OnDismiss()
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingsDialog) createGeneralTab() *container.TabItem {
|
||||||
|
twoDigitValidator := func(text string, r rune) bool {
|
||||||
|
return unicode.IsDigit(r) && len(text) < 2
|
||||||
|
}
|
||||||
|
|
||||||
|
percentEntry := widgets.NewTextRestrictedEntry(twoDigitValidator)
|
||||||
|
percentEntry.SetMinCharWidth(2)
|
||||||
|
percentEntry.OnChanged = func(str string) {
|
||||||
|
if i, err := strconv.Atoi(str); err == nil {
|
||||||
|
s.config.Scrobbling.ThresholdPercent = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
percentEntry.Text = strconv.Itoa(s.config.Scrobbling.ThresholdPercent)
|
||||||
|
if !s.config.Scrobbling.Enabled {
|
||||||
|
percentEntry.Disable()
|
||||||
|
}
|
||||||
|
|
||||||
|
durationEntry := widgets.NewTextRestrictedEntry(twoDigitValidator)
|
||||||
|
durationEntry.SetMinCharWidth(2)
|
||||||
|
durationEntry.OnChanged = func(str string) {
|
||||||
|
if i, err := strconv.Atoi(str); err == nil {
|
||||||
|
s.config.Scrobbling.ThresholdTimeSeconds = i * 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if secs := s.config.Scrobbling.ThresholdTimeSeconds; secs >= 0 {
|
||||||
|
val := int(math.Round(float64(secs) / 60.))
|
||||||
|
durationEntry.Text = strconv.Itoa(val)
|
||||||
|
}
|
||||||
|
if !s.config.Scrobbling.Enabled || s.config.Scrobbling.ThresholdTimeSeconds < 0 {
|
||||||
|
durationEntry.Disable()
|
||||||
|
}
|
||||||
|
|
||||||
|
lastScrobbleText := durationEntry.Text
|
||||||
|
if lastScrobbleText == "" {
|
||||||
|
lastScrobbleText = "4" // default scrobble minutes
|
||||||
|
}
|
||||||
|
durationEnabled := widget.NewCheck("or when", func(checked bool) {
|
||||||
|
if !checked {
|
||||||
|
s.config.Scrobbling.ThresholdTimeSeconds = -1
|
||||||
|
lastScrobbleText = durationEntry.Text
|
||||||
|
durationEntry.Text = ""
|
||||||
|
durationEntry.Disable()
|
||||||
|
} else {
|
||||||
|
durationEntry.Text = lastScrobbleText
|
||||||
|
durationEntry.Enable()
|
||||||
|
durationEntry.Refresh()
|
||||||
|
durationEntry.OnChanged(durationEntry.Text)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
durationEnabled.Checked = s.config.Scrobbling.ThresholdTimeSeconds >= 0
|
||||||
|
if !s.config.Scrobbling.Enabled {
|
||||||
|
durationEnabled.Disable()
|
||||||
|
}
|
||||||
|
|
||||||
|
scrobbleEnabled := widget.NewCheck("Send playback statistics to server", func(checked bool) {
|
||||||
|
s.config.Scrobbling.Enabled = checked
|
||||||
|
if !checked {
|
||||||
|
percentEntry.Disable()
|
||||||
|
durationEnabled.Disable()
|
||||||
|
durationEntry.Disable()
|
||||||
|
} else {
|
||||||
|
percentEntry.Enable()
|
||||||
|
durationEnabled.Enable()
|
||||||
|
if durationEnabled.Checked {
|
||||||
|
durationEntry.Enable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
scrobbleEnabled.Checked = s.config.Scrobbling.Enabled
|
||||||
|
|
||||||
|
return container.NewTabItem("General", container.NewVBox(
|
||||||
|
widget.NewRichText(&widget.TextSegment{Text: "Scrobbling", Style: boldStyle}),
|
||||||
|
scrobbleEnabled,
|
||||||
|
container.NewHBox(
|
||||||
|
widget.NewLabel("Scrobble when"),
|
||||||
|
percentEntry,
|
||||||
|
widget.NewLabel("percent of track is played"),
|
||||||
|
),
|
||||||
|
container.NewHBox(
|
||||||
|
durationEnabled,
|
||||||
|
durationEntry,
|
||||||
|
widget.NewLabel("minutes of track have been played"),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingsDialog) createPlaybackTab() *container.TabItem {
|
||||||
|
replayGainSelect := widget.NewSelect([]string{"None", "Album", "Track"}, nil)
|
||||||
|
replayGainSelect.OnChanged = func(_ string) {
|
||||||
|
switch replayGainSelect.SelectedIndex() {
|
||||||
|
case 0:
|
||||||
|
s.config.ReplayGain.Mode = backend.ReplayGainNone
|
||||||
|
case 1:
|
||||||
|
s.config.ReplayGain.Mode = backend.ReplayGainAlbum
|
||||||
|
case 2:
|
||||||
|
s.config.ReplayGain.Mode = backend.ReplayGainTrack
|
||||||
|
}
|
||||||
|
s.onReplayGainSettingsChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
// set initially selected option
|
||||||
|
switch s.config.ReplayGain.Mode {
|
||||||
|
case backend.ReplayGainAlbum:
|
||||||
|
replayGainSelect.SetSelectedIndex(1)
|
||||||
|
case backend.ReplayGainTrack:
|
||||||
|
replayGainSelect.SetSelectedIndex(2)
|
||||||
|
default:
|
||||||
|
replayGainSelect.SetSelectedIndex(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
preampGain := widgets.NewTextRestrictedEntry(func(curText string, r rune) bool {
|
||||||
|
return (curText == "" && r == '-') ||
|
||||||
|
(curText == "" && unicode.IsDigit(r)) ||
|
||||||
|
((curText == "-" || curText == "0") && unicode.IsDigit(r))
|
||||||
|
})
|
||||||
|
preampGain.SetMinCharWidth(2)
|
||||||
|
preampGain.OnChanged = func(text string) {
|
||||||
|
if f, err := strconv.ParseFloat(text, 64); err == nil {
|
||||||
|
s.config.ReplayGain.PreampGainDB = f
|
||||||
|
s.onReplayGainSettingsChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
initVal := math.Round(s.config.ReplayGain.PreampGainDB)
|
||||||
|
if initVal < -9 {
|
||||||
|
initVal = -9
|
||||||
|
} else if initVal > 9 {
|
||||||
|
initVal = 9
|
||||||
|
}
|
||||||
|
preampGain.Text = strconv.Itoa(int(initVal))
|
||||||
|
|
||||||
|
preventClipping := widget.NewCheck("", func(checked bool) {
|
||||||
|
s.config.ReplayGain.PreventClipping = checked
|
||||||
|
s.onReplayGainSettingsChanged()
|
||||||
|
})
|
||||||
|
preventClipping.Checked = s.config.ReplayGain.PreventClipping
|
||||||
|
|
||||||
|
audioExclusive := widget.NewCheck("Audio exclusive mode", func(checked bool) {
|
||||||
|
s.config.LocalPlayback.AudioExclusive = checked
|
||||||
|
s.onAudioExclusiveSettingsChanged()
|
||||||
|
})
|
||||||
|
audioExclusive.Checked = s.config.LocalPlayback.AudioExclusive
|
||||||
|
|
||||||
|
return container.NewTabItem("Playback", container.NewVBox(
|
||||||
|
widget.NewRichText(&widget.TextSegment{Text: "ReplayGain", Style: boldStyle}),
|
||||||
|
container.New(layout.NewFormLayout(),
|
||||||
|
widget.NewLabel("ReplayGain mode"), replayGainSelect,
|
||||||
|
widget.NewLabel("ReplayGain preamp"), container.NewHBox(preampGain, widget.NewLabel("dB")),
|
||||||
|
widget.NewLabel("Prevent clipping"), container.NewHBox(preventClipping, layout.NewSpacer()),
|
||||||
|
),
|
||||||
|
container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15}, widget.NewSeparator()),
|
||||||
|
container.NewHBox(audioExclusive, layout.NewSpacer()),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingsDialog) onReplayGainSettingsChanged() {
|
||||||
|
if s.OnReplayGainSettingsChanged != nil {
|
||||||
|
s.OnReplayGainSettingsChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingsDialog) onAudioExclusiveSettingsChanged() {
|
||||||
|
if s.OnAudioExclusiveSettingChanged != nil {
|
||||||
|
s.OnAudioExclusiveSettingChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingsDialog) CreateRenderer() fyne.WidgetRenderer {
|
||||||
|
return widget.NewSimpleRenderer(s.content)
|
||||||
|
}
|
||||||
@@ -108,6 +108,7 @@ func NewMainWindow(fyneApp fyne.App, appName, appVersion string, app *backend.Ap
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
})
|
})
|
||||||
|
m.BrowsingPane.AddSettingsMenuItem("Settings...", m.Controller.ShowSettingsDialog)
|
||||||
m.BrowsingPane.AddSettingsMenuItem("About...", m.Controller.ShowAboutDialog)
|
m.BrowsingPane.AddSettingsMenuItem("About...", m.Controller.ShowAboutDialog)
|
||||||
m.addNavigationButtons()
|
m.addNavigationButtons()
|
||||||
m.BrowsingPane.DisableNavigationButtons()
|
m.BrowsingPane.DisableNavigationButtons()
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package widgets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/theme"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TextRestrictedEntry struct {
|
||||||
|
widget.Entry
|
||||||
|
|
||||||
|
charAllowed func(string, rune) bool
|
||||||
|
|
||||||
|
minWidth float32
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTextRestrictedEntry(charAllowed func(curText string, r rune) bool) *TextRestrictedEntry {
|
||||||
|
e := &TextRestrictedEntry{charAllowed: charAllowed}
|
||||||
|
e.ExtendBaseWidget(e)
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *TextRestrictedEntry) TypedRune(r rune) {
|
||||||
|
if e.charAllowed == nil || e.charAllowed(e.Text, r) {
|
||||||
|
e.Entry.TypedRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *TextRestrictedEntry) SetMinCharWidth(numChars int) {
|
||||||
|
e.minWidth = theme.Padding()*2 + fyne.MeasureText(strings.Repeat("W", numChars),
|
||||||
|
fyne.CurrentApp().Settings().Theme().Size(theme.SizeNameText), e.TextStyle).Width
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *TextRestrictedEntry) MinSize() fyne.Size {
|
||||||
|
if e.minWidth < 0.001 {
|
||||||
|
return e.Entry.MinSize()
|
||||||
|
}
|
||||||
|
return fyne.NewSize(e.minWidth, e.Entry.MinSize().Height)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user