diff --git a/ui/dialogs/autoeqbrowser.go b/ui/dialogs/autoeqbrowser.go new file mode 100644 index 0000000..a473141 --- /dev/null +++ b/ui/dialogs/autoeqbrowser.go @@ -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) +} diff --git a/ui/dialogs/graphicequalizer.go b/ui/dialogs/graphicequalizer.go index c2b1579..0b5ff77 100644 --- a/ui/dialogs/graphicequalizer.go +++ b/ui/dialogs/graphicequalizer.go @@ -5,10 +5,13 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/dialog" + "fyne.io/fyne/v2/lang" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" ttwidget "github.com/dweymouth/fyne-tooltip/widget" + "github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/ui/layouts" myTheme "github.com/dweymouth/supersonic/ui/theme" "github.com/dweymouth/supersonic/ui/util" @@ -17,43 +20,182 @@ import ( type GraphicEqualizer struct { widget.BaseWidget - OnChanged func(band int, gain float64) - OnPreampChanged func(gain float64) + OnChanged func(band int, 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 - container *fyne.Container + bandSliders []*eqSlider + 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 { - g := &GraphicEqualizer{} +func NewGraphicEqualizer(preamp float64, bandFreqs []string, bandGains []float64, eqType string, presetMgr *backend.EQPresetManager, parentWindow fyne.Window, activePresetName string) *GraphicEqualizer { + g := &GraphicEqualizer{ + presetManager: presetMgr, + parentWindow: parentWindow, + currentEQType: eqType, + } g.ExtendBaseWidget(g) + g.loadPresets() g.buildSliders(preamp, bandFreqs, bandGains) + // Set the dropdown to the active preset if one exists + if activePresetName != "" { + g.setActivePreset(activePresetName) + } + 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) { + // 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( newCaptionTextSizeLabel("+12", fyne.TextAlignTrailing), layout.NewSpacer(), - newCaptionTextSizeLabel("0", fyne.TextAlignTrailing), + newCaptionTextSizeLabel("0 dB", fyne.TextAlignTrailing), layout.NewSpacer(), newCaptionTextSizeLabel("-12", fyne.TextAlignTrailing), ) + g.bandSliders = make([]*eqSlider, len(bands)) bandSlidersCtr := container.New(layouts.NewGridLayoutWithColumnsAndPadding(len(bands)+2, -16)) - pre := newCaptionTextSizeLabel("Pre", fyne.TextAlignCenter) - preampSlider := newEQSlider() - preampSlider.SetValue(preamp) - preampSlider.OnChanged = func(f float64) { + + // Preamp slider + pre := newCaptionTextSizeLabel(lang.L("EQ Preamp"), fyne.TextAlignCenter) + g.preampSlider = newEQSlider() + g.preampSlider.SetValue(preamp) + g.preampSlider.OnChanged = func(f float64) { if g.OnPreampChanged != nil { g.OnPreampChanged(f) } - preampSlider.UpdateToolTip() + g.preampSlider.UpdateToolTip() + if !g.isApplyingPreset && g.OnManualAdjustment != nil { + g.OnManualAdjustment() + } } - preampSlider.UpdateToolTip() - bandSlidersCtr.Add(container.NewBorder(nil, pre, nil, nil, preampSlider)) + g.preampSlider.UpdateToolTip() + bandSlidersCtr.Add(container.NewBorder(nil, pre, nil, nil, g.preampSlider)) bandSlidersCtr.Add(container.NewBorder(nil, widget.NewLabel(""), nil, nil, rng)) + + // Band sliders for i, band := range bands { s := newEQSlider() if i < len(bandGains) { @@ -66,13 +208,17 @@ func (g *GraphicEqualizer) buildSliders(preamp float64, bands []string, bandGain g.OnChanged(_i, f) } g.bandSliders[_i].UpdateToolTip() + if !g.isApplyingPreset && g.OnManualAdjustment != nil { + g.OnManualAdjustment() + } } l := newCaptionTextSizeLabel(band, fyne.TextAlignCenter) c := container.NewBorder(nil, l, nil, nil, s) bandSlidersCtr.Add(c) g.bandSliders[i] = s } - g.container = container.NewStack( + + return container.NewStack( container.NewBorder(nil, widget.NewLabel(""), nil, nil, container.NewBorder(nil, nil, util.NewHSpace(5), util.NewHSpace(5), 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 { l := widget.NewRichTextWithText(text) ts := l.Segments[0].(*widget.TextSegment) diff --git a/ui/dialogs/settingsdialog.go b/ui/dialogs/settingsdialog.go index 8c65e20..41c3f4d 100644 --- a/ui/dialogs/settingsdialog.go +++ b/ui/dialogs/settingsdialog.go @@ -2,6 +2,7 @@ package dialogs import ( "errors" + "log" "math" "os" "slices" @@ -41,10 +42,14 @@ type SettingsDialog struct { OnPageNeedsRefresh func() OnClearCaches func() - config *backend.Config - audioDevices []mpv.AudioDevice - themeFiles map[string]string // filename -> displayName - promptText *widget.RichText + config *backend.Config + audioDevices []mpv.AudioDevice + themeFiles map[string]string // filename -> displayName + promptText *widget.RichText + eqPresetManager *backend.EQPresetManager + autoEQManager *backend.AutoEQManager + imageManager util.ImageFetcher + window fyne.Window clientDecidesScrobble bool @@ -62,9 +67,21 @@ func NewSettingsDialog( isReplayGainPlayer bool, isEqualizerPlayer bool, canSavePlayQueue bool, + eqPresetMgr *backend.EQPresetManager, window fyne.Window, + autoEQManager *backend.AutoEQManager, + imageManager util.ImageFetcher, ) *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) // 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 geq := NewGraphicEqualizer(s.config.LocalPlayback.EqualizerPreamp, 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() { if s.OnEqualizerSettingsChanged != nil { s.OnEqualizerSettingsChanged() @@ -479,10 +500,150 @@ func (s *SettingsDialog) createEqualizerTab(eqBands []string) *container.TabItem s.config.LocalPlayback.EqualizerPreamp = g 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) 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 { themeNames := []string{"Default"} themeFileNames := []string{""}