Add EQ type selector, preset management, and AutoEQ browser UI
This commit is contained in:
@@ -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)
|
||||||
|
}
|
||||||
+402
-11
@@ -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"
|
||||||
@@ -19,41 +22,180 @@ type GraphicEqualizer struct {
|
|||||||
|
|
||||||
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
|
||||||
|
preampSlider *eqSlider
|
||||||
|
presetSelect *widget.Select
|
||||||
|
eqTypeSelect *widget.Select
|
||||||
|
autoEQBtn *widget.Button
|
||||||
|
profileLabel *widget.Label
|
||||||
container *fyne.Container
|
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()
|
}
|
||||||
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))
|
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)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package dialogs
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"log"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -45,6 +46,10 @@ type SettingsDialog struct {
|
|||||||
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{""}
|
||||||
|
|||||||
Reference in New Issue
Block a user