169 lines
4.7 KiB
Go
169 lines
4.7 KiB
Go
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/theme"
|
|
"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,
|
|
Icon: theme.HeadphonesIcon, // Use headphone icon for all profiles
|
|
ID: profile.Path, // Store path as ID for retrieval
|
|
Type: mediaprovider.ContentTypeOther, // Use "Other" content type for AutoEQ profiles
|
|
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)
|
|
}
|