Add EQ type selector, preset management, and AutoEQ browser UI

This commit is contained in:
Gianluca Boiano
2026-02-06 02:50:41 +01:00
parent b0b2dc46f9
commit 7780efb344
3 changed files with 739 additions and 21 deletions
+166
View File
@@ -0,0 +1,166 @@
package dialogs
import (
"context"
"fmt"
"log"
"strings"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/lang"
"github.com/deluan/sanitize"
"github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/sharedutil"
"github.com/dweymouth/supersonic/ui/util"
)
// AutoEQBrowser allows users to browse and select AutoEQ headphone profiles
type AutoEQBrowser struct {
SearchDialog *SearchDialog
manager *backend.AutoEQManager
allProfileResults []*mediaprovider.SearchResult
OnProfileSelected func(*backend.AutoEQProfile)
}
func NewAutoEQBrowser(manager *backend.AutoEQManager, im util.ImageFetcher) *AutoEQBrowser {
ab := &AutoEQBrowser{
manager: manager,
}
sd := NewSearchDialog(
im,
lang.L("Browse Headphone Profiles"),
lang.L("Cancel"),
ab.onSearched,
)
sd.PlaceholderText = lang.L("Search headphones...")
ab.SearchDialog = sd
return ab
}
func (ab *AutoEQBrowser) fetchAllProfiles() error {
ctx := context.Background()
profiles, err := ab.manager.FetchIndex(ctx)
if err != nil {
log.Printf("Error fetching AutoEQ index: %v", err)
// Show empty results on error
ab.allProfileResults = []*mediaprovider.SearchResult{}
return fmt.Errorf("failed to fetch AutoEQ index: %w", err)
}
log.Printf("Successfully fetched %d AutoEQ profiles", len(profiles))
// Convert to SearchResult format for display
ab.allProfileResults = sharedutil.MapSlice(profiles, ab.profileToSearchResult)
return nil
}
func (ab *AutoEQBrowser) profileToSearchResult(profile backend.AutoEQProfileMetadata) *mediaprovider.SearchResult {
// Format secondary text as "type · source" (e.g., "over-ear · oratory1990")
subtitle := ""
if profile.Type != "" {
subtitle = profile.Type
}
if profile.Source != "" {
if subtitle != "" {
subtitle += " · "
}
subtitle += profile.Source
}
return &mediaprovider.SearchResult{
Name: profile.Name,
ID: profile.Path, // Store path as ID for retrieval
Type: mediaprovider.ContentTypeAlbum, // Use album icon (looks like headphones)
ArtistName: subtitle,
Size: 0, // Don't show track count
}
}
func (ab *AutoEQBrowser) onSearched(query string) []*mediaprovider.SearchResult {
if ab.allProfileResults == nil {
if err := ab.fetchAllProfiles(); err != nil {
log.Printf("Failed to load AutoEQ profiles: %v", err)
// Return a single error result
return []*mediaprovider.SearchResult{
{
Name: lang.L("Error loading AutoEQ profiles"),
ArtistName: lang.L("Check network connection and try again"),
Type: mediaprovider.ContentTypePlaylist,
},
}
}
}
if query == "" {
return ab.allProfileResults
}
// Filter by name (case-insensitive, accent-insensitive)
return sharedutil.FilterSlice(ab.allProfileResults, func(result *mediaprovider.SearchResult) bool {
return strings.Contains(
sanitize.Accents(strings.ToLower(result.Name)),
sanitize.Accents(strings.ToLower(query)),
)
})
}
func (ab *AutoEQBrowser) SetOnDismiss(onDismiss func()) {
ab.SearchDialog.OnDismiss = onDismiss
}
func (ab *AutoEQBrowser) SetOnProfileSelected(callback func(*backend.AutoEQProfile)) {
ab.OnProfileSelected = callback
ab.SearchDialog.OnNavigateTo = func(_ mediaprovider.ContentType, profilePath string) {
// Fetch the full profile data
ctx := context.Background()
profile, err := ab.manager.FetchProfile(ctx, profilePath)
if err != nil {
log.Printf("Error fetching AutoEQ profile: %v", err)
// TODO: Show error dialog to user
return
}
if ab.OnProfileSelected != nil {
ab.OnProfileSelected(profile)
}
}
}
func (ab *AutoEQBrowser) MinSize() fyne.Size {
return ab.SearchDialog.MinSize()
}
func (ab *AutoEQBrowser) GetSearchEntry() fyne.Focusable {
return ab.SearchDialog.GetSearchEntry()
}
func (ab *AutoEQBrowser) Show() {
ab.SearchDialog.Show()
}
func (ab *AutoEQBrowser) Hide() {
ab.SearchDialog.Hide()
}
func (ab *AutoEQBrowser) Refresh() {
ab.SearchDialog.Refresh()
}
// ShowErrorDialog displays an error message to the user
func ShowAutoEQError(window fyne.Window, err error) {
title := lang.L("Error")
message := lang.L("Failed to load profile")
if err == backend.ErrProfileNotFound {
message = lang.L("Profile not found")
} else if strings.Contains(err.Error(), "context deadline exceeded") ||
strings.Contains(err.Error(), "connection") {
message = lang.L("Network error. Check connection.")
}
// Use a simple dialog (would need to import "fyne.io/fyne/v2/dialog")
// For now just log it
log.Printf("AutoEQ Error: %s - %v", message, err)
fmt.Printf("%s: %s\n", title, message)
}
+406 -15
View File
@@ -5,10 +5,13 @@ import (
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"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"
ttwidget "github.com/dweymouth/fyne-tooltip/widget" ttwidget "github.com/dweymouth/fyne-tooltip/widget"
"github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/ui/layouts" "github.com/dweymouth/supersonic/ui/layouts"
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"
@@ -17,43 +20,182 @@ import (
type GraphicEqualizer struct { type GraphicEqualizer struct {
widget.BaseWidget widget.BaseWidget
OnChanged func(band int, gain float64) OnChanged func(band int, gain float64)
OnPreampChanged func(gain float64) OnPreampChanged func(gain float64)
OnLoadAutoEQProfile func()
OnManualAdjustment func() // Called when user manually changes a slider
OnPresetSelected func(presetName string) // Called when user selects a preset
OnPresetDeleted func(presetName string) // Called when user deletes a preset
OnEQTypeChanged func(eqType string) // Called when EQ type is changed
bandSliders []*eqSlider bandSliders []*eqSlider
container *fyne.Container preampSlider *eqSlider
presetSelect *widget.Select
eqTypeSelect *widget.Select
autoEQBtn *widget.Button
profileLabel *widget.Label
container *fyne.Container
sliderArea *fyne.Container // Stores the slider area for dynamic rebuilding
topBar *fyne.Container // Stores the top bar
eqPresets []backend.EQPreset
presetManager *backend.EQPresetManager
parentWindow fyne.Window
isApplyingPreset bool // Flag to prevent clearing profile during preset application
currentEQType string // Current EQ type ("ISO10Band" or "ISO15Band")
} }
func NewGraphicEqualizer(preamp float64, bandFreqs []string, bandGains []float64) *GraphicEqualizer { func NewGraphicEqualizer(preamp float64, bandFreqs []string, bandGains []float64, eqType string, presetMgr *backend.EQPresetManager, parentWindow fyne.Window, activePresetName string) *GraphicEqualizer {
g := &GraphicEqualizer{} g := &GraphicEqualizer{
presetManager: presetMgr,
parentWindow: parentWindow,
currentEQType: eqType,
}
g.ExtendBaseWidget(g) g.ExtendBaseWidget(g)
g.loadPresets()
g.buildSliders(preamp, bandFreqs, bandGains) g.buildSliders(preamp, bandFreqs, bandGains)
// Set the dropdown to the active preset if one exists
if activePresetName != "" {
g.setActivePreset(activePresetName)
}
return g return g
} }
func (g *GraphicEqualizer) loadPresets() {
presets, err := g.presetManager.LoadPresets()
if err != nil {
// Fallback to empty list if load fails
g.eqPresets = []backend.EQPreset{}
return
}
g.eqPresets = presets
}
func (g *GraphicEqualizer) buildSliders(preamp float64, bands []string, bandGains []float64) { func (g *GraphicEqualizer) buildSliders(preamp float64, bands []string, bandGains []float64) {
// Build preset selector
g.updatePresetSelect()
// Build EQ type selector
if g.eqTypeSelect == nil {
g.eqTypeSelect = widget.NewSelect([]string{"ISO 15-Band", "ISO 10-Band"}, func(selected string) {
// Convert display name to type
newType := "ISO15Band"
if selected == "ISO 10-Band" {
newType = "ISO10Band"
}
if newType != g.currentEQType {
g.currentEQType = newType
if g.OnEQTypeChanged != nil {
g.OnEQTypeChanged(newType)
}
}
})
}
// Set current selection
if g.currentEQType == "ISO10Band" {
g.eqTypeSelect.SetSelected("ISO 10-Band")
} else {
g.eqTypeSelect.SetSelected("ISO 15-Band")
}
// Reset button
resetBtn := widget.NewButton(lang.L("Reset"), func() {
// Find and apply the "Flat" preset
for _, p := range g.eqPresets {
if p.Name == "Flat" {
g.applyPreset(p)
g.presetSelect.SetSelected(p.Name)
if g.OnPresetSelected != nil {
g.OnPresetSelected(p.Name)
}
break
}
}
})
// Save button
saveBtn := widget.NewButton(lang.L("Save"), func() {
g.showSavePresetDialog()
})
// Delete button
deleteBtn := widget.NewButton(lang.L("Delete"), func() {
g.showDeletePresetDialog()
})
// AutoEQ button
g.autoEQBtn = widget.NewButton(lang.L("AutoEQ"), func() {
if g.OnLoadAutoEQProfile != nil {
g.OnLoadAutoEQProfile()
}
})
// Profile label (hidden by default)
g.profileLabel = widget.NewLabel("")
g.profileLabel.Hide()
// Set minimum width for preset dropdown
g.presetSelect.Resize(fyne.NewSize(200, g.presetSelect.MinSize().Height))
// Top bar with controls - AutoEQ in main row for better discoverability
topBar := container.NewVBox(
// Main row: EQ type, preset selector, AutoEQ, and action buttons
container.NewHBox(
widget.NewLabel(lang.L("EQ Type:")),
g.eqTypeSelect,
widget.NewLabel(lang.L("EQ Preset:")),
g.presetSelect,
g.autoEQBtn,
layout.NewSpacer(),
saveBtn,
deleteBtn,
resetBtn,
),
// Second row: Profile label (shown only when AutoEQ profile is active)
g.profileLabel,
)
// Build slider area
g.sliderArea = g.buildSliderArea(preamp, bands, bandGains)
// Store topBar and create main container
g.topBar = topBar
g.container = container.NewBorder(g.topBar, nil, nil, nil, g.sliderArea)
}
func (g *GraphicEqualizer) buildSliderArea(preamp float64, bands []string, bandGains []float64) *fyne.Container {
// Range labels
rng := container.NewVBox( rng := container.NewVBox(
newCaptionTextSizeLabel("+12", fyne.TextAlignTrailing), newCaptionTextSizeLabel("+12", fyne.TextAlignTrailing),
layout.NewSpacer(), layout.NewSpacer(),
newCaptionTextSizeLabel("0", fyne.TextAlignTrailing), newCaptionTextSizeLabel("0 dB", fyne.TextAlignTrailing),
layout.NewSpacer(), layout.NewSpacer(),
newCaptionTextSizeLabel("-12", fyne.TextAlignTrailing), newCaptionTextSizeLabel("-12", fyne.TextAlignTrailing),
) )
g.bandSliders = make([]*eqSlider, len(bands)) g.bandSliders = make([]*eqSlider, len(bands))
bandSlidersCtr := container.New(layouts.NewGridLayoutWithColumnsAndPadding(len(bands)+2, -16)) bandSlidersCtr := container.New(layouts.NewGridLayoutWithColumnsAndPadding(len(bands)+2, -16))
pre := newCaptionTextSizeLabel("Pre", fyne.TextAlignCenter)
preampSlider := newEQSlider() // Preamp slider
preampSlider.SetValue(preamp) pre := newCaptionTextSizeLabel(lang.L("EQ Preamp"), fyne.TextAlignCenter)
preampSlider.OnChanged = func(f float64) { g.preampSlider = newEQSlider()
g.preampSlider.SetValue(preamp)
g.preampSlider.OnChanged = func(f float64) {
if g.OnPreampChanged != nil { if g.OnPreampChanged != nil {
g.OnPreampChanged(f) g.OnPreampChanged(f)
} }
preampSlider.UpdateToolTip() g.preampSlider.UpdateToolTip()
if !g.isApplyingPreset && g.OnManualAdjustment != nil {
g.OnManualAdjustment()
}
} }
preampSlider.UpdateToolTip() g.preampSlider.UpdateToolTip()
bandSlidersCtr.Add(container.NewBorder(nil, pre, nil, nil, preampSlider)) bandSlidersCtr.Add(container.NewBorder(nil, pre, nil, nil, g.preampSlider))
bandSlidersCtr.Add(container.NewBorder(nil, widget.NewLabel(""), nil, nil, rng)) bandSlidersCtr.Add(container.NewBorder(nil, widget.NewLabel(""), nil, nil, rng))
// Band sliders
for i, band := range bands { for i, band := range bands {
s := newEQSlider() s := newEQSlider()
if i < len(bandGains) { if i < len(bandGains) {
@@ -66,13 +208,17 @@ func (g *GraphicEqualizer) buildSliders(preamp float64, bands []string, bandGain
g.OnChanged(_i, f) g.OnChanged(_i, f)
} }
g.bandSliders[_i].UpdateToolTip() g.bandSliders[_i].UpdateToolTip()
if !g.isApplyingPreset && g.OnManualAdjustment != nil {
g.OnManualAdjustment()
}
} }
l := newCaptionTextSizeLabel(band, fyne.TextAlignCenter) l := newCaptionTextSizeLabel(band, fyne.TextAlignCenter)
c := container.NewBorder(nil, l, nil, nil, s) c := container.NewBorder(nil, l, nil, nil, s)
bandSlidersCtr.Add(c) bandSlidersCtr.Add(c)
g.bandSliders[i] = s g.bandSliders[i] = s
} }
g.container = container.NewStack(
return container.NewStack(
container.NewBorder(nil, widget.NewLabel(""), nil, nil, container.NewBorder(nil, widget.NewLabel(""), nil, nil,
container.NewBorder(nil, nil, util.NewHSpace(5), util.NewHSpace(5), container.NewBorder(nil, nil, util.NewHSpace(5), util.NewHSpace(5),
container.NewVBox( container.NewVBox(
@@ -86,6 +232,251 @@ func (g *GraphicEqualizer) buildSliders(preamp float64, bands []string, bandGain
) )
} }
// RebuildForEQType rebuilds the sliders for a new EQ type
func (g *GraphicEqualizer) RebuildForEQType(eqType string, bandGains []float64) {
// Determine band frequencies for the new type
var bands []string
if eqType == "ISO10Band" {
bands = []string{"31", "62", "125", "250", "500", "1k", "2k", "4k", "8k", "16k"}
} else {
bands = []string{"25", "40", "63", "100", "160", "250", "400", "630", "1k", "1.6k", "2.5k", "4k", "6.3k", "10k", "16k"}
}
// Get current preamp value
currentPreamp := 0.0
if g.preampSlider != nil {
currentPreamp = g.preampSlider.Value
}
// Rebuild the slider area
newSliderArea := g.buildSliderArea(currentPreamp, bands, bandGains)
// Replace the old slider area in the container
g.sliderArea = newSliderArea
g.container.Objects = []fyne.CanvasObject{g.topBar, g.sliderArea}
g.container.Refresh()
}
func (g *GraphicEqualizer) updatePresetSelect() {
presetNames := make([]string, len(g.eqPresets))
for i, p := range g.eqPresets {
displayName := p.Name
if !p.IsBuiltin {
displayName = p.Name + " *" // Mark custom presets with asterisk
}
presetNames[i] = displayName
}
if g.presetSelect == nil {
g.presetSelect = widget.NewSelect(presetNames, func(selected string) {
// Remove asterisk marker if present
cleanName := selected
if len(selected) > 2 && selected[len(selected)-2:] == " *" {
cleanName = selected[:len(selected)-2]
}
for _, p := range g.eqPresets {
if p.Name == cleanName {
g.applyPreset(p)
if g.OnPresetSelected != nil {
g.OnPresetSelected(cleanName)
}
break
}
}
})
g.presetSelect.PlaceHolder = lang.L("EQ Preset")
} else {
g.presetSelect.Options = presetNames
g.presetSelect.Refresh()
}
}
// setActivePreset sets the dropdown selection to match the given preset name
func (g *GraphicEqualizer) setActivePreset(presetName string) {
if presetName == "" {
return
}
// Find the preset and determine display name (with asterisk for custom)
for _, p := range g.eqPresets {
if p.Name == presetName {
displayName := p.Name
if !p.IsBuiltin {
displayName = p.Name + " *"
}
g.presetSelect.SetSelected(displayName)
return
}
}
}
func (g *GraphicEqualizer) applyPreset(preset backend.EQPreset) {
g.isApplyingPreset = true
defer func() { g.isApplyingPreset = false }()
// If preset type differs from current type, switch type first
if preset.Type != "" && preset.Type != g.currentEQType {
g.currentEQType = preset.Type
// Update the type selector UI
if preset.Type == "ISO10Band" {
g.eqTypeSelect.SetSelected("ISO 10-Band")
} else {
g.eqTypeSelect.SetSelected("ISO 15-Band")
}
// Notify about type change
if g.OnEQTypeChanged != nil {
g.OnEQTypeChanged(preset.Type)
}
}
// Apply preamp
g.preampSlider.SetValue(preset.Preamp)
g.preampSlider.UpdateToolTip()
if g.OnPreampChanged != nil {
g.OnPreampChanged(preset.Preamp)
}
// Apply band gains
for i, gain := range preset.Bands {
if i < len(g.bandSliders) {
g.bandSliders[i].SetValue(gain)
g.bandSliders[i].UpdateToolTip()
if g.OnChanged != nil {
g.OnChanged(i, gain)
}
}
}
}
func (g *GraphicEqualizer) getCurrentSettings() backend.EQPreset {
bands := make([]float64, len(g.bandSliders))
for i, slider := range g.bandSliders {
bands[i] = slider.Value
}
return backend.EQPreset{
Type: g.currentEQType,
Preamp: g.preampSlider.Value,
Bands: bands,
}
}
func (g *GraphicEqualizer) showSavePresetDialog() {
nameEntry := widget.NewEntry()
nameEntry.SetPlaceHolder(lang.L("Preset name"))
formDialog := dialog.NewForm(
lang.L("Save Preset"),
lang.L("Save"),
lang.L("Cancel"),
[]*widget.FormItem{
widget.NewFormItem(lang.L("Name"), nameEntry),
},
func(confirmed bool) {
if !confirmed || nameEntry.Text == "" {
return
}
preset := g.getCurrentSettings()
preset.Name = nameEntry.Text
preset.IsBuiltin = false
if err := g.presetManager.SavePreset(preset); err != nil {
dialog.ShowError(err, g.parentWindow)
return
}
// Reload presets and update UI
g.loadPresets()
g.updatePresetSelect()
// Select the newly saved preset
g.presetSelect.SetSelected(preset.Name + " *")
if g.OnPresetSelected != nil {
g.OnPresetSelected(preset.Name)
}
},
g.parentWindow,
)
formDialog.Resize(fyne.NewSize(400, 150))
formDialog.Show()
}
func (g *GraphicEqualizer) showDeletePresetDialog() {
selected := g.presetSelect.Selected
if selected == "" {
dialog.ShowInformation(lang.L("No Preset Selected"), lang.L("Please select a preset to delete"), g.parentWindow)
return
}
// Remove asterisk marker if present
cleanName := selected
if len(selected) > 2 && selected[len(selected)-2:] == " *" {
cleanName = selected[:len(selected)-2]
}
// Find the preset
var presetToDelete *backend.EQPreset
for i, p := range g.eqPresets {
if p.Name == cleanName {
presetToDelete = &g.eqPresets[i]
break
}
}
if presetToDelete == nil || presetToDelete.IsBuiltin {
dialog.ShowInformation(lang.L("Cannot Delete"), lang.L("Cannot delete builtin presets"), g.parentWindow)
return
}
dialog.ShowConfirm(
lang.L("Delete Preset"),
fmt.Sprintf(lang.L("Delete preset '%s'?"), cleanName),
func(confirmed bool) {
if !confirmed {
return
}
if err := g.presetManager.DeletePreset(cleanName); err != nil {
dialog.ShowError(err, g.parentWindow)
return
}
// Notify about deletion
if g.OnPresetDeleted != nil {
g.OnPresetDeleted(cleanName)
}
// Reload presets and update UI
g.loadPresets()
g.updatePresetSelect()
g.presetSelect.ClearSelected()
},
g.parentWindow,
)
}
// SetProfileLabel displays the name of the applied AutoEQ profile
func (g *GraphicEqualizer) SetProfileLabel(profileName string) {
if profileName == "" {
g.profileLabel.SetText("")
g.profileLabel.Hide()
} else {
g.profileLabel.SetText(fmt.Sprintf("%s: %s", lang.L("Profile"), profileName))
g.profileLabel.Show()
}
}
// ClearProfileLabel hides the profile label (called on manual adjustment)
func (g *GraphicEqualizer) ClearProfileLabel() {
g.SetProfileLabel("")
}
// ClearPresetSelection clears the preset dropdown selection
func (g *GraphicEqualizer) ClearPresetSelection() {
g.presetSelect.ClearSelected()
}
func newCaptionTextSizeLabel(text string, alignment fyne.TextAlign) *widget.RichText { func newCaptionTextSizeLabel(text string, alignment fyne.TextAlign) *widget.RichText {
l := widget.NewRichTextWithText(text) l := widget.NewRichTextWithText(text)
ts := l.Segments[0].(*widget.TextSegment) ts := l.Segments[0].(*widget.TextSegment)
+167 -6
View File
@@ -2,6 +2,7 @@ package dialogs
import ( import (
"errors" "errors"
"log"
"math" "math"
"os" "os"
"slices" "slices"
@@ -41,10 +42,14 @@ type SettingsDialog struct {
OnPageNeedsRefresh func() OnPageNeedsRefresh func()
OnClearCaches func() OnClearCaches func()
config *backend.Config config *backend.Config
audioDevices []mpv.AudioDevice audioDevices []mpv.AudioDevice
themeFiles map[string]string // filename -> displayName themeFiles map[string]string // filename -> displayName
promptText *widget.RichText promptText *widget.RichText
eqPresetManager *backend.EQPresetManager
autoEQManager *backend.AutoEQManager
imageManager util.ImageFetcher
window fyne.Window
clientDecidesScrobble bool clientDecidesScrobble bool
@@ -62,9 +67,21 @@ func NewSettingsDialog(
isReplayGainPlayer bool, isReplayGainPlayer bool,
isEqualizerPlayer bool, isEqualizerPlayer bool,
canSavePlayQueue bool, canSavePlayQueue bool,
eqPresetMgr *backend.EQPresetManager,
window fyne.Window, window fyne.Window,
autoEQManager *backend.AutoEQManager,
imageManager util.ImageFetcher,
) *SettingsDialog { ) *SettingsDialog {
s := &SettingsDialog{config: config, audioDevices: audioDeviceList, themeFiles: themeFileList, clientDecidesScrobble: clientDecidesScrobble} s := &SettingsDialog{
config: config,
audioDevices: audioDeviceList,
themeFiles: themeFileList,
clientDecidesScrobble: clientDecidesScrobble,
eqPresetManager: eqPresetMgr,
autoEQManager: autoEQManager,
imageManager: imageManager,
window: window,
}
s.ExtendBaseWidget(s) s.ExtendBaseWidget(s)
// TODO: It may be a nicer UX to always create the equalizer tab, // TODO: It may be a nicer UX to always create the equalizer tab,
@@ -465,7 +482,11 @@ func (s *SettingsDialog) createEqualizerTab(eqBands []string) *container.TabItem
enabled.Checked = s.config.LocalPlayback.EqualizerEnabled enabled.Checked = s.config.LocalPlayback.EqualizerEnabled
geq := NewGraphicEqualizer(s.config.LocalPlayback.EqualizerPreamp, geq := NewGraphicEqualizer(s.config.LocalPlayback.EqualizerPreamp,
eqBands, eqBands,
s.config.LocalPlayback.GraphicEqualizerBands) s.config.LocalPlayback.GraphicEqualizerBands,
s.config.LocalPlayback.EqualizerType,
s.eqPresetManager,
s.window,
s.config.LocalPlayback.ActiveEQPresetName)
debouncer := util.NewDebouncer(350*time.Millisecond, func() { debouncer := util.NewDebouncer(350*time.Millisecond, func() {
if s.OnEqualizerSettingsChanged != nil { if s.OnEqualizerSettingsChanged != nil {
s.OnEqualizerSettingsChanged() s.OnEqualizerSettingsChanged()
@@ -479,10 +500,150 @@ func (s *SettingsDialog) createEqualizerTab(eqBands []string) *container.TabItem
s.config.LocalPlayback.EqualizerPreamp = g s.config.LocalPlayback.EqualizerPreamp = g
debouncer() debouncer()
} }
geq.OnManualAdjustment = func() {
// Clear AutoEQ profile and preset when user manually adjusts sliders
s.config.LocalPlayback.AutoEQProfilePath = ""
s.config.LocalPlayback.AutoEQProfileName = ""
s.config.LocalPlayback.ActiveEQPresetName = ""
geq.ClearProfileLabel()
geq.ClearPresetSelection()
}
geq.OnLoadAutoEQProfile = func() {
s.openAutoEQBrowser(geq, debouncer)
}
geq.OnPresetSelected = func(presetName string) {
// Save the active preset name in config
s.config.LocalPlayback.ActiveEQPresetName = presetName
}
geq.OnPresetDeleted = func(presetName string) {
// Clear active preset name if the deleted preset was active
if s.config.LocalPlayback.ActiveEQPresetName == presetName {
s.config.LocalPlayback.ActiveEQPresetName = ""
}
}
geq.OnEQTypeChanged = func(eqType string) {
// Update config with new EQ type
s.config.LocalPlayback.EqualizerType = eqType
// Convert bands using interpolation to preserve EQ curve shape
var newBands []float64
currentBands := s.config.LocalPlayback.GraphicEqualizerBands
if eqType == "ISO10Band" {
// Converting from 15-band to 10-band
if len(currentBands) == 15 {
// Use interpolation to downsample
var bands15 [15]float64
copy(bands15[:], currentBands)
bands10 := backend.Interpolate15BandTo10Band(bands15)
newBands = bands10[:]
} else {
// Already 10-band or invalid size, just copy what we can
newBands = make([]float64, 10)
numCopy := min(len(currentBands), 10)
copy(newBands, currentBands[:numCopy])
}
} else {
// Converting from 10-band to 15-band
if len(currentBands) == 10 {
// Use interpolation to upsample
var bands10 [10]float64
copy(bands10[:], currentBands)
bands15 := backend.InterpolateAutoEQTo15Band(bands10)
newBands = bands15[:]
} else {
// Already 15-band or invalid size, just copy what we can
newBands = make([]float64, 15)
numCopy := min(len(currentBands), 15)
copy(newBands, currentBands[:numCopy])
}
}
s.config.LocalPlayback.GraphicEqualizerBands = newBands
// Dynamically rebuild the UI with the correct number of sliders
geq.RebuildForEQType(eqType, newBands)
// Apply the change to the player
if s.OnEqualizerSettingsChanged != nil {
s.OnEqualizerSettingsChanged()
}
}
// Restore profile label if a profile is currently applied
if s.config.LocalPlayback.AutoEQProfileName != "" {
geq.SetProfileLabel(s.config.LocalPlayback.AutoEQProfileName)
}
cont := container.NewBorder(enabled, nil, nil, nil, geq) cont := container.NewBorder(enabled, nil, nil, nil, geq)
return container.NewTabItem(lang.L("Equalizer"), cont) return container.NewTabItem(lang.L("Equalizer"), cont)
} }
func (s *SettingsDialog) openAutoEQBrowser(geq *GraphicEqualizer, debouncer func()) {
if s.autoEQManager == nil {
log.Printf("ERROR: AutoEQ manager not available (nil)")
return
}
if s.imageManager == nil {
log.Printf("ERROR: Image manager not available (nil)")
return
}
browser := NewAutoEQBrowser(s.autoEQManager, s.imageManager)
// Show in a modal popup dialog
var popup *widget.PopUp
popup = widget.NewModalPopUp(browser.SearchDialog, s.window.Canvas())
browser.SetOnProfileSelected(func(profile *backend.AutoEQProfile) {
s.applyAutoEQProfile(profile, geq, debouncer)
popup.Hide()
})
browser.SetOnDismiss(func() {
popup.Hide()
})
popup.Show()
s.window.Canvas().Focus(browser.GetSearchEntry())
}
func (s *SettingsDialog) applyAutoEQProfile(profile *backend.AutoEQProfile, geq *GraphicEqualizer, debouncer func()) {
// Use native 10-band AutoEQ profile
// Update config to use ISO10Band type
s.config.LocalPlayback.EqualizerType = "ISO10Band"
s.config.LocalPlayback.EqualizerPreamp = profile.Preamp
s.config.LocalPlayback.AutoEQProfilePath = profile.Path
s.config.LocalPlayback.AutoEQProfileName = profile.Name
s.config.LocalPlayback.ActiveEQPresetName = "" // Clear preset when applying AutoEQ
// Ensure GraphicEqualizerBands has the right size for 10 bands
if len(s.config.LocalPlayback.GraphicEqualizerBands) != 10 {
s.config.LocalPlayback.GraphicEqualizerBands = make([]float64, 10)
}
// Copy native 10-band values
for i := 0; i < 10; i++ {
s.config.LocalPlayback.GraphicEqualizerBands[i] = profile.Bands[i]
}
// Update UI using applyPreset to avoid triggering manual adjustment
preset := backend.EQPreset{
Name: profile.Name,
Type: "ISO10Band",
Preamp: profile.Preamp,
Bands: profile.Bands[:],
}
geq.applyPreset(preset)
// Clear preset dropdown since AutoEQ is now active
geq.ClearPresetSelection()
// Show profile label
geq.SetProfileLabel(profile.Name)
// Trigger equalizer update
debouncer()
}
func (s *SettingsDialog) createAppearanceTab(window fyne.Window) *container.TabItem { func (s *SettingsDialog) createAppearanceTab(window fyne.Window) *container.TabItem {
themeNames := []string{"Default"} themeNames := []string{"Default"}
themeFileNames := []string{""} themeFileNames := []string{""}