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,
|
||||
PreampGain: a.Config.ReplayGain.PreampGainDB,
|
||||
})
|
||||
a.Player.SetAudioExclusive(a.Config.LocalPlayback.AudioExclusive)
|
||||
|
||||
a.ServerManager = NewServerManager(appName)
|
||||
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)
|
||||
c := a.Config.LocalPlayback
|
||||
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())
|
||||
}
|
||||
a.Player = p
|
||||
|
||||
@@ -270,6 +270,14 @@ func (p *PlaybackManager) StopAndClearPlayQueue() {
|
||||
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
|
||||
func (p *PlaybackManager) checkScrobble(playDur time.Duration) {
|
||||
if !p.scrobbleCfg.Enabled || len(p.playQueue) == 0 || p.nowPlayingIdx < 0 {
|
||||
|
||||
+18
-5
@@ -63,6 +63,7 @@ type Player struct {
|
||||
vol int
|
||||
replayGainOpts ReplayGainOptions
|
||||
haveRGainOpts bool
|
||||
audioExclusive bool
|
||||
status Status
|
||||
seeking bool
|
||||
curPlaylistPos int64
|
||||
@@ -96,7 +97,7 @@ func NewWithClientName(c string) *Player {
|
||||
|
||||
// Initializes the Player and makes it ready for playback.
|
||||
// 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 {
|
||||
m, err := CreateMPV()
|
||||
if err != nil {
|
||||
@@ -118,9 +119,7 @@ func (p *Player) Init(audioExclusive bool, maxCacheMB int) error {
|
||||
}
|
||||
m.SetOption("volume", MPVFormatInt64, p.vol)
|
||||
|
||||
if audioExclusive {
|
||||
m.SetOptionString("audio-exclusive", "yes")
|
||||
}
|
||||
p.SetAudioExclusive(p.audioExclusive)
|
||||
if p.haveRGainOpts {
|
||||
p.SetReplayGainOptions(p.replayGainOpts)
|
||||
}
|
||||
@@ -259,7 +258,7 @@ func (p *Player) SetVolume(vol int) error {
|
||||
|
||||
// Sets the ReplayGain options of the player.
|
||||
// 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 {
|
||||
p.replayGainOpts = options
|
||||
p.haveRGainOpts = true
|
||||
@@ -281,6 +280,20 @@ func (p *Player) SetReplayGainOptions(options ReplayGainOptions) error {
|
||||
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.
|
||||
func (p *Player) GetVolume() int {
|
||||
return p.vol
|
||||
|
||||
@@ -304,6 +304,24 @@ func (c *Controller) ShowAboutDialog() {
|
||||
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 {
|
||||
if err := c.App.ServerManager.SetServerPassword(server, password); err != nil {
|
||||
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.addNavigationButtons()
|
||||
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