Merge pull request #111 from dweymouth/feature/settingsdialog

Add settings dialog
This commit is contained in:
Drew Weymouth
2023-04-01 10:57:18 -07:00
committed by GitHub
7 changed files with 305 additions and 6 deletions
+2 -1
View File
@@ -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
+8
View File
@@ -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
View File
@@ -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
+18
View File
@@ -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)
+217
View File
@@ -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)
}
+1
View File
@@ -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()
+41
View File
@@ -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)
}