Merge pull request #835 from M0Rf30/feat/equalizer-improvements
Add equalizer preset management and AutoEQ integration
This commit is contained in:
+42
-15
@@ -43,17 +43,19 @@ var (
|
||||
)
|
||||
|
||||
type App struct {
|
||||
Config *Config
|
||||
ServerManager *ServerManager
|
||||
LyricsManager *LyricsManager
|
||||
ImageManager *ImageManager
|
||||
AudioCache *AudioCache
|
||||
PlaybackManager *PlaybackManager
|
||||
LocalPlayer *mpv.Player
|
||||
UpdateChecker UpdateChecker
|
||||
MPRISHandler *MPRISHandler
|
||||
WinSMTC *windows.SMTC
|
||||
ipcServer ipc.IPCServer
|
||||
Config *Config
|
||||
ServerManager *ServerManager
|
||||
LyricsManager *LyricsManager
|
||||
ImageManager *ImageManager
|
||||
AudioCache *AudioCache
|
||||
AutoEQManager *AutoEQManager
|
||||
EQPresetManager *EQPresetManager
|
||||
PlaybackManager *PlaybackManager
|
||||
LocalPlayer *mpv.Player
|
||||
UpdateChecker UpdateChecker
|
||||
MPRISHandler *MPRISHandler
|
||||
WinSMTC *windows.SMTC
|
||||
ipcServer ipc.IPCServer
|
||||
|
||||
// UI callbacks to be set in main
|
||||
OnReactivate func()
|
||||
@@ -165,6 +167,11 @@ func StartupApp(appName, displayAppName, appVersion, appVersionTag, latestReleas
|
||||
fetch = NewLrcLibFetcher(a.cacheDir, a.Config.Application.CustomLrcLibUrl, timeout)
|
||||
}
|
||||
a.LyricsManager = NewLyricsManager(a.ServerManager, fetch)
|
||||
a.EQPresetManager = NewEQPresetManager(confDir)
|
||||
|
||||
// Initialize AutoEQ manager
|
||||
autoEQTimeout := time.Duration(a.Config.Application.RequestTimeoutSeconds) * time.Second
|
||||
a.AutoEQManager = NewAutoEQManager(filepath.Join(cacheDir, "autoeq"), autoEQTimeout)
|
||||
|
||||
// Periodically scan for remote players
|
||||
go a.PlaybackManager.ScanRemotePlayers(a.bgrndCtx, true /*fastScan*/)
|
||||
@@ -399,11 +406,31 @@ func (a *App) setupMPV() error {
|
||||
a.LocalPlayer.SetAudioExclusive(a.Config.LocalPlayback.AudioExclusive)
|
||||
a.LocalPlayer.SetPauseFade(a.Config.LocalPlayback.PauseFade)
|
||||
|
||||
eq := &mpv.ISO15BandEqualizer{
|
||||
EQPreamp: a.Config.LocalPlayback.EqualizerPreamp,
|
||||
Disabled: !a.Config.LocalPlayback.EqualizerEnabled,
|
||||
// Initialize the appropriate equalizer type based on config
|
||||
var eq mpv.Equalizer
|
||||
if a.Config.LocalPlayback.EqualizerType == "ISO10Band" {
|
||||
eq10 := &mpv.ISO10BandEqualizer{
|
||||
EQPreamp: a.Config.LocalPlayback.EqualizerPreamp,
|
||||
Disabled: !a.Config.LocalPlayback.EqualizerEnabled,
|
||||
}
|
||||
// Copy up to 10 bands
|
||||
numBands := min(len(a.Config.LocalPlayback.GraphicEqualizerBands), 10)
|
||||
for i := 0; i < numBands; i++ {
|
||||
eq10.BandGains[i] = a.Config.LocalPlayback.GraphicEqualizerBands[i]
|
||||
}
|
||||
eq = eq10
|
||||
} else {
|
||||
eq15 := &mpv.ISO15BandEqualizer{
|
||||
EQPreamp: a.Config.LocalPlayback.EqualizerPreamp,
|
||||
Disabled: !a.Config.LocalPlayback.EqualizerEnabled,
|
||||
}
|
||||
// Copy up to 15 bands
|
||||
numBands := min(len(a.Config.LocalPlayback.GraphicEqualizerBands), 15)
|
||||
for i := 0; i < numBands; i++ {
|
||||
eq15.BandGains[i] = a.Config.LocalPlayback.GraphicEqualizerBands[i]
|
||||
}
|
||||
eq = eq15
|
||||
}
|
||||
copy(eq.BandGains[:], a.Config.LocalPlayback.GraphicEqualizerBands)
|
||||
a.LocalPlayer.SetEqualizer(eq)
|
||||
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,515 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/20after4/configdir"
|
||||
)
|
||||
|
||||
const (
|
||||
autoEQIndexURL = "https://raw.githubusercontent.com/jaakkopasanen/AutoEq/master/results/INDEX.md"
|
||||
autoEQBaseURL = "https://raw.githubusercontent.com/jaakkopasanen/AutoEq/master/results/"
|
||||
indexCacheTTL = 7 * 24 * time.Hour // 7 days
|
||||
profileCacheTTL = 30 * 24 * time.Hour // 30 days
|
||||
maxMemoryCacheSize = 20 // LRU cache size
|
||||
)
|
||||
|
||||
var (
|
||||
ErrProfileNotFound = errors.New("AutoEQ profile not found")
|
||||
ErrInvalidFormat = errors.New("invalid AutoEQ profile format")
|
||||
)
|
||||
|
||||
// AutoEQProfile represents a headphone equalizer profile from AutoEQ
|
||||
type AutoEQProfile struct {
|
||||
Name string // Display name (e.g., "Sennheiser HD 650")
|
||||
Path string // Full path in repo (e.g., "oratory1990/over-ear/Sennheiser HD 650")
|
||||
Source string // Measurement source (e.g., "oratory1990")
|
||||
Type string // Headphone type (e.g., "over-ear")
|
||||
Preamp float64 // Preamp gain in dB
|
||||
Bands [10]float64 // 10-band equalizer gains in dB
|
||||
}
|
||||
|
||||
// AutoEQProfileMetadata contains just the metadata without the EQ data
|
||||
type AutoEQProfileMetadata struct {
|
||||
Name string
|
||||
Path string
|
||||
Source string
|
||||
Type string
|
||||
}
|
||||
|
||||
// AutoEQManager manages fetching and caching of AutoEQ profiles
|
||||
type AutoEQManager struct {
|
||||
cachePath string
|
||||
timeout time.Duration
|
||||
|
||||
// Memory cache (LRU)
|
||||
memCache map[string]*memoryCacheEntry
|
||||
memCacheMutex sync.RWMutex
|
||||
memCacheLRU []string // Keys in LRU order (most recently used at end)
|
||||
|
||||
// Index cache
|
||||
indexCache []AutoEQProfileMetadata
|
||||
indexCacheTime time.Time
|
||||
indexCacheMutex sync.RWMutex
|
||||
}
|
||||
|
||||
type memoryCacheEntry struct {
|
||||
profile *AutoEQProfile
|
||||
lastAccessed time.Time
|
||||
}
|
||||
|
||||
// NewAutoEQManager creates a new AutoEQ manager
|
||||
func NewAutoEQManager(cachePath string, timeout time.Duration) *AutoEQManager {
|
||||
configdir.MakePath(cachePath)
|
||||
log.Printf("Initializing AutoEQ manager: cache=%s, timeout=%v", cachePath, timeout)
|
||||
return &AutoEQManager{
|
||||
cachePath: cachePath,
|
||||
timeout: timeout,
|
||||
memCache: make(map[string]*memoryCacheEntry),
|
||||
memCacheLRU: make([]string, 0, maxMemoryCacheSize),
|
||||
}
|
||||
}
|
||||
|
||||
// FetchIndex fetches the list of all available AutoEQ profiles
|
||||
// Results are cached for 7 days
|
||||
func (m *AutoEQManager) FetchIndex(ctx context.Context) ([]AutoEQProfileMetadata, error) {
|
||||
// Check if index is already cached in memory
|
||||
m.indexCacheMutex.RLock()
|
||||
if len(m.indexCache) > 0 && time.Since(m.indexCacheTime) < indexCacheTTL {
|
||||
cached := m.indexCache
|
||||
m.indexCacheMutex.RUnlock()
|
||||
return cached, nil
|
||||
}
|
||||
m.indexCacheMutex.RUnlock()
|
||||
|
||||
// Try to load from disk cache
|
||||
indexCachePath := filepath.Join(m.cachePath, "index.json")
|
||||
if profiles, err := m.loadIndexFromDisk(indexCachePath); err == nil {
|
||||
m.indexCacheMutex.Lock()
|
||||
m.indexCache = profiles
|
||||
m.indexCacheTime = time.Now()
|
||||
m.indexCacheMutex.Unlock()
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
// Fetch from network
|
||||
profiles, err := m.fetchIndexFromNetwork(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("Successfully fetched %d AutoEQ profiles", len(profiles))
|
||||
|
||||
// Cache in memory and disk
|
||||
m.indexCacheMutex.Lock()
|
||||
m.indexCache = profiles
|
||||
m.indexCacheTime = time.Now()
|
||||
m.indexCacheMutex.Unlock()
|
||||
|
||||
m.saveIndexToDisk(indexCachePath, profiles)
|
||||
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
func (m *AutoEQManager) loadIndexFromDisk(cachePath string) ([]AutoEQProfileMetadata, error) {
|
||||
info, err := os.Stat(cachePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if cache is expired
|
||||
if time.Since(info.ModTime()) > indexCacheTTL {
|
||||
return nil, errors.New("cache expired")
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(cachePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var profiles []AutoEQProfileMetadata
|
||||
if err := json.Unmarshal(data, &profiles); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
func (m *AutoEQManager) saveIndexToDisk(cachePath string, profiles []AutoEQProfileMetadata) {
|
||||
data, err := json.Marshal(profiles)
|
||||
if err != nil {
|
||||
log.Printf("Failed to marshal AutoEQ index: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.WriteFile(cachePath, data, 0644); err != nil {
|
||||
log.Printf("Failed to write AutoEQ index cache: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *AutoEQManager) fetchIndexFromNetwork(ctx context.Context) ([]AutoEQProfileMetadata, error) {
|
||||
log.Printf("Fetching AutoEQ index from: %s (timeout: %v)", autoEQIndexURL, m.timeout)
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, m.timeout)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, autoEQIndexURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("HTTP request failed: %v", err)
|
||||
return nil, fmt.Errorf("fetching index: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status)
|
||||
}
|
||||
|
||||
profiles, err := m.parseIndex(resp.Body)
|
||||
if err != nil {
|
||||
log.Printf("Failed to parse AutoEQ index: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Printf("Successfully parsed %d profiles from AutoEQ index", len(profiles))
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
// parseIndex parses the INDEX.md markdown file
|
||||
// Format: [Display Name](./path/to/profile) by source
|
||||
var indexLinkRegex = regexp.MustCompile(`-\s*\[([^\]]+)\]\(\./([^)]+)\)`)
|
||||
|
||||
func (m *AutoEQManager) parseIndex(r io.Reader) ([]AutoEQProfileMetadata, error) {
|
||||
scanner := bufio.NewScanner(r)
|
||||
var profiles []AutoEQProfileMetadata
|
||||
lineCount := 0
|
||||
matchCount := 0
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
lineCount++
|
||||
matches := indexLinkRegex.FindStringSubmatch(line)
|
||||
if len(matches) == 3 {
|
||||
matchCount++
|
||||
name := matches[1]
|
||||
path := matches[2]
|
||||
|
||||
// URL-decode the name to handle any encoded characters
|
||||
if decodedName, err := url.QueryUnescape(name); err == nil {
|
||||
name = decodedName
|
||||
}
|
||||
|
||||
// Extract source and type from path
|
||||
// Format: source/type/name (e.g., "oratory1990/over-ear/Sennheiser HD 650")
|
||||
parts := strings.Split(path, "/")
|
||||
source := ""
|
||||
typ := ""
|
||||
if len(parts) >= 2 {
|
||||
source = parts[0]
|
||||
typ = parts[1]
|
||||
|
||||
// URL-decode source and type to display clean text
|
||||
if decodedSource, err := url.QueryUnescape(source); err == nil {
|
||||
source = decodedSource
|
||||
}
|
||||
if decodedType, err := url.QueryUnescape(typ); err == nil {
|
||||
typ = decodedType
|
||||
}
|
||||
}
|
||||
|
||||
profiles = append(profiles, AutoEQProfileMetadata{
|
||||
Name: name,
|
||||
Path: path,
|
||||
Source: source,
|
||||
Type: typ,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("reading index: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Parsed %d lines, found %d profile matches", lineCount, matchCount)
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
// FetchProfile fetches a specific AutoEQ profile by its path
|
||||
// Results are cached with LRU eviction
|
||||
func (m *AutoEQManager) FetchProfile(ctx context.Context, path string) (*AutoEQProfile, error) {
|
||||
// Check memory cache
|
||||
m.memCacheMutex.RLock()
|
||||
if entry, ok := m.memCache[path]; ok {
|
||||
entry.lastAccessed = time.Now()
|
||||
profile := entry.profile
|
||||
m.memCacheMutex.RUnlock()
|
||||
m.updateLRU(path)
|
||||
return profile, nil
|
||||
}
|
||||
m.memCacheMutex.RUnlock()
|
||||
|
||||
// Check disk cache
|
||||
profile, err := m.loadProfileFromDisk(path)
|
||||
if err == nil {
|
||||
m.addToMemoryCache(path, profile)
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
// Fetch from network
|
||||
profile, err = m.fetchProfileFromNetwork(ctx, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Cache in memory and disk
|
||||
m.addToMemoryCache(path, profile)
|
||||
m.saveProfileToDisk(path, profile)
|
||||
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
func (m *AutoEQManager) loadProfileFromDisk(path string) (*AutoEQProfile, error) {
|
||||
cacheKey := m.profileCacheKey(path)
|
||||
cachePath := filepath.Join(m.cachePath, cacheKey+".json")
|
||||
|
||||
info, err := os.Stat(cachePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if cache is expired
|
||||
if time.Since(info.ModTime()) > profileCacheTTL {
|
||||
os.Remove(cachePath)
|
||||
return nil, errors.New("cache expired")
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(cachePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var profile AutoEQProfile
|
||||
if err := json.Unmarshal(data, &profile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &profile, nil
|
||||
}
|
||||
|
||||
func (m *AutoEQManager) saveProfileToDisk(path string, profile *AutoEQProfile) {
|
||||
cacheKey := m.profileCacheKey(path)
|
||||
cachePath := filepath.Join(m.cachePath, cacheKey+".json")
|
||||
|
||||
data, err := json.Marshal(profile)
|
||||
if err != nil {
|
||||
log.Printf("Failed to marshal AutoEQ profile: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.WriteFile(cachePath, data, 0644); err != nil {
|
||||
log.Printf("Failed to write AutoEQ profile cache: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *AutoEQManager) profileCacheKey(path string) string {
|
||||
hash := md5.Sum([]byte(path))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
func (m *AutoEQManager) fetchProfileFromNetwork(ctx context.Context, path string) (*AutoEQProfile, error) {
|
||||
// URL-decode the path first (INDEX.md contains HTML-encoded paths like %20 for spaces)
|
||||
decodedPath, err := url.QueryUnescape(path)
|
||||
if err != nil {
|
||||
log.Printf("Failed to decode path %s: %v", path, err)
|
||||
decodedPath = path // fallback to original
|
||||
}
|
||||
|
||||
// Extract the headphone name from the path (last component)
|
||||
// Path format: "source/type/Headphone Name"
|
||||
pathComponents := strings.Split(decodedPath, "/")
|
||||
if len(pathComponents) == 0 {
|
||||
return nil, fmt.Errorf("invalid path format: %s", path)
|
||||
}
|
||||
headphoneName := pathComponents[len(pathComponents)-1]
|
||||
|
||||
// Construct the URL by properly encoding each path component
|
||||
for i, component := range pathComponents {
|
||||
pathComponents[i] = url.PathEscape(component)
|
||||
}
|
||||
encodedPath := strings.Join(pathComponents, "/")
|
||||
|
||||
// The file is named "{HeadphoneName} FixedBandEQ.txt"
|
||||
encodedFileName := url.PathEscape(headphoneName + " FixedBandEQ.txt")
|
||||
profileURL := autoEQBaseURL + encodedPath + "/" + encodedFileName
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, m.timeout)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, profileURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetching profile: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil, ErrProfileNotFound
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status)
|
||||
}
|
||||
|
||||
return m.parseProfile(path, resp.Body)
|
||||
}
|
||||
|
||||
// parseProfile parses the FixedBandEQ.txt file
|
||||
// Format:
|
||||
// Preamp: -6.0 dB
|
||||
// Filter 1: ON PK Fc 31 Hz Gain 5.0 dB Q 0.70
|
||||
// ... (10 filters total)
|
||||
var preampRegex = regexp.MustCompile(`Preamp:\s*([-+]?\d+\.?\d*)\s*dB`)
|
||||
var filterRegex = regexp.MustCompile(`Filter\s+\d+:.*?Fc\s+(\d+)\s+Hz.*?Gain\s+([-+]?\d+\.?\d*)\s*dB`)
|
||||
|
||||
func (m *AutoEQManager) parseProfile(path string, r io.Reader) (*AutoEQProfile, error) {
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading profile: %w", err)
|
||||
}
|
||||
|
||||
content := string(data)
|
||||
|
||||
// Parse preamp
|
||||
preampMatch := preampRegex.FindStringSubmatch(content)
|
||||
if len(preampMatch) < 2 {
|
||||
return nil, fmt.Errorf("%w: preamp not found", ErrInvalidFormat)
|
||||
}
|
||||
|
||||
preamp, err := strconv.ParseFloat(preampMatch[1], 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid preamp value", ErrInvalidFormat)
|
||||
}
|
||||
|
||||
// Parse filters
|
||||
filterMatches := filterRegex.FindAllStringSubmatch(content, -1)
|
||||
if len(filterMatches) != 10 {
|
||||
return nil, fmt.Errorf("%w: expected 10 filters, found %d", ErrInvalidFormat, len(filterMatches))
|
||||
}
|
||||
|
||||
var bands [10]float64
|
||||
expectedFreqs := []int{31, 62, 125, 250, 500, 1000, 2000, 4000, 8000, 16000}
|
||||
|
||||
for i, match := range filterMatches {
|
||||
if len(match) < 3 {
|
||||
return nil, fmt.Errorf("%w: invalid filter %d", ErrInvalidFormat, i+1)
|
||||
}
|
||||
|
||||
freq, err := strconv.Atoi(match[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid frequency in filter %d", ErrInvalidFormat, i+1)
|
||||
}
|
||||
|
||||
// Verify frequency matches expected (with tolerance for rounding)
|
||||
if freq != expectedFreqs[i] {
|
||||
log.Printf("Warning: AutoEQ filter %d has unexpected frequency %d Hz (expected %d Hz)",
|
||||
i+1, freq, expectedFreqs[i])
|
||||
}
|
||||
|
||||
gain, err := strconv.ParseFloat(match[2], 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid gain in filter %d", ErrInvalidFormat, i+1)
|
||||
}
|
||||
|
||||
bands[i] = gain
|
||||
}
|
||||
|
||||
// Extract name and metadata from path
|
||||
// URL-decode the path first to get clean names
|
||||
decodedPath, err := url.QueryUnescape(path)
|
||||
if err != nil {
|
||||
decodedPath = path // fallback to original if decode fails
|
||||
}
|
||||
|
||||
parts := strings.Split(decodedPath, "/")
|
||||
name := decodedPath
|
||||
source := ""
|
||||
typ := ""
|
||||
|
||||
if len(parts) >= 3 {
|
||||
name = parts[len(parts)-1]
|
||||
source = parts[0]
|
||||
typ = parts[1]
|
||||
}
|
||||
|
||||
return &AutoEQProfile{
|
||||
Name: name,
|
||||
Path: path,
|
||||
Source: source,
|
||||
Type: typ,
|
||||
Preamp: preamp,
|
||||
Bands: bands,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *AutoEQManager) addToMemoryCache(path string, profile *AutoEQProfile) {
|
||||
m.memCacheMutex.Lock()
|
||||
defer m.memCacheMutex.Unlock()
|
||||
|
||||
// If already in cache, just update
|
||||
if _, exists := m.memCache[path]; exists {
|
||||
m.memCache[path].profile = profile
|
||||
m.memCache[path].lastAccessed = time.Now()
|
||||
return
|
||||
}
|
||||
|
||||
// Evict LRU if at capacity
|
||||
if len(m.memCache) >= maxMemoryCacheSize {
|
||||
lruKey := m.memCacheLRU[0]
|
||||
delete(m.memCache, lruKey)
|
||||
m.memCacheLRU = m.memCacheLRU[1:]
|
||||
}
|
||||
|
||||
// Add new entry
|
||||
m.memCache[path] = &memoryCacheEntry{
|
||||
profile: profile,
|
||||
lastAccessed: time.Now(),
|
||||
}
|
||||
m.memCacheLRU = append(m.memCacheLRU, path)
|
||||
}
|
||||
|
||||
func (m *AutoEQManager) updateLRU(path string) {
|
||||
m.memCacheMutex.Lock()
|
||||
defer m.memCacheMutex.Unlock()
|
||||
|
||||
// Find and move to end
|
||||
for i, key := range m.memCacheLRU {
|
||||
if key == path {
|
||||
m.memCacheLRU = append(m.memCacheLRU[:i], m.memCacheLRU[i+1:]...)
|
||||
m.memCacheLRU = append(m.memCacheLRU, path)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,8 +135,12 @@ type LocalPlaybackConfig struct {
|
||||
InMemoryCacheSizeMB int
|
||||
Volume int
|
||||
EqualizerEnabled bool
|
||||
EqualizerType string // "ISO10Band" or "ISO15Band"
|
||||
EqualizerPreamp float64
|
||||
GraphicEqualizerBands []float64
|
||||
ActiveEQPresetName string // Name of currently selected EQ preset
|
||||
AutoEQProfilePath string // Path to applied AutoEQ profile (e.g., "oratory1990/over-ear/Sennheiser HD 650")
|
||||
AutoEQProfileName string // Display name of applied profile (e.g., "Sennheiser HD 650")
|
||||
PauseFade bool
|
||||
}
|
||||
|
||||
@@ -269,6 +273,7 @@ func DefaultConfig(appVersionTag string) *Config {
|
||||
InMemoryCacheSizeMB: 30,
|
||||
Volume: 100,
|
||||
EqualizerEnabled: false,
|
||||
EqualizerType: "ISO15Band",
|
||||
EqualizerPreamp: 0,
|
||||
GraphicEqualizerBands: make([]float64, 15),
|
||||
PauseFade: true,
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
package backend
|
||||
|
||||
import "math"
|
||||
|
||||
// AutoEQ uses 10 bands at these frequencies (in Hz)
|
||||
var autoEQFreqs = []float64{31.25, 62.5, 125, 250, 500, 1000, 2000, 4000, 8000, 16000}
|
||||
|
||||
// Supersonic uses 15 bands at these frequencies (in Hz)
|
||||
var supersonicFreqs = []float64{25, 40, 63, 100, 160, 250, 400, 630, 1000, 1600, 2500, 4000, 6300, 10000, 16000}
|
||||
|
||||
// InterpolateEQ10To15Band converts a 10-band EQ profile to Supersonic's 15-band ISO equalizer.
|
||||
// Uses logarithmic frequency positioning with linear dB interpolation.
|
||||
//
|
||||
// Parameters:
|
||||
// - eq10Gains: Array of 10 gain values (dB) from 10-band EQ at 31, 62, 125, 250, 500, 1k, 2k, 4k, 8k, 16k Hz
|
||||
//
|
||||
// Returns:
|
||||
// - Array of 15 gain values (dB) for Supersonic at 25, 40, 63, 100, 160, 250, 400, 630, 1k, 1.6k, 2.5k, 4k, 6.3k, 10k, 16k Hz
|
||||
func InterpolateEQ10To15Band(eq10Gains [10]float64) [15]float64 {
|
||||
var result [15]float64
|
||||
|
||||
for i, targetFreq := range supersonicFreqs {
|
||||
// Find the surrounding AutoEQ bands for this target frequency
|
||||
lowerIdx, upperIdx := findSurroundingBands(targetFreq)
|
||||
|
||||
if lowerIdx == upperIdx {
|
||||
// Exact match - use the AutoEQ gain directly
|
||||
result[i] = eq10Gains[lowerIdx]
|
||||
} else if lowerIdx == -1 {
|
||||
// Target frequency is below the lowest AutoEQ band (25 Hz < 31.25 Hz)
|
||||
// Extrapolate using the first two AutoEQ bands
|
||||
result[i] = extrapolateBelow(targetFreq, eq10Gains)
|
||||
} else if upperIdx == -1 {
|
||||
// Target frequency is above the highest AutoEQ band (should not happen with our ranges)
|
||||
// Use the highest AutoEQ gain
|
||||
result[i] = eq10Gains[len(eq10Gains)-1]
|
||||
} else {
|
||||
// Interpolate between two AutoEQ bands
|
||||
fLow := autoEQFreqs[lowerIdx]
|
||||
fHigh := autoEQFreqs[upperIdx]
|
||||
gLow := eq10Gains[lowerIdx]
|
||||
gHigh := eq10Gains[upperIdx]
|
||||
|
||||
// Calculate logarithmic position between the two bands
|
||||
// t = log(targetFreq/fLow) / log(fHigh/fLow)
|
||||
t := math.Log(targetFreq/fLow) / math.Log(fHigh/fLow)
|
||||
|
||||
// Linear interpolation of gain values
|
||||
result[i] = gLow + t*(gHigh-gLow)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// findSurroundingBands finds the AutoEQ band indices that surround the target frequency.
|
||||
// Returns (idx, idx) if there's an exact match, (lowerIdx, upperIdx) if between bands,
|
||||
// (-1, 0) if below all bands, or (lastIdx, -1) if above all bands.
|
||||
func findSurroundingBands(targetFreq float64) (lowerIdx, upperIdx int) {
|
||||
const epsilon = 0.01 // Tolerance for floating point comparison
|
||||
|
||||
// Check if below all bands
|
||||
if targetFreq < autoEQFreqs[0]-epsilon {
|
||||
return -1, 0
|
||||
}
|
||||
|
||||
// Check if above all bands
|
||||
if targetFreq > autoEQFreqs[len(autoEQFreqs)-1]+epsilon {
|
||||
return len(autoEQFreqs) - 1, -1
|
||||
}
|
||||
|
||||
// Find surrounding bands
|
||||
for i := 0; i < len(autoEQFreqs)-1; i++ {
|
||||
// Check for exact match
|
||||
if math.Abs(targetFreq-autoEQFreqs[i]) < epsilon {
|
||||
return i, i
|
||||
}
|
||||
|
||||
// Check if between this band and the next
|
||||
if targetFreq > autoEQFreqs[i] && targetFreq < autoEQFreqs[i+1] {
|
||||
return i, i + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Check for exact match with last band
|
||||
if math.Abs(targetFreq-autoEQFreqs[len(autoEQFreqs)-1]) < epsilon {
|
||||
return len(autoEQFreqs) - 1, len(autoEQFreqs) - 1
|
||||
}
|
||||
|
||||
// Should not reach here with valid input
|
||||
return len(autoEQFreqs) - 1, -1
|
||||
}
|
||||
|
||||
// extrapolateBelow extrapolates the gain for frequencies below the lowest AutoEQ band.
|
||||
// Uses the slope between the first two AutoEQ bands.
|
||||
func extrapolateBelow(targetFreq float64, autoEQGains [10]float64) float64 {
|
||||
// Use the slope between the first two bands to extrapolate
|
||||
f1 := autoEQFreqs[0]
|
||||
f2 := autoEQFreqs[1]
|
||||
g1 := autoEQGains[0]
|
||||
g2 := autoEQGains[1]
|
||||
|
||||
// Calculate the slope in log-frequency space
|
||||
slope := (g2 - g1) / math.Log(f2/f1)
|
||||
|
||||
// Extrapolate
|
||||
return g1 + slope*math.Log(targetFreq/f1)
|
||||
}
|
||||
|
||||
// InterpolateEQ15BandTo10Band converts a 15-band ISO equalizer to 10-band format.
|
||||
// Uses logarithmic frequency positioning with linear dB interpolation.
|
||||
func InterpolateEQ15BandTo10Band(gains15Band [15]float64) [10]float64 {
|
||||
var result [10]float64
|
||||
|
||||
for i, targetFreq := range autoEQFreqs {
|
||||
// Find the surrounding 15-band frequencies for this target
|
||||
lowerIdx, upperIdx := findSurrounding15Bands(targetFreq)
|
||||
|
||||
if lowerIdx == upperIdx {
|
||||
// Exact match
|
||||
result[i] = gains15Band[lowerIdx]
|
||||
} else if lowerIdx == -1 {
|
||||
// Below lowest band - use first band value
|
||||
result[i] = gains15Band[0]
|
||||
} else if upperIdx == -1 {
|
||||
// Above highest band - use last band value
|
||||
result[i] = gains15Band[len(gains15Band)-1]
|
||||
} else {
|
||||
// Interpolate between two 15-band frequencies
|
||||
fLow := supersonicFreqs[lowerIdx]
|
||||
fHigh := supersonicFreqs[upperIdx]
|
||||
gLow := gains15Band[lowerIdx]
|
||||
gHigh := gains15Band[upperIdx]
|
||||
|
||||
// Logarithmic position
|
||||
t := math.Log(targetFreq/fLow) / math.Log(fHigh/fLow)
|
||||
|
||||
// Linear interpolation of gain
|
||||
result[i] = gLow + t*(gHigh-gLow)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// findSurrounding15Bands finds the 15-band indices that surround the target frequency
|
||||
func findSurrounding15Bands(targetFreq float64) (lowerIdx, upperIdx int) {
|
||||
const epsilon = 0.01
|
||||
|
||||
if targetFreq < supersonicFreqs[0]-epsilon {
|
||||
return -1, 0
|
||||
}
|
||||
|
||||
if targetFreq > supersonicFreqs[len(supersonicFreqs)-1]+epsilon {
|
||||
return len(supersonicFreqs) - 1, -1
|
||||
}
|
||||
|
||||
for i := 0; i < len(supersonicFreqs)-1; i++ {
|
||||
if math.Abs(targetFreq-supersonicFreqs[i]) < epsilon {
|
||||
return i, i
|
||||
}
|
||||
|
||||
if targetFreq > supersonicFreqs[i] && targetFreq < supersonicFreqs[i+1] {
|
||||
return i, i + 1
|
||||
}
|
||||
}
|
||||
|
||||
if math.Abs(targetFreq-supersonicFreqs[len(supersonicFreqs)-1]) < epsilon {
|
||||
return len(supersonicFreqs) - 1, len(supersonicFreqs) - 1
|
||||
}
|
||||
|
||||
return len(supersonicFreqs) - 1, -1
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInterpolateAutoEQTo15Band_FlatProfile(t *testing.T) {
|
||||
// Test with a flat profile (all zeros)
|
||||
flatProfile := [10]float64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
result := InterpolateEQ10To15Band(flatProfile)
|
||||
|
||||
for i, gain := range result {
|
||||
if math.Abs(gain) > 0.001 {
|
||||
t.Errorf("Flat profile should result in zero gains, got %f at index %d", gain, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterpolateAutoEQTo15Band_ExactMatches(t *testing.T) {
|
||||
// Test that exact frequency matches use the AutoEQ gain directly
|
||||
// AutoEQ bands: 31, 62, 125, 250, 500, 1k, 2k, 4k, 8k, 16k Hz
|
||||
// Supersonic bands: 25, 40, 63, 100, 160, 250, 400, 630, 1k, 1.6k, 2.5k, 4k, 6.3k, 10k, 16k Hz
|
||||
// Exact matches: 250 (idx 5→3), 1k (idx 5→8), 4k (idx 7→11), 16k (idx 9→14)
|
||||
|
||||
testProfile := [10]float64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||
result := InterpolateEQ10To15Band(testProfile)
|
||||
|
||||
tests := []struct {
|
||||
supersonicIdx int
|
||||
autoEQIdx int
|
||||
expectedGain float64
|
||||
}{
|
||||
{5, 3, 4}, // 250 Hz
|
||||
{8, 5, 6}, // 1000 Hz
|
||||
{11, 7, 8}, // 4000 Hz
|
||||
{14, 9, 10}, // 16000 Hz
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if math.Abs(result[tt.supersonicIdx]-tt.expectedGain) > 0.001 {
|
||||
t.Errorf("Exact match at index %d (AutoEQ idx %d): expected %f, got %f",
|
||||
tt.supersonicIdx, tt.autoEQIdx, tt.expectedGain, result[tt.supersonicIdx])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterpolateAutoEQTo15Band_Interpolation(t *testing.T) {
|
||||
// Test interpolation between bands
|
||||
// Use a profile with known values to verify interpolation
|
||||
testProfile := [10]float64{0, 0, 0, 0, 0, 10, 0, 0, 0, 0}
|
||||
// Only 1000 Hz (index 5) has gain of 10 dB
|
||||
|
||||
result := InterpolateEQ10To15Band(testProfile)
|
||||
|
||||
// Check that 1000 Hz (index 8) has the exact value
|
||||
if math.Abs(result[8]-10) > 0.001 {
|
||||
t.Errorf("1000 Hz should be 10 dB, got %f", result[8])
|
||||
}
|
||||
|
||||
// Check that nearby frequencies are interpolated (should be between 0 and 10)
|
||||
// 630 Hz (index 7) should be between 500 Hz (0 dB) and 1000 Hz (10 dB)
|
||||
if result[7] < 0 || result[7] > 10 {
|
||||
t.Errorf("630 Hz interpolation out of range: %f", result[7])
|
||||
}
|
||||
|
||||
// 1600 Hz (index 9) should be between 1000 Hz (10 dB) and 2000 Hz (0 dB)
|
||||
if result[9] < 0 || result[9] > 10 {
|
||||
t.Errorf("1600 Hz interpolation out of range: %f", result[9])
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterpolateAutoEQTo15Band_Extrapolation(t *testing.T) {
|
||||
// Test extrapolation for 25 Hz (below lowest AutoEQ band of 31.25 Hz)
|
||||
testProfile := [10]float64{5, 10, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
// 31.25 Hz = 5 dB, 62.5 Hz = 10 dB
|
||||
|
||||
result := InterpolateEQ10To15Band(testProfile)
|
||||
|
||||
// 25 Hz should be extrapolated below 31.25 Hz
|
||||
// Since the slope is positive (5 to 10), 25 Hz should be < 5 dB
|
||||
if result[0] > 5 {
|
||||
t.Errorf("25 Hz extrapolation should be < 5 dB, got %f", result[0])
|
||||
}
|
||||
|
||||
// It should be a reasonable value (not too extreme)
|
||||
if math.Abs(result[0]) > 20 {
|
||||
t.Errorf("25 Hz extrapolation too extreme: %f", result[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterpolateAutoEQTo15Band_RealProfile(t *testing.T) {
|
||||
// Test with a realistic profile shape (bass and treble boost)
|
||||
// Approximating a V-shaped response
|
||||
testProfile := [10]float64{6, 5, 3, 1, 0, 0, 1, 3, 5, 6}
|
||||
|
||||
result := InterpolateEQ10To15Band(testProfile)
|
||||
|
||||
// Verify output is reasonable
|
||||
if len(result) != 15 {
|
||||
t.Errorf("Expected 15 bands, got %d", len(result))
|
||||
}
|
||||
|
||||
// Check that interpolated values are between neighboring AutoEQ values
|
||||
// For example, 40 Hz (index 1) should be between 31.25 Hz and 62.5 Hz values
|
||||
if result[1] < math.Min(testProfile[0], testProfile[1])-1 ||
|
||||
result[1] > math.Max(testProfile[0], testProfile[1])+1 {
|
||||
t.Errorf("40 Hz interpolation out of reasonable range: %f (between %f and %f)",
|
||||
result[1], testProfile[0], testProfile[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterpolateAutoEQTo15Band_MonotonicPreservation(t *testing.T) {
|
||||
// Test that monotonic sections are preserved
|
||||
// If AutoEQ gains are monotonically increasing, interpolated values should also increase
|
||||
monotonicProfile := [10]float64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
|
||||
result := InterpolateEQ10To15Band(monotonicProfile)
|
||||
|
||||
// Check general increasing trend (allowing for some extrapolation variance at edges)
|
||||
for i := 2; i < len(result)-1; i++ {
|
||||
if result[i] < result[i-1]-0.5 {
|
||||
t.Errorf("Monotonicity not preserved at index %d: %f < %f",
|
||||
i, result[i], result[i-1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindSurroundingBands_ExactMatches(t *testing.T) {
|
||||
tests := []struct {
|
||||
freq float64
|
||||
expectedLower int
|
||||
expectedUpper int
|
||||
}{
|
||||
{31.25, 0, 0},
|
||||
{62.5, 1, 1},
|
||||
{125, 2, 2},
|
||||
{250, 3, 3},
|
||||
{1000, 5, 5},
|
||||
{4000, 7, 7},
|
||||
{16000, 9, 9},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
lower, upper := findSurroundingBands(tt.freq)
|
||||
if lower != tt.expectedLower || upper != tt.expectedUpper {
|
||||
t.Errorf("findSurroundingBands(%f): expected (%d, %d), got (%d, %d)",
|
||||
tt.freq, tt.expectedLower, tt.expectedUpper, lower, upper)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindSurroundingBands_BetweenBands(t *testing.T) {
|
||||
tests := []struct {
|
||||
freq float64
|
||||
expectedLower int
|
||||
expectedUpper int
|
||||
}{
|
||||
{40, 0, 1}, // Between 31.25 and 62.5
|
||||
{100, 1, 2}, // Between 62.5 and 125
|
||||
{500, 4, 4}, // Exact match at 500
|
||||
{2000, 6, 6}, // Exact match at 2000
|
||||
{10000, 8, 9}, // Between 8000 and 16000
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
lower, upper := findSurroundingBands(tt.freq)
|
||||
if lower != tt.expectedLower || upper != tt.expectedUpper {
|
||||
t.Errorf("findSurroundingBands(%f): expected (%d, %d), got (%d, %d)",
|
||||
tt.freq, tt.expectedLower, tt.expectedUpper, lower, upper)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindSurroundingBands_BelowRange(t *testing.T) {
|
||||
lower, upper := findSurroundingBands(25)
|
||||
if lower != -1 || upper != 0 {
|
||||
t.Errorf("findSurroundingBands(25): expected (-1, 0), got (%d, %d)", lower, upper)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
)
|
||||
|
||||
const eqPresetsDir = "eq_presets"
|
||||
|
||||
// EQPreset represents an equalizer preset that can be saved/loaded
|
||||
type EQPreset struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"` // "ISO10Band" or "ISO15Band"
|
||||
Preamp float64 `json:"preamp"`
|
||||
Bands []float64 `json:"bands"`
|
||||
IsBuiltin bool `json:"-"` // not saved to file, determined at load time
|
||||
}
|
||||
|
||||
// EQPresetManager handles loading and saving EQ presets
|
||||
type EQPresetManager struct {
|
||||
presetsDir string
|
||||
builtinPresets []EQPreset
|
||||
}
|
||||
|
||||
// NewEQPresetManager creates a new preset manager
|
||||
func NewEQPresetManager(configDir string) *EQPresetManager {
|
||||
return &EQPresetManager{
|
||||
presetsDir: filepath.Join(configDir, eqPresetsDir),
|
||||
builtinPresets: getBuiltinPresets(),
|
||||
}
|
||||
}
|
||||
|
||||
// getBuiltinPresets returns the built-in equalizer presets
|
||||
func getBuiltinPresets() []EQPreset {
|
||||
return []EQPreset{
|
||||
{Name: "Flat", Type: "ISO15Band", Preamp: 0, Bands: []float64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, IsBuiltin: true},
|
||||
{Name: "Rock", Type: "ISO15Band", Preamp: 0, Bands: []float64{5, 4, 3, 1, -1, -1, 0, 2, 3, 4, 4, 4, 3, 2, 2}, IsBuiltin: true},
|
||||
{Name: "Pop", Type: "ISO15Band", Preamp: 0, Bands: []float64{-1, -1, 0, 2, 4, 4, 2, 0, -1, -1, 0, 1, 2, 3, 3}, IsBuiltin: true},
|
||||
{Name: "Jazz", Type: "ISO15Band", Preamp: 0, Bands: []float64{4, 3, 1, 2, -2, -2, 0, 2, 3, 3, 3, 4, 4, 4, 4}, IsBuiltin: true},
|
||||
{Name: "Classical", Type: "ISO15Band", Preamp: 0, Bands: []float64{5, 4, 3, 2, -1, -1, 0, 2, 3, 3, 3, 2, 2, 2, -1}, IsBuiltin: true},
|
||||
{Name: "Bass Boost", Type: "ISO15Band", Preamp: 0, Bands: []float64{6, 5, 4, 3, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, IsBuiltin: true},
|
||||
{Name: "Treble Boost", Type: "ISO15Band", Preamp: 0, Bands: []float64{0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 4, 5, 6, 6}, IsBuiltin: true},
|
||||
{Name: "Vocal", Type: "ISO15Band", Preamp: 0, Bands: []float64{-2, -3, -3, 1, 4, 4, 4, 3, 2, 1, 0, -1, -2, -2, -3}, IsBuiltin: true},
|
||||
{Name: "Electronic", Type: "ISO15Band", Preamp: 0, Bands: []float64{5, 4, 2, 0, -2, -2, 0, 2, 3, 4, 4, 3, 4, 4, 3}, IsBuiltin: true},
|
||||
{Name: "Acoustic", Type: "ISO15Band", Preamp: 0, Bands: []float64{5, 4, 3, 1, 2, 1, 1, 2, 2, 2, 1, 2, 2, 3, 2}, IsBuiltin: true},
|
||||
{Name: "R&B", Type: "ISO15Band", Preamp: 0, Bands: []float64{3, 6, 5, 2, -2, -2, 2, 3, 2, 2, 3, 3, 3, 3, 4}, IsBuiltin: true},
|
||||
{Name: "Loudness", Type: "ISO15Band", Preamp: 0, Bands: []float64{6, 5, 3, 0, -1, -1, -1, -1, 0, 1, 2, 4, 5, 5, 3}, IsBuiltin: true},
|
||||
}
|
||||
}
|
||||
|
||||
// LoadPresets loads all presets (builtin + user-defined)
|
||||
func (m *EQPresetManager) LoadPresets() ([]EQPreset, error) {
|
||||
// Start with builtin presets
|
||||
presets := make([]EQPreset, len(m.builtinPresets))
|
||||
copy(presets, m.builtinPresets)
|
||||
|
||||
// Create presets directory if it doesn't exist
|
||||
if err := os.MkdirAll(m.presetsDir, 0o755); err != nil {
|
||||
return presets, err
|
||||
}
|
||||
|
||||
// Load user presets
|
||||
entries, err := os.ReadDir(m.presetsDir)
|
||||
if err != nil {
|
||||
return presets, nil // Return builtins if can't read dir
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" {
|
||||
continue
|
||||
}
|
||||
|
||||
presetPath := filepath.Join(m.presetsDir, entry.Name())
|
||||
data, err := os.ReadFile(presetPath)
|
||||
if err != nil {
|
||||
continue // Skip invalid files
|
||||
}
|
||||
|
||||
var preset EQPreset
|
||||
if err := json.Unmarshal(data, &preset); err != nil {
|
||||
continue // Skip invalid JSON
|
||||
}
|
||||
|
||||
preset.IsBuiltin = false
|
||||
presets = append(presets, preset)
|
||||
}
|
||||
|
||||
// Sort: builtins first, then user presets alphabetically
|
||||
sort.Slice(presets, func(i, j int) bool {
|
||||
if presets[i].IsBuiltin != presets[j].IsBuiltin {
|
||||
return presets[i].IsBuiltin
|
||||
}
|
||||
return presets[i].Name < presets[j].Name
|
||||
})
|
||||
|
||||
return presets, nil
|
||||
}
|
||||
|
||||
// SavePreset saves a preset to disk
|
||||
func (m *EQPresetManager) SavePreset(preset EQPreset) error {
|
||||
if preset.IsBuiltin {
|
||||
return fmt.Errorf("cannot overwrite builtin preset")
|
||||
}
|
||||
|
||||
// Create presets directory if it doesn't exist
|
||||
if err := os.MkdirAll(m.presetsDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Sanitize filename
|
||||
filename := sanitizeFilename(preset.Name) + ".json"
|
||||
presetPath := filepath.Join(m.presetsDir, filename)
|
||||
|
||||
data, err := json.MarshalIndent(preset, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(presetPath, data, 0o644)
|
||||
}
|
||||
|
||||
// DeletePreset deletes a user preset
|
||||
func (m *EQPresetManager) DeletePreset(name string) error {
|
||||
// Check if it's a builtin preset
|
||||
for _, bp := range m.builtinPresets {
|
||||
if bp.Name == name {
|
||||
return fmt.Errorf("cannot delete builtin preset")
|
||||
}
|
||||
}
|
||||
|
||||
filename := sanitizeFilename(name) + ".json"
|
||||
presetPath := filepath.Join(m.presetsDir, filename)
|
||||
|
||||
return os.Remove(presetPath)
|
||||
}
|
||||
|
||||
// sanitizeFilename removes characters that aren't safe for filenames
|
||||
func sanitizeFilename(name string) string {
|
||||
// Simple sanitization - could be enhanced
|
||||
var result []rune
|
||||
for _, r := range name {
|
||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_' || r == ' ' {
|
||||
result = append(result, r)
|
||||
}
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
package mediaprovider
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
)
|
||||
|
||||
// Bit field flag for the ReleaseTypes property
|
||||
type ReleaseType = int32
|
||||
@@ -282,6 +286,7 @@ const (
|
||||
ContentTypeTrack
|
||||
ContentTypeGenre
|
||||
ContentTypeRadioStation
|
||||
ContentTypeOther
|
||||
)
|
||||
|
||||
func (c ContentType) String() string {
|
||||
@@ -298,6 +303,8 @@ func (c ContentType) String() string {
|
||||
return "Genre"
|
||||
case ContentTypeRadioStation:
|
||||
return "Radio station"
|
||||
case ContentTypeOther:
|
||||
return "Other"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
@@ -309,6 +316,9 @@ type SearchResult struct {
|
||||
CoverID string
|
||||
Type ContentType
|
||||
|
||||
// Optional icon to display instead of the default for this content type
|
||||
Icon fyne.Resource
|
||||
|
||||
// for Album / Playlist: track count
|
||||
// Artist / Genre: album count
|
||||
// Track: length (seconds)
|
||||
|
||||
@@ -120,3 +120,49 @@ func (w WidthType) String() string {
|
||||
}
|
||||
return "x" // not reached
|
||||
}
|
||||
|
||||
type ISO10BandEqualizer struct {
|
||||
Disabled bool
|
||||
EQPreamp float64
|
||||
BandGains [10]float64
|
||||
}
|
||||
|
||||
var (
|
||||
iso10Bands = []string{"31", "62", "125", "250", "500", "1k", "2k", "4k", "8k", "16k"}
|
||||
iso10FMult = 2.0 // Octave doubling
|
||||
)
|
||||
|
||||
var _ Equalizer = (*ISO10BandEqualizer)(nil)
|
||||
|
||||
func (i *ISO10BandEqualizer) IsEnabled() bool {
|
||||
return !i.Disabled
|
||||
}
|
||||
|
||||
func (i *ISO10BandEqualizer) Preamp() float64 {
|
||||
return i.EQPreamp
|
||||
}
|
||||
|
||||
func (i *ISO10BandEqualizer) Curve() EqualizerCurve {
|
||||
fC := float64(31.25)
|
||||
curve := make([]EqualizerBand, 0, len(i.BandGains))
|
||||
for _, bandGain := range i.BandGains {
|
||||
curve = append(curve, EqualizerBand{
|
||||
Frequency: int(math.Round(fC)),
|
||||
Width: 1.0,
|
||||
WidthType: WidthTypeOctave,
|
||||
Gain: bandGain,
|
||||
})
|
||||
fC *= iso10FMult
|
||||
}
|
||||
return curve
|
||||
}
|
||||
|
||||
func (*ISO10BandEqualizer) BandFrequencies() []string {
|
||||
ret := make([]string, len(iso10Bands))
|
||||
copy(ret, iso10Bands)
|
||||
return ret
|
||||
}
|
||||
|
||||
func (*ISO10BandEqualizer) Type() string {
|
||||
return "ISO10Band"
|
||||
}
|
||||
|
||||
+674
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
+177
-97
File diff suppressed because one or more lines are too long
+3
-1
@@ -22,6 +22,8 @@ fyne bundle -append -prefix Res icons/publicdomain/theatermasks.svg >> bundled.g
|
||||
fyne bundle -append -prefix Res icons/publicdomain/grid.svg >> bundled.go
|
||||
fyne bundle -append -prefix Res icons/publicdomain/list.svg >> bundled.go
|
||||
fyne bundle -append -prefix Res icons/publicdomain/filter.svg >> bundled.go
|
||||
fyne bundle -append -prefix Res icons/publicdomain/save.svg >> bundled.go
|
||||
fyne bundle -append -prefix Res icons/publicdomain/saveas.svg >> bundled.go
|
||||
fyne bundle -append -prefix Res icons/remix_design/broadcast.svg >> bundled.go
|
||||
fyne bundle -append -prefix Res icons/remix_design/repeat.svg >> bundled.go
|
||||
fyne bundle -append -prefix Res icons/remix_design/repeatone.svg >> bundled.go
|
||||
@@ -31,6 +33,6 @@ fyne bundle -append -prefix Res icons/remix_design/updownarrow.svg >> bundled.go
|
||||
|
||||
fyne bundle -append -prefix Res themes/default.toml >> bundled.go
|
||||
|
||||
fyne bundle -append -prefix Res ../LICENSE >> bundled.go
|
||||
fyne bundle -append -prefix Res LICENSE >> bundled.go
|
||||
fyne bundle -append -prefix Res licenses/BSDLICENSE >> bundled.go
|
||||
fyne bundle -append -prefix Res licenses/MITLICENSE >> bundled.go
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="128" height="128" version="1.1">
|
||||
<title>Clarity Icon</title>
|
||||
<desc>This is shape (source) for Clarity vector icon theme for gtk</desc>
|
||||
<metadata>
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:title>Clarity Icon</dc:title>
|
||||
<dc:description>This is shape (source) for Clarity vector icon theme for gtk</dc:description>
|
||||
<dc:creator>
|
||||
<cc:Agent>
|
||||
<dc:title>Jakub Jankiewicz</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:creator>
|
||||
<dc:rights>
|
||||
<cc:Agent>
|
||||
<dc:title>Jakub Jankiewicz</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:rights>
|
||||
<dc:date>2010</dc:date>
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<cc:license rdf:resource="http://creativecommons.org/licenses/by-sa/3.0/" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<path d="m 19.699937,14.248006 c -3.96276,0 -7.12317,3.160405 -7.12317,7.123169 l 0,73.871171 c 0,1.773366 0.58862,2.346919 1.61625,3.374548 l 13.90492,13.904916 c 1.05639,1.05639 1.87534,1.2957 3.23955,1.2957 l 76.968473,0 c 3.96276,0 7.15414,-3.19137 7.15414,-7.15414 l 0,-85.292195 c 0,-3.962764 -3.19138,-7.123169 -7.15414,-7.123169 z m 7.37093,10.870575 73.895133,0 0,34.438972 -73.895133,0 z m 13.28626,46.393508 42.92483,0 c 3.96276,0 7.15414,3.191375 7.15414,7.154139 l 0,20.378456 c 0,3.962756 -3.19138,7.154136 -7.15414,7.154136 l -42.92483,0 c -3.96276,0 -7.12317,-3.19138 -7.12317,-7.154136 l 0,-20.378456 c 0,-3.962764 3.16041,-7.154139 7.12317,-7.154139 z m 2.94217,2.849267 0,23.878101 15.08254,0 0,-23.878101 z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="128" height="128" version="1.1">
|
||||
<title>Save-As Icon</title>
|
||||
<desc>This is shape (source) for Clarity vector icon theme for gtk</desc>
|
||||
<metadata>
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:title>Save-As Icon</dc:title>
|
||||
<dc:description>This is shape (source) for Clarity vector icon theme for gtk</dc:description>
|
||||
<dc:creator>
|
||||
<cc:Agent>
|
||||
<dc:title>Jakub Jankiewicz</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:creator>
|
||||
<dc:rights>
|
||||
<cc:Agent>
|
||||
<dc:title>Jakub Jankiewicz</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:rights>
|
||||
<dc:date>2010</dc:date>
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<cc:license rdf:resource="http://creativecommons.org/licenses/by-sa/3.0/" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<path d="m 106.14802,9.8174808 c -0.69945,0.05836 -1.44764,0.3851432 -2.03125,0.9687502 l -6.312497,6.3125 13.937497,13.9375 6.34375,-6.3125 c 1.16722,-1.167213 1.31701,-2.870496 0.34375,-3.84375 l -10.4375,-10.4375 c -0.48663,-0.4866312 -1.1443,-0.6833572 -1.84375,-0.6250002 z M 16.054273,18.629981 c -3.96276,0 -7.1249997,3.162236 -7.1249997,7.125 l 0,73.84375 c 0,1.773369 0.59737,2.347369 1.6249997,3.374999 l 13.90625,13.90625 c 1.05639,1.05639 1.88579,1.3125 3.25,1.3125 l 76.968747,0 c 3.96276,0 7.125,-3.19348 7.125,-7.15625 l 0,-76.062499 -8.21875,8.21875 a 2.8488206,2.8488206 0 0 1 -1.0625,0.6875 2.8488206,2.8488206 0 0 1 -0.65625,1.03125 l -4.531247,4.53125 0,14.5 -15.21875,0 -20.84375,8.5625 c -0.50376,0.206891 -1.03178,0.396671 -1.75,0.4375 -0.71822,0.04083 -1.89183,-0.157309 -2.71875,-1.0625 -0.82692,-0.905191 -0.90228,-1.980261 -0.84375,-2.65625 0.05853,-0.675989 0.23289,-1.18929 0.4375,-1.6875 l 1.46875,-3.59375 -34.4375,0 0,-34.46875 57.96875,0 2.53125,-2.53125 a 2.8488206,2.8488206 0 0 1 1.0625,-0.6875 2.8488206,2.8488206 0 0 1 0.65625,-1 l 6.625,-6.625 -76.21875,0 z m 80.03125,0.1875 -8.4375,8.4375 13.937497,13.9375 8.4375,-8.4375 -13.937497,-13.9375 z m -10.15625,10.15625 -15.59375,15.59375 13.96875,13.9375 15.59375,-15.59375 -13.96875,-13.9375 z m -17.34375,17.59375 c -0.32967,0.02101 -0.6214,0.329524 -0.78125,0.71875 l -8.78125,21.375 c -0.54748,1.333046 -0.14826,1.754499 1.15625,1.21875 l 21.25,-8.71875 c 0.83734,-0.343888 1.12932,-0.933185 0.6875,-1.375 l -12.90625,-12.90625 c -0.22309,-0.223089 -0.4272,-0.325107 -0.625,-0.3125 z m -31.875,29.3125 42.9375,0 c 3.96276,0 7.15625,3.193486 7.15625,7.15625 l 0,20.374999 c 0,3.96276 -3.19349,7.15625 -7.15625,7.15625 l -42.9375,0 c -3.96276,0 -7.125,-3.19349 -7.125,-7.15625 l 0,-20.374999 c 0,-3.962764 3.16224,-7.15625 7.125,-7.15625 z m 2.9375,2.84375 0,23.874999 15.09375,0 0,-23.874999 -15.09375,0 z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -36,6 +36,7 @@
|
||||
"Aug": "Aug",
|
||||
"Authentication failed": "Authentifizierung fehlgeschlagen",
|
||||
"Auto": "Auto",
|
||||
"AutoEQ": "AutoEQ",
|
||||
"Automatically check for updates": "Automatically check for updates",
|
||||
"Autoplay": "Autoplay",
|
||||
"Autoselect device": "Automatische Geräteauswahl",
|
||||
@@ -45,10 +46,15 @@
|
||||
"Bit rate": "Bitrate",
|
||||
"Bold font": "Bold font",
|
||||
"Broadcast": "Broadcast",
|
||||
"Browse Headphone Profiles": "Browse Headphone Profiles",
|
||||
"Cancel": "Abbrechen",
|
||||
"Cannot Delete": "Cannot Delete",
|
||||
"Cannot delete builtin presets": "Cannot delete builtin presets",
|
||||
"Cannot use the name of a builtin preset": "Cannot use the name of a builtin preset",
|
||||
"Cast to device": "Cast to device",
|
||||
"Channels": "Channels",
|
||||
"Check for Updates": "Nach Updates suchen",
|
||||
"Check network connection and try again": "Check network connection and try again",
|
||||
"Clear caches": "Clear caches",
|
||||
"Close": "Schließen",
|
||||
"Close to system tray": "In Taskleiste minimieren",
|
||||
@@ -69,7 +75,10 @@
|
||||
"DJ-Mix": "DJ-Mix",
|
||||
"Date added": "Date added",
|
||||
"Dec": "Dec",
|
||||
"Delete": "Delete",
|
||||
"Delete Playlist": "Playlist löschen",
|
||||
"Delete Preset": "Delete Preset",
|
||||
"Delete preset '%s'?": "Delete preset '%s'?",
|
||||
"Demo": "Demo",
|
||||
"Disable automatic DPI adjustment": "Disable automatic DPI adjustment",
|
||||
"Disable server transcoding": "Server-Transkodierung deaktivieren",
|
||||
@@ -80,6 +89,22 @@
|
||||
"Duration": "Dauer",
|
||||
"EP": "EP",
|
||||
"EPs": "EPs",
|
||||
"EQ Acoustic": "Acoustic",
|
||||
"EQ Bass Boost": "Bass Boost",
|
||||
"EQ Classical": "Classical",
|
||||
"EQ Electronic": "Electronic",
|
||||
"EQ Flat": "Flat",
|
||||
"EQ Jazz": "Jazz",
|
||||
"EQ Loudness": "Loudness",
|
||||
"EQ Pop": "Pop",
|
||||
"EQ Preamp": "Pre",
|
||||
"EQ Preset": "Preset",
|
||||
"EQ Preset:": "Preset:",
|
||||
"EQ R\u0026B": "R\u0026B",
|
||||
"EQ Rock": "Rock",
|
||||
"EQ Treble Boost": "Treble Boost",
|
||||
"EQ Type:": "EQ Type:",
|
||||
"EQ Vocal": "Vocal",
|
||||
"Edit": "Bearbeiten",
|
||||
"Edit Playlist": "Playlist bearbeiten",
|
||||
"Edit server": "Server bearbeiten",
|
||||
@@ -91,9 +116,11 @@
|
||||
"Equalizer": "Equalizer",
|
||||
"Error": "Error",
|
||||
"Error creating playlist": "Error creating playlist",
|
||||
"Error loading AutoEQ profiles": "Error loading AutoEQ profiles",
|
||||
"Error updating playlist": "Error updating playlist",
|
||||
"Exclusive mode": "Exklusiver Modus",
|
||||
"Fade out on pause": "Fade out on pause",
|
||||
"Failed to load profile": "Failed to load profile",
|
||||
"Fav.": "Fav.",
|
||||
"Favorites": "Favoriten",
|
||||
"Feb": "Feb",
|
||||
@@ -117,6 +144,7 @@
|
||||
"In order": "In order",
|
||||
"Internet Radio Stations": "Internet-Radiosender",
|
||||
"Interview": "Interview",
|
||||
"Invalid Name": "Invalid Name",
|
||||
"Is favorite": "Ist Favorit",
|
||||
"Is not favorite": "Ist kein Favorit",
|
||||
"Jan": "Jan",
|
||||
@@ -141,9 +169,11 @@
|
||||
"My Server": "Mein Server",
|
||||
"Name": "Name",
|
||||
"Name (A-Z)": "Name (A-Z)",
|
||||
"Network error. Check connection.": "Network error. Check connection.",
|
||||
"New Playlist": "New Playlist",
|
||||
"Next": "Nächster",
|
||||
"Nickname": "Spitzname",
|
||||
"No Preset Selected": "No Preset Selected",
|
||||
"No new version found": "Keine neue Version gefunden",
|
||||
"No radio stations available": "Keine Radiosender verfügbar",
|
||||
"None": "Keine",
|
||||
@@ -153,6 +183,7 @@
|
||||
"Now Playing": "Aktuelle Wiedergabe",
|
||||
"OK": "OK",
|
||||
"Oct": "Okt",
|
||||
"Overwrite Preset": "Overwrite Preset",
|
||||
"Owner": "Besitzer",
|
||||
"Password": "Passwort",
|
||||
"Pause": "Pause",
|
||||
@@ -173,10 +204,15 @@
|
||||
"Playlist": "Playlist",
|
||||
"Playlists": "Playlists",
|
||||
"Plays": "Wiedergaben",
|
||||
"Please select a preset to delete": "Please select a preset to delete",
|
||||
"Preset '%s' already exists. Overwrite?": "Preset '%s' already exists. Overwrite?",
|
||||
"Preset name": "Preset name",
|
||||
"Prevent clipping": "Übersteuerung vermeiden",
|
||||
"Prevent screensaver on Now Playing page": "Prevent screensaver on Now Playing page",
|
||||
"Previous": "Vorheriger",
|
||||
"Private playlist by": "Private Playlist von",
|
||||
"Profile": "Profile",
|
||||
"Profile not found": "Profile not found",
|
||||
"Public": "Public",
|
||||
"Public playlist by": "Öffentliche Playlist von",
|
||||
"Quit": "Beenden",
|
||||
@@ -193,13 +229,19 @@
|
||||
"ReplayGain mode": "ReplayGain-Modus",
|
||||
"ReplayGain preamp": "ReplayGain-Vorverstärker",
|
||||
"Rescan Library": "Bibliothek neu scannen",
|
||||
"Reset": "Reset",
|
||||
"Restart required": "Neustart erforderlich",
|
||||
"Sample rate": "Sample rate",
|
||||
"Save": "Save",
|
||||
"Save As": "Save As",
|
||||
"Save Preset": "Save Preset",
|
||||
"Save Preset As": "Save Preset As",
|
||||
"Save play queue": "Wiedergabeliste beim Verlassen speichern",
|
||||
"Saved at": "Gespeichert am",
|
||||
"Scrobble when": "Scrobble wenn",
|
||||
"Search": "Suche",
|
||||
"Search Everywhere": "Überall suchen",
|
||||
"Search headphones...": "Search headphones...",
|
||||
"Search page": "Suchseite",
|
||||
"Search playlists or new playlist name": "Nach Playlist suchen oder Namen für neue Playlist eingeben",
|
||||
"Select Library": "Select Library",
|
||||
@@ -319,4 +361,4 @@
|
||||
"{{.trackCount}} tracks": {
|
||||
"other": "{{.trackCount}} Titel"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@
|
||||
"Aug": "Aug",
|
||||
"Authentication failed": "Authentication failed",
|
||||
"Auto": "Auto",
|
||||
"AutoEQ": "AutoEQ",
|
||||
"Automatically check for updates": "Automatically check for updates",
|
||||
"Autoplay": "Autoplay",
|
||||
"Autoselect device": "Autoselect device",
|
||||
@@ -46,10 +47,15 @@
|
||||
"Bit rate": "Bit rate",
|
||||
"Bold font": "Bold font",
|
||||
"Broadcast": "Broadcast",
|
||||
"Browse Headphone Profiles": "Browse Headphone Profiles",
|
||||
"Cancel": "Cancel",
|
||||
"Cannot Delete": "Cannot Delete",
|
||||
"Cannot delete builtin presets": "Cannot delete builtin presets",
|
||||
"Cannot use the name of a builtin preset": "Cannot use the name of a builtin preset",
|
||||
"Cast to device": "Cast to device",
|
||||
"Channels": "Channels",
|
||||
"Check for Updates": "Check for Updates",
|
||||
"Check network connection and try again": "Check network connection and try again",
|
||||
"Clear caches": "Clear caches",
|
||||
"Close": "Close",
|
||||
"Close to system tray": "Close to system tray",
|
||||
@@ -70,7 +76,10 @@
|
||||
"DJ-Mix": "DJ-Mix",
|
||||
"Date added": "Date added",
|
||||
"Dec": "Dec",
|
||||
"Delete": "Delete",
|
||||
"Delete Playlist": "Delete Playlist",
|
||||
"Delete Preset": "Delete Preset",
|
||||
"Delete preset '%s'?": "Delete preset '%s'?",
|
||||
"Demo": "Demo",
|
||||
"Disable automatic DPI adjustment": "Disable automatic DPI adjustment",
|
||||
"Disable server transcoding": "Disable server transcoding",
|
||||
@@ -81,6 +90,22 @@
|
||||
"Duration": "Duration",
|
||||
"EP": "EP",
|
||||
"EPs": "EPs",
|
||||
"EQ Acoustic": "Acoustic",
|
||||
"EQ Bass Boost": "Bass Boost",
|
||||
"EQ Classical": "Classical",
|
||||
"EQ Electronic": "Electronic",
|
||||
"EQ Flat": "Flat",
|
||||
"EQ Jazz": "Jazz",
|
||||
"EQ Loudness": "Loudness",
|
||||
"EQ Pop": "Pop",
|
||||
"EQ Preamp": "Pre",
|
||||
"EQ Preset": "Preset",
|
||||
"EQ Preset:": "Preset:",
|
||||
"EQ R\u0026B": "R\u0026B",
|
||||
"EQ Rock": "Rock",
|
||||
"EQ Treble Boost": "Treble Boost",
|
||||
"EQ Type:": "EQ Type:",
|
||||
"EQ Vocal": "Vocal",
|
||||
"Edit": "Edit",
|
||||
"Edit Playlist": "Edit Playlist",
|
||||
"Edit server": "Edit server",
|
||||
@@ -92,9 +117,11 @@
|
||||
"Equalizer": "Equalizer",
|
||||
"Error": "Error",
|
||||
"Error creating playlist": "Error creating playlist",
|
||||
"Error loading AutoEQ profiles": "Error loading AutoEQ profiles",
|
||||
"Error updating playlist": "Error updating playlist",
|
||||
"Exclusive mode": "Exclusive mode",
|
||||
"Fade out on pause": "Fade out on pause",
|
||||
"Failed to load profile": "Failed to load profile",
|
||||
"Fav.": "Fav.",
|
||||
"Favorites": "Favorites",
|
||||
"Feb": "Feb",
|
||||
@@ -118,6 +145,7 @@
|
||||
"In order": "In order",
|
||||
"Internet Radio Stations": "Internet Radio Stations",
|
||||
"Interview": "Interview",
|
||||
"Invalid Name": "Invalid Name",
|
||||
"Is favorite": "Is favorite",
|
||||
"Is not favorite": "Is not favorite",
|
||||
"Jan": "Jan",
|
||||
@@ -142,9 +170,11 @@
|
||||
"My Server": "My Server",
|
||||
"Name": "Name",
|
||||
"Name (A-Z)": "Name (A-Z)",
|
||||
"Network error. Check connection.": "Network error. Check connection.",
|
||||
"New Playlist": "New Playlist",
|
||||
"Next": "Next",
|
||||
"Nickname": "Nickname",
|
||||
"No Preset Selected": "No Preset Selected",
|
||||
"No new version found": "No new version found",
|
||||
"No radio stations available": "No radio stations available",
|
||||
"None": "None",
|
||||
@@ -154,6 +184,7 @@
|
||||
"Now Playing": "Now Playing",
|
||||
"OK": "OK",
|
||||
"Oct": "Oct",
|
||||
"Overwrite Preset": "Overwrite Preset",
|
||||
"Owner": "Owner",
|
||||
"Password": "Password",
|
||||
"Pause": "Pause",
|
||||
@@ -174,10 +205,15 @@
|
||||
"Playlist": "Playlist",
|
||||
"Playlists": "Playlists",
|
||||
"Plays": "Plays",
|
||||
"Please select a preset to delete": "Please select a preset to delete",
|
||||
"Preset '%s' already exists. Overwrite?": "Preset '%s' already exists. Overwrite?",
|
||||
"Preset name": "Preset name",
|
||||
"Prevent clipping": "Prevent clipping",
|
||||
"Prevent screensaver on Now Playing page": "Prevent screensaver on Now Playing page",
|
||||
"Previous": "Previous",
|
||||
"Private playlist by": "Private playlist by",
|
||||
"Profile": "Profile",
|
||||
"Profile not found": "Profile not found",
|
||||
"Public": "Public",
|
||||
"Public playlist by": "Public playlist by",
|
||||
"Quit": "Quit",
|
||||
@@ -194,13 +230,19 @@
|
||||
"ReplayGain mode": "ReplayGain mode",
|
||||
"ReplayGain preamp": "ReplayGain preamp",
|
||||
"Rescan Library": "Rescan Library",
|
||||
"Reset": "Reset",
|
||||
"Restart required": "Restart required",
|
||||
"Sample rate": "Sample rate",
|
||||
"Save": "Save",
|
||||
"Save As": "Save As",
|
||||
"Save Preset": "Save Preset",
|
||||
"Save Preset As": "Save Preset As",
|
||||
"Save play queue": "Save play queue",
|
||||
"Saved at": "Saved at",
|
||||
"Scrobble when": "Scrobble when",
|
||||
"Search": "Search",
|
||||
"Search Everywhere": "Search Everywhere",
|
||||
"Search headphones...": "Search headphones...",
|
||||
"Search page": "Search page",
|
||||
"Search playlists or new playlist name": "Search playlists or new playlist name",
|
||||
"Select Library": "Select Library",
|
||||
@@ -340,4 +382,4 @@
|
||||
"one": "{{.trackCount}} track",
|
||||
"other": "{{.trackCount}} tracks"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@
|
||||
"Aug": "Ago",
|
||||
"Authentication failed": "Autenticación fallida",
|
||||
"Auto": "Automático",
|
||||
"AutoEQ": "AutoEQ",
|
||||
"Automatically check for updates": "Automatically check for updates",
|
||||
"Autoplay": "Autoplay",
|
||||
"Autoselect device": "Seleccionar dispositivo automáticamente",
|
||||
@@ -45,10 +46,15 @@
|
||||
"Bit rate": "Tasa de bits",
|
||||
"Bold font": "Bold font",
|
||||
"Broadcast": "Transmisión",
|
||||
"Browse Headphone Profiles": "Browse Headphone Profiles",
|
||||
"Cancel": "Cancelar",
|
||||
"Cannot Delete": "Cannot Delete",
|
||||
"Cannot delete builtin presets": "Cannot delete builtin presets",
|
||||
"Cannot use the name of a builtin preset": "Cannot use the name of a builtin preset",
|
||||
"Cast to device": "Cast to device",
|
||||
"Channels": "Channels",
|
||||
"Check for Updates": "Buscar actualizaciones",
|
||||
"Check network connection and try again": "Check network connection and try again",
|
||||
"Clear caches": "Borrar cachés",
|
||||
"Close": "Cerrar",
|
||||
"Close to system tray": "Cerrar a la bandeja del sistema",
|
||||
@@ -69,7 +75,10 @@
|
||||
"DJ-Mix": "Mezcla de DJ",
|
||||
"Date added": "Date added",
|
||||
"Dec": "Dic",
|
||||
"Delete": "Delete",
|
||||
"Delete Playlist": "Eliminar lista de reproducción",
|
||||
"Delete Preset": "Delete Preset",
|
||||
"Delete preset '%s'?": "Delete preset '%s'?",
|
||||
"Demo": "Demo",
|
||||
"Disable automatic DPI adjustment": "Disable automatic DPI adjustment",
|
||||
"Disable server transcoding": "Desactivar transcodificación del servidor",
|
||||
@@ -80,6 +89,22 @@
|
||||
"Duration": "Duración",
|
||||
"EP": "EP",
|
||||
"EPs": "EPs",
|
||||
"EQ Acoustic": "Acoustic",
|
||||
"EQ Bass Boost": "Bass Boost",
|
||||
"EQ Classical": "Classical",
|
||||
"EQ Electronic": "Electronic",
|
||||
"EQ Flat": "Flat",
|
||||
"EQ Jazz": "Jazz",
|
||||
"EQ Loudness": "Loudness",
|
||||
"EQ Pop": "Pop",
|
||||
"EQ Preamp": "Pre",
|
||||
"EQ Preset": "Preset",
|
||||
"EQ Preset:": "Preset:",
|
||||
"EQ R\u0026B": "R\u0026B",
|
||||
"EQ Rock": "Rock",
|
||||
"EQ Treble Boost": "Treble Boost",
|
||||
"EQ Type:": "EQ Type:",
|
||||
"EQ Vocal": "Vocal",
|
||||
"Edit": "Editar",
|
||||
"Edit Playlist": "Editar lista de reproducción",
|
||||
"Edit server": "Editar servidor",
|
||||
@@ -91,9 +116,11 @@
|
||||
"Equalizer": "Ecualizador",
|
||||
"Error": "Error",
|
||||
"Error creating playlist": "Ha ocurrido un error al crear la lista de reproducción",
|
||||
"Error loading AutoEQ profiles": "Error loading AutoEQ profiles",
|
||||
"Error updating playlist": "Error updating playlist",
|
||||
"Exclusive mode": "Modo exclusivo",
|
||||
"Fade out on pause": "Fade out on pause",
|
||||
"Failed to load profile": "Failed to load profile",
|
||||
"Fav.": "Fav.",
|
||||
"Favorites": "Favoritos",
|
||||
"Feb": "Feb",
|
||||
@@ -117,6 +144,7 @@
|
||||
"In order": "In order",
|
||||
"Internet Radio Stations": "Estaciones de radio por Internet",
|
||||
"Interview": "Entrevista",
|
||||
"Invalid Name": "Invalid Name",
|
||||
"Is favorite": "Es favorito",
|
||||
"Is not favorite": "No es favorito",
|
||||
"Jan": "Ene",
|
||||
@@ -141,9 +169,11 @@
|
||||
"My Server": "Mi servidor",
|
||||
"Name": "Nombre",
|
||||
"Name (A-Z)": "Nombre (A-Z)",
|
||||
"Network error. Check connection.": "Network error. Check connection.",
|
||||
"New Playlist": "Crear Nueva",
|
||||
"Next": "Siguiente",
|
||||
"Nickname": "Apodo",
|
||||
"No Preset Selected": "No Preset Selected",
|
||||
"No new version found": "No se encontró nueva versión",
|
||||
"No radio stations available": "No hay estaciones de radio disponibles",
|
||||
"None": "Ninguno",
|
||||
@@ -153,6 +183,7 @@
|
||||
"Now Playing": "Reproduciendo",
|
||||
"OK": "OK",
|
||||
"Oct": "Oct",
|
||||
"Overwrite Preset": "Overwrite Preset",
|
||||
"Owner": "Propietario",
|
||||
"Password": "Contraseña",
|
||||
"Pause": "Pausar",
|
||||
@@ -173,10 +204,15 @@
|
||||
"Playlist": "Lista de reproducción",
|
||||
"Playlists": "Listas de reproducción",
|
||||
"Plays": "Reproducciones",
|
||||
"Please select a preset to delete": "Please select a preset to delete",
|
||||
"Preset '%s' already exists. Overwrite?": "Preset '%s' already exists. Overwrite?",
|
||||
"Preset name": "Preset name",
|
||||
"Prevent clipping": "Prevenir recortes",
|
||||
"Prevent screensaver on Now Playing page": "Prevent screensaver on Now Playing page",
|
||||
"Previous": "Anterior",
|
||||
"Private playlist by": "Lista de reproducción privada de",
|
||||
"Profile": "Profile",
|
||||
"Profile not found": "Profile not found",
|
||||
"Public": "Public",
|
||||
"Public playlist by": "Lista de reproducción pública de",
|
||||
"Quit": "Salir",
|
||||
@@ -193,13 +229,19 @@
|
||||
"ReplayGain mode": "Modo ReplayGain",
|
||||
"ReplayGain preamp": "Preamp ReplayGain",
|
||||
"Rescan Library": "Reescanear la biblioteca",
|
||||
"Reset": "Reset",
|
||||
"Restart required": "Reinicio requerido",
|
||||
"Sample rate": "Sample rate",
|
||||
"Save": "Save",
|
||||
"Save As": "Save As",
|
||||
"Save Preset": "Save Preset",
|
||||
"Save Preset As": "Save Preset As",
|
||||
"Save play queue": "Guardar cola de reproducción",
|
||||
"Saved at": "Guardado en",
|
||||
"Scrobble when": "Scrobble cuando",
|
||||
"Search": "Buscar",
|
||||
"Search Everywhere": "Buscar en todas partes",
|
||||
"Search headphones...": "Search headphones...",
|
||||
"Search page": "Buscar en la página",
|
||||
"Search playlists or new playlist name": "Buscar listas de reproducción o nombre de nueva lista",
|
||||
"Select Library": "Select Library",
|
||||
@@ -330,4 +372,4 @@
|
||||
"one": "hace un año",
|
||||
"other": "hace {{.years}} años"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@
|
||||
"Aug": "Août",
|
||||
"Authentication failed": "Échec d'authentification",
|
||||
"Auto": "Automatique",
|
||||
"AutoEQ": "AutoEQ",
|
||||
"Automatically check for updates": "Vérifier automatiquement les mises à jour",
|
||||
"Autoplay": "Lecture auto",
|
||||
"Autoselect device": "Sélection automatique",
|
||||
@@ -46,10 +47,15 @@
|
||||
"Bit rate": "Débit binaire",
|
||||
"Bold font": "Police en gras",
|
||||
"Broadcast": "Broadcast",
|
||||
"Browse Headphone Profiles": "Browse Headphone Profiles",
|
||||
"Cancel": "Annuler",
|
||||
"Cannot Delete": "Cannot Delete",
|
||||
"Cannot delete builtin presets": "Cannot delete builtin presets",
|
||||
"Cannot use the name of a builtin preset": "Cannot use the name of a builtin preset",
|
||||
"Cast to device": "Diffuser sur un appareil",
|
||||
"Channels": "Canaux",
|
||||
"Check for Updates": "Vérifier les mises à jour",
|
||||
"Check network connection and try again": "Check network connection and try again",
|
||||
"Clear caches": "Vider les caches",
|
||||
"Close": "Fermer",
|
||||
"Close to system tray": "Réduire dans la barre d'état à la fermeture",
|
||||
@@ -70,7 +76,10 @@
|
||||
"DJ-Mix": "DJ mix",
|
||||
"Date added": "Date added",
|
||||
"Dec": "Déc",
|
||||
"Delete": "Delete",
|
||||
"Delete Playlist": "Supprimer la liste de lecture",
|
||||
"Delete Preset": "Delete Preset",
|
||||
"Delete preset '%s'?": "Delete preset '%s'?",
|
||||
"Demo": "Démo",
|
||||
"Disable automatic DPI adjustment": "Désactiver l'ajustement automatique de DPI",
|
||||
"Disable server transcoding": "Désactiver le transcodage par le serveur",
|
||||
@@ -81,6 +90,22 @@
|
||||
"Duration": "Durée",
|
||||
"EP": "EP",
|
||||
"EPs": "EPs",
|
||||
"EQ Acoustic": "Acoustic",
|
||||
"EQ Bass Boost": "Bass Boost",
|
||||
"EQ Classical": "Classical",
|
||||
"EQ Electronic": "Electronic",
|
||||
"EQ Flat": "Flat",
|
||||
"EQ Jazz": "Jazz",
|
||||
"EQ Loudness": "Loudness",
|
||||
"EQ Pop": "Pop",
|
||||
"EQ Preamp": "Pre",
|
||||
"EQ Preset": "Preset",
|
||||
"EQ Preset:": "Preset:",
|
||||
"EQ R\u0026B": "R\u0026B",
|
||||
"EQ Rock": "Rock",
|
||||
"EQ Treble Boost": "Treble Boost",
|
||||
"EQ Type:": "EQ Type:",
|
||||
"EQ Vocal": "Vocal",
|
||||
"Edit": "Modifier",
|
||||
"Edit Playlist": "Modifier la liste de lecture",
|
||||
"Edit server": "Modifier le serveur",
|
||||
@@ -92,9 +117,11 @@
|
||||
"Equalizer": "Égaliseur",
|
||||
"Error": "Erreur",
|
||||
"Error creating playlist": "Error creating playlist",
|
||||
"Error loading AutoEQ profiles": "Error loading AutoEQ profiles",
|
||||
"Error updating playlist": "Error updating playlist",
|
||||
"Exclusive mode": "Mode exclusif",
|
||||
"Fade out on pause": "Fade out on pause",
|
||||
"Failed to load profile": "Failed to load profile",
|
||||
"Fav.": "Fav.",
|
||||
"Favorites": "Favoris",
|
||||
"Feb": "Févr",
|
||||
@@ -118,6 +145,7 @@
|
||||
"In order": "Dans l'ordre",
|
||||
"Internet Radio Stations": "Stations de radio Internet",
|
||||
"Interview": "Interview",
|
||||
"Invalid Name": "Invalid Name",
|
||||
"Is favorite": "Favoris",
|
||||
"Is not favorite": "Non favoris",
|
||||
"Jan": "Janv",
|
||||
@@ -142,9 +170,11 @@
|
||||
"My Server": "Mon serveur",
|
||||
"Name": "Nom",
|
||||
"Name (A-Z)": "Nom (A-Z)",
|
||||
"Network error. Check connection.": "Network error. Check connection.",
|
||||
"New Playlist": "New Playlist",
|
||||
"Next": "Suivante",
|
||||
"Nickname": "Surnom",
|
||||
"No Preset Selected": "No Preset Selected",
|
||||
"No new version found": "Aucune nouvelle version n'a été trouvée",
|
||||
"No radio stations available": "Aucune station de radio disponible",
|
||||
"None": "Aucun",
|
||||
@@ -154,6 +184,7 @@
|
||||
"Now Playing": "Lecture en cours",
|
||||
"OK": "OK",
|
||||
"Oct": "Oct",
|
||||
"Overwrite Preset": "Overwrite Preset",
|
||||
"Owner": "Propriétaire",
|
||||
"Password": "Mot de passe",
|
||||
"Pause": "Pause",
|
||||
@@ -174,10 +205,15 @@
|
||||
"Playlist": "Liste de lecture",
|
||||
"Playlists": "Listes de lecture",
|
||||
"Plays": "Lectures",
|
||||
"Please select a preset to delete": "Please select a preset to delete",
|
||||
"Preset '%s' already exists. Overwrite?": "Preset '%s' already exists. Overwrite?",
|
||||
"Preset name": "Preset name",
|
||||
"Prevent clipping": "Empêcher le clipping",
|
||||
"Prevent screensaver on Now Playing page": "Prevent screensaver on Now Playing page",
|
||||
"Previous": "Précédente",
|
||||
"Private playlist by": "Liste de lecture privée créée par",
|
||||
"Profile": "Profile",
|
||||
"Profile not found": "Profile not found",
|
||||
"Public": "Public",
|
||||
"Public playlist by": "Liste de lecture publique créée par",
|
||||
"Quit": "Quitter",
|
||||
@@ -194,13 +230,19 @@
|
||||
"ReplayGain mode": "Mode ReplayGain",
|
||||
"ReplayGain preamp": "Préamp. ReplayGain",
|
||||
"Rescan Library": "Analyser à nouveau la bibliothèque",
|
||||
"Reset": "Reset",
|
||||
"Restart required": "Redémarrage nécessaire",
|
||||
"Sample rate": "Taux d'échantillonnage",
|
||||
"Save": "Save",
|
||||
"Save As": "Save As",
|
||||
"Save Preset": "Save Preset",
|
||||
"Save Preset As": "Save Preset As",
|
||||
"Save play queue": "Sauvegarder la file d'attente",
|
||||
"Saved at": "Sauvegardé sur",
|
||||
"Scrobble when": "Scrobbler quand",
|
||||
"Search": "Recherche",
|
||||
"Search Everywhere": "Rechercher dans toutes les données du serveur",
|
||||
"Search headphones...": "Search headphones...",
|
||||
"Search page": "Recherche",
|
||||
"Search playlists or new playlist name": "Nom de liste de lecture à rechercher ou à créer",
|
||||
"Select Library": "Choisir une bibliothèque",
|
||||
@@ -315,4 +357,4 @@
|
||||
"x_minutes_ago": {
|
||||
"other": "59 minutes ago"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@
|
||||
"Aug": "Ago",
|
||||
"Authentication failed": "Autenticazione fallita",
|
||||
"Auto": "Automatico",
|
||||
"AutoEQ": "AutoEQ",
|
||||
"Automatically check for updates": "Controlla automaticamente gli aggiornamenti",
|
||||
"Autoplay": "Riproduzione automatica",
|
||||
"Autoselect device": "Seleziona dispositivo automaticamente",
|
||||
@@ -46,10 +47,15 @@
|
||||
"Bit rate": "Bit rate",
|
||||
"Bold font": "Font grassetto",
|
||||
"Broadcast": "Broadcast",
|
||||
"Browse Headphone Profiles": "Sfoglia Profili Cuffie",
|
||||
"Cancel": "Annulla",
|
||||
"Cannot Delete": "Impossibile Eliminare",
|
||||
"Cannot delete builtin presets": "Impossibile eliminare i preset predefiniti",
|
||||
"Cannot use the name of a builtin preset": "Non è possibile usare il nome di un preset predefinito",
|
||||
"Cast to device": "Trasmetti a dispositivo",
|
||||
"Channels": "Canali",
|
||||
"Check for Updates": "Controlla aggiornamenti",
|
||||
"Check network connection and try again": "Controlla la connessione di rete e riprova",
|
||||
"Clear caches": "Svuota cache",
|
||||
"Close": "Chiudi",
|
||||
"Close to system tray": "Chiudi nella barra di sistema",
|
||||
@@ -70,7 +76,10 @@
|
||||
"DJ-Mix": "DJ-Mix",
|
||||
"Date added": "Data aggiunta",
|
||||
"Dec": "Dic",
|
||||
"Delete": "Elimina",
|
||||
"Delete Playlist": "Elimina playlist",
|
||||
"Delete Preset": "Elimina Preset",
|
||||
"Delete preset '%s'?": "Eliminare il preset '%s'?",
|
||||
"Demo": "Demo",
|
||||
"Disable automatic DPI adjustment": "Disabilita regolazione automatica DPI",
|
||||
"Disable server transcoding": "Disabilita la transocodifica lato server",
|
||||
@@ -81,6 +90,22 @@
|
||||
"Duration": "Durata",
|
||||
"EP": "EP",
|
||||
"EPs": "EPs",
|
||||
"EQ Acoustic": "Acustico",
|
||||
"EQ Bass Boost": "Amplifica Bassi",
|
||||
"EQ Classical": "Classica",
|
||||
"EQ Electronic": "Elettronica",
|
||||
"EQ Flat": "Piatto",
|
||||
"EQ Jazz": "Jazz",
|
||||
"EQ Loudness": "Loudness",
|
||||
"EQ Pop": "Pop",
|
||||
"EQ Preamp": "Pre",
|
||||
"EQ Preset": "Preset",
|
||||
"EQ Preset:": "Preset:",
|
||||
"EQ R\u0026B": "R\u0026B",
|
||||
"EQ Rock": "Rock",
|
||||
"EQ Treble Boost": "Amplifica Alti",
|
||||
"EQ Type:": "Tipo EQ:",
|
||||
"EQ Vocal": "Vocale",
|
||||
"Edit": "Modifica",
|
||||
"Edit Playlist": "Modifica playlist",
|
||||
"Edit server": "Modifica server",
|
||||
@@ -92,9 +117,11 @@
|
||||
"Equalizer": "Equalizzatore",
|
||||
"Error": "Errore",
|
||||
"Error creating playlist": "Errore durante la creazione della playlist",
|
||||
"Error loading AutoEQ profiles": "Errore nel caricamento dei profili AutoEQ",
|
||||
"Error updating playlist": "Errore durante l'aggiornamento della playlist",
|
||||
"Exclusive mode": "Modalità esclusiva",
|
||||
"Fade out on pause": "Dissolvenza in pausa",
|
||||
"Failed to load profile": "Failed to load profile",
|
||||
"Fav.": "Pref.",
|
||||
"Favorites": "Preferiti",
|
||||
"Feb": "Feb",
|
||||
@@ -118,6 +145,7 @@
|
||||
"In order": "In ordine",
|
||||
"Internet Radio Stations": "Stazioni radio via Internet",
|
||||
"Interview": "Interview",
|
||||
"Invalid Name": "Nome non valido",
|
||||
"Is favorite": "Nei preferiti",
|
||||
"Is not favorite": "Non nei preferiti",
|
||||
"Jan": "Gen",
|
||||
@@ -127,6 +155,7 @@
|
||||
"Larger": "Più grande",
|
||||
"Last played": "Ultimo ascolto",
|
||||
"Live": "Live",
|
||||
"Load AutoEQ Profile": "Carica Profilo AutoEQ",
|
||||
"Locally": "Localmente",
|
||||
"Log Out": "Disconnetti",
|
||||
"Login to Server": "Accendi al server",
|
||||
@@ -142,9 +171,11 @@
|
||||
"My Server": "Il mio server",
|
||||
"Name": "Nome",
|
||||
"Name (A-Z)": "Nome (A-Z)",
|
||||
"Network error. Check connection.": "Network error. Check connection.",
|
||||
"New Playlist": "Nuova playlist",
|
||||
"Next": "Successiva",
|
||||
"Nickname": "Nickname",
|
||||
"No Preset Selected": "Nessun Preset Selezionato",
|
||||
"No new version found": "Nessuna nuova versione trovata",
|
||||
"No radio stations available": "Nessuna stazione radio disponibile",
|
||||
"None": "Nessuno",
|
||||
@@ -154,6 +185,7 @@
|
||||
"Now Playing": "In riproduzione",
|
||||
"OK": "OK",
|
||||
"Oct": "Ott",
|
||||
"Overwrite Preset": "Sovrascrivi preset",
|
||||
"Owner": "Proprietario",
|
||||
"Password": "Password",
|
||||
"Pause": "Pausa",
|
||||
@@ -174,10 +206,15 @@
|
||||
"Playlist": "Playlist",
|
||||
"Playlists": "Playlist",
|
||||
"Plays": "Nº ascolti",
|
||||
"Please select a preset to delete": "Seleziona un preset da eliminare",
|
||||
"Preset '%s' already exists. Overwrite?": "Il preset '%s' esiste già. Sovrascrivere?",
|
||||
"Preset name": "Nome preset",
|
||||
"Prevent clipping": "Previeni clipping",
|
||||
"Prevent screensaver on Now Playing page": "Previeni screensaver sulla pagina In riproduzione",
|
||||
"Previous": "Precedente",
|
||||
"Private playlist by": "Playlist privata by",
|
||||
"Profile": "Profilo",
|
||||
"Profile not found": "Profile not found",
|
||||
"Public": "Pubblica",
|
||||
"Public playlist by": "Playlist pubblica by",
|
||||
"Quit": "Esci",
|
||||
@@ -194,13 +231,19 @@
|
||||
"ReplayGain mode": "ReplayGain mode",
|
||||
"ReplayGain preamp": "ReplayGain preamp.",
|
||||
"Rescan Library": "Ricarica libreria",
|
||||
"Reset": "Ripristina",
|
||||
"Restart required": "Riavvio richiesto",
|
||||
"Sample rate": "Frequenza di campionamento",
|
||||
"Save": "Salva",
|
||||
"Save As": "Salva come",
|
||||
"Save Preset": "Salva Preset",
|
||||
"Save Preset As": "Salva preset come",
|
||||
"Save play queue": "Salva coda di ascolto",
|
||||
"Saved at": "Salvato in",
|
||||
"Scrobble when": "Scrobbla quando",
|
||||
"Search": "Cerca",
|
||||
"Search Everywhere": "Cerca ovunque",
|
||||
"Search headphones...": "Cerca cuffie...",
|
||||
"Search page": "Cerca nella pagina",
|
||||
"Search playlists or new playlist name": "Cerca tra le playlist o creane una nuova",
|
||||
"Select Library": "Seleziona libreria",
|
||||
@@ -340,4 +383,4 @@
|
||||
"one": "{{.trackCount}} traccia",
|
||||
"other": "{{.trackCount}} tracce"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@
|
||||
"Aug": "8月",
|
||||
"Authentication failed": "認証に失敗しました",
|
||||
"Auto": "自動",
|
||||
"AutoEQ": "AutoEQ",
|
||||
"Automatically check for updates": "アップデートを自動的にチェックする",
|
||||
"Autoplay": "自動再生",
|
||||
"Autoselect device": "デバイスを自動選択",
|
||||
@@ -46,10 +47,15 @@
|
||||
"Bit rate": "ビットレート",
|
||||
"Bold font": "太字フォント",
|
||||
"Broadcast": "ブロードキャスト",
|
||||
"Browse Headphone Profiles": "Browse Headphone Profiles",
|
||||
"Cancel": "取消",
|
||||
"Cannot Delete": "Cannot Delete",
|
||||
"Cannot delete builtin presets": "Cannot delete builtin presets",
|
||||
"Cannot use the name of a builtin preset": "Cannot use the name of a builtin preset",
|
||||
"Cast to device": "デバイスにキャスト",
|
||||
"Channels": "チャンネル",
|
||||
"Check for Updates": "アップデートの確認",
|
||||
"Check network connection and try again": "Check network connection and try again",
|
||||
"Clear caches": "キャッシュクリア",
|
||||
"Close": "閉じる",
|
||||
"Close to system tray": "システムトレイに閉じる",
|
||||
@@ -70,7 +76,10 @@
|
||||
"DJ-Mix": "DJ-Mix",
|
||||
"Date added": "追加日時",
|
||||
"Dec": "12月",
|
||||
"Delete": "Delete",
|
||||
"Delete Playlist": "プレイリストの削除",
|
||||
"Delete Preset": "Delete Preset",
|
||||
"Delete preset '%s'?": "Delete preset '%s'?",
|
||||
"Demo": "デモ",
|
||||
"Disable automatic DPI adjustment": "自動DPI調整を無効にする",
|
||||
"Disable server transcoding": "サーバーのトランスコーディングを無効にする",
|
||||
@@ -81,6 +90,22 @@
|
||||
"Duration": "演奏時間",
|
||||
"EP": "EP",
|
||||
"EPs": "EPs",
|
||||
"EQ Acoustic": "Acoustic",
|
||||
"EQ Bass Boost": "Bass Boost",
|
||||
"EQ Classical": "Classical",
|
||||
"EQ Electronic": "Electronic",
|
||||
"EQ Flat": "Flat",
|
||||
"EQ Jazz": "Jazz",
|
||||
"EQ Loudness": "Loudness",
|
||||
"EQ Pop": "Pop",
|
||||
"EQ Preamp": "Pre",
|
||||
"EQ Preset": "Preset",
|
||||
"EQ Preset:": "Preset:",
|
||||
"EQ R\u0026B": "R\u0026B",
|
||||
"EQ Rock": "Rock",
|
||||
"EQ Treble Boost": "Treble Boost",
|
||||
"EQ Type:": "EQ Type:",
|
||||
"EQ Vocal": "Vocal",
|
||||
"Edit": "編集",
|
||||
"Edit Playlist": "プレイリストを編集",
|
||||
"Edit server": "サーバーを編集",
|
||||
@@ -92,9 +117,11 @@
|
||||
"Equalizer": "イコライザー",
|
||||
"Error": "エラー",
|
||||
"Error creating playlist": "プレイリストの作成中にエラーが発生しました",
|
||||
"Error loading AutoEQ profiles": "Error loading AutoEQ profiles",
|
||||
"Error updating playlist": "Error updating playlist",
|
||||
"Exclusive mode": "排他モード",
|
||||
"Fade out on pause": "Fade out on pause",
|
||||
"Failed to load profile": "Failed to load profile",
|
||||
"Fav.": "お気に入り",
|
||||
"Favorites": "お気に入り",
|
||||
"Feb": "2月",
|
||||
@@ -118,6 +145,7 @@
|
||||
"In order": "順番",
|
||||
"Internet Radio Stations": "インターネットラジオ局",
|
||||
"Interview": "インタビュー",
|
||||
"Invalid Name": "Invalid Name",
|
||||
"Is favorite": "お気に入り",
|
||||
"Is not favorite": "お気に入りでない",
|
||||
"Jan": "1月",
|
||||
@@ -142,9 +170,11 @@
|
||||
"My Server": "自分のサーバー",
|
||||
"Name": "名前",
|
||||
"Name (A-Z)": "名前 (A-Z)",
|
||||
"Network error. Check connection.": "Network error. Check connection.",
|
||||
"New Playlist": "新しいプレイリスト",
|
||||
"Next": "次",
|
||||
"Nickname": "ニックネーム",
|
||||
"No Preset Selected": "No Preset Selected",
|
||||
"No new version found": "新しいバージョンが見つかりません",
|
||||
"No radio stations available": "利用できるラジオ局はありません",
|
||||
"None": "なし",
|
||||
@@ -154,6 +184,7 @@
|
||||
"Now Playing": "再生中",
|
||||
"OK": "OK",
|
||||
"Oct": "10月",
|
||||
"Overwrite Preset": "Overwrite Preset",
|
||||
"Owner": "所有者",
|
||||
"Password": "パスワード",
|
||||
"Pause": "一時停止",
|
||||
@@ -174,10 +205,15 @@
|
||||
"Playlist": "プレイリスト",
|
||||
"Playlists": "プレイリスト",
|
||||
"Plays": "再生",
|
||||
"Please select a preset to delete": "Please select a preset to delete",
|
||||
"Preset '%s' already exists. Overwrite?": "Preset '%s' already exists. Overwrite?",
|
||||
"Preset name": "Preset name",
|
||||
"Prevent clipping": "クリッピングを防ぐ",
|
||||
"Prevent screensaver on Now Playing page": "再生中ページでスクリーンセーバーを無効にする",
|
||||
"Previous": "前",
|
||||
"Private playlist by": "プライベートプレイリスト",
|
||||
"Profile": "Profile",
|
||||
"Profile not found": "Profile not found",
|
||||
"Public": "Public",
|
||||
"Public playlist by": "公開プレイリスト",
|
||||
"Quit": "終了",
|
||||
@@ -194,13 +230,19 @@
|
||||
"ReplayGain mode": "リプレイゲインモード",
|
||||
"ReplayGain preamp": "リプレイゲイン プリアンプ",
|
||||
"Rescan Library": "ライブラリを再スキャン",
|
||||
"Reset": "Reset",
|
||||
"Restart required": "再起動が必要です",
|
||||
"Sample rate": "サンプルレート",
|
||||
"Save": "Save",
|
||||
"Save As": "Save As",
|
||||
"Save Preset": "Save Preset",
|
||||
"Save Preset As": "Save Preset As",
|
||||
"Save play queue": "再生キューを保存",
|
||||
"Saved at": "保存場所",
|
||||
"Scrobble when": "スクロールすると",
|
||||
"Search": "検索",
|
||||
"Search Everywhere": "全体から検索",
|
||||
"Search headphones...": "Search headphones...",
|
||||
"Search page": "検索ページ",
|
||||
"Search playlists or new playlist name": "プレイリストを検索または新しいプレイリスト名",
|
||||
"Select Library": "ライブラリを選択",
|
||||
@@ -340,4 +382,4 @@
|
||||
"one": "{{.trackCount}}トラック",
|
||||
"other": "{{.trackCount}}トラック"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@
|
||||
"Aug": "8월",
|
||||
"Authentication failed": "인증 실패",
|
||||
"Auto": "자동",
|
||||
"AutoEQ": "AutoEQ",
|
||||
"Automatically check for updates": "자동으로 업데이트 확인",
|
||||
"Autoplay": "자동 재생",
|
||||
"Autoselect device": "자동 선택 장치",
|
||||
@@ -46,10 +47,15 @@
|
||||
"Bit rate": "비트레이트",
|
||||
"Bold font": "굵은 글꼴",
|
||||
"Broadcast": "방송",
|
||||
"Browse Headphone Profiles": "Browse Headphone Profiles",
|
||||
"Cancel": "취소",
|
||||
"Cannot Delete": "Cannot Delete",
|
||||
"Cannot delete builtin presets": "Cannot delete builtin presets",
|
||||
"Cannot use the name of a builtin preset": "Cannot use the name of a builtin preset",
|
||||
"Cast to device": "장치로 캐스트",
|
||||
"Channels": "Channels",
|
||||
"Check for Updates": "업데이트 확인",
|
||||
"Check network connection and try again": "Check network connection and try again",
|
||||
"Clear caches": "Clear caches",
|
||||
"Close": "닫기",
|
||||
"Close to system tray": "시스템 트레이로 최소화",
|
||||
@@ -70,7 +76,10 @@
|
||||
"DJ-Mix": "DJ-믹스",
|
||||
"Date added": "Date added",
|
||||
"Dec": "12월",
|
||||
"Delete": "Delete",
|
||||
"Delete Playlist": "재생 목록 삭제",
|
||||
"Delete Preset": "Delete Preset",
|
||||
"Delete preset '%s'?": "Delete preset '%s'?",
|
||||
"Demo": "데모",
|
||||
"Disable automatic DPI adjustment": "자동 DPI 조정 비활성화",
|
||||
"Disable server transcoding": "서버 트랜스코딩 비활성화",
|
||||
@@ -81,6 +90,22 @@
|
||||
"Duration": "기간",
|
||||
"EP": "EP",
|
||||
"EPs": "EPs",
|
||||
"EQ Acoustic": "Acoustic",
|
||||
"EQ Bass Boost": "Bass Boost",
|
||||
"EQ Classical": "Classical",
|
||||
"EQ Electronic": "Electronic",
|
||||
"EQ Flat": "Flat",
|
||||
"EQ Jazz": "Jazz",
|
||||
"EQ Loudness": "Loudness",
|
||||
"EQ Pop": "Pop",
|
||||
"EQ Preamp": "Pre",
|
||||
"EQ Preset": "Preset",
|
||||
"EQ Preset:": "Preset:",
|
||||
"EQ R\u0026B": "R\u0026B",
|
||||
"EQ Rock": "Rock",
|
||||
"EQ Treble Boost": "Treble Boost",
|
||||
"EQ Type:": "EQ Type:",
|
||||
"EQ Vocal": "Vocal",
|
||||
"Edit": "편집",
|
||||
"Edit Playlist": "재생 목록 편집",
|
||||
"Edit server": "서버 편집",
|
||||
@@ -92,9 +117,11 @@
|
||||
"Equalizer": "이퀄라이저",
|
||||
"Error": "오류",
|
||||
"Error creating playlist": "Error creating playlist",
|
||||
"Error loading AutoEQ profiles": "Error loading AutoEQ profiles",
|
||||
"Error updating playlist": "Error updating playlist",
|
||||
"Exclusive mode": "독점 모드",
|
||||
"Fade out on pause": "Fade out on pause",
|
||||
"Failed to load profile": "Failed to load profile",
|
||||
"Fav.": "즐겨찾기",
|
||||
"Favorites": "즐겨찾기",
|
||||
"Feb": "2월",
|
||||
@@ -118,6 +145,7 @@
|
||||
"In order": "In order",
|
||||
"Internet Radio Stations": "인터넷 라디오 방송국",
|
||||
"Interview": "인터뷰",
|
||||
"Invalid Name": "Invalid Name",
|
||||
"Is favorite": "즐겨찾기에 속한",
|
||||
"Is not favorite": "즐겨찾기에 속하지 않은",
|
||||
"Jan": "1월",
|
||||
@@ -142,9 +170,11 @@
|
||||
"My Server": "내 서버",
|
||||
"Name": "이름",
|
||||
"Name (A-Z)": "이름 (A-Z)",
|
||||
"Network error. Check connection.": "Network error. Check connection.",
|
||||
"New Playlist": "New Playlist",
|
||||
"Next": "다음",
|
||||
"Nickname": "별명",
|
||||
"No Preset Selected": "No Preset Selected",
|
||||
"No new version found": "새 버전을 찾을 수 없습니다",
|
||||
"No radio stations available": "라디오 방송국이 없습니다",
|
||||
"None": "없음",
|
||||
@@ -154,6 +184,7 @@
|
||||
"Now Playing": "현재 재생 중",
|
||||
"OK": "예",
|
||||
"Oct": "10월",
|
||||
"Overwrite Preset": "Overwrite Preset",
|
||||
"Owner": "소유자",
|
||||
"Password": "비밀번호",
|
||||
"Pause": "일시정지",
|
||||
@@ -174,10 +205,15 @@
|
||||
"Playlist": "재생 목록",
|
||||
"Playlists": "재생 목록",
|
||||
"Plays": "재생",
|
||||
"Please select a preset to delete": "Please select a preset to delete",
|
||||
"Preset '%s' already exists. Overwrite?": "Preset '%s' already exists. Overwrite?",
|
||||
"Preset name": "Preset name",
|
||||
"Prevent clipping": "클리핑 방지",
|
||||
"Prevent screensaver on Now Playing page": "Prevent screensaver on Now Playing page",
|
||||
"Previous": "이전",
|
||||
"Private playlist by": "비공개 재생목록",
|
||||
"Profile": "Profile",
|
||||
"Profile not found": "Profile not found",
|
||||
"Public": "Public",
|
||||
"Public playlist by": "공개 재생 목록",
|
||||
"Quit": "종료",
|
||||
@@ -194,14 +230,20 @@
|
||||
"ReplayGain mode": "리플레이게인 모드",
|
||||
"ReplayGain preamp": "리플레이게인 프리앰프",
|
||||
"Rescan Library": "라이브러리 재스캔",
|
||||
"Reset": "Reset",
|
||||
"Restart required": "재시작 필요",
|
||||
"Sample rate": "Sample rate",
|
||||
"Save": "Save",
|
||||
"Save As": "Save As",
|
||||
"Save Preset": "Save Preset",
|
||||
"Save Preset As": "Save Preset As",
|
||||
"Save play queue": "Save play queue",
|
||||
"Save play queuet": "종료 시 재생 대기열 저장",
|
||||
"Saved at": "저장됨",
|
||||
"Scrobble when": "스크로블할 시점",
|
||||
"Search": "검색",
|
||||
"Search Everywhere": "전체 검색",
|
||||
"Search headphones...": "Search headphones...",
|
||||
"Search page": "검색 페이지",
|
||||
"Search playlists or new playlist name": "재생 목록이나 새 재생 목록 이름 검색",
|
||||
"Select Library": "Select Library",
|
||||
@@ -317,4 +359,4 @@
|
||||
"x_minutes_ago": {
|
||||
"other": "59 minutes ago"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@
|
||||
"Aug": "Aug",
|
||||
"Authentication failed": "Authenticatie mislukt",
|
||||
"Auto": "Auto",
|
||||
"AutoEQ": "AutoEQ",
|
||||
"Automatically check for updates": "Automatisch controleren op updates",
|
||||
"Autoplay": "Automatisch afspelen",
|
||||
"Autoselect device": "Automatisch apparaat selecteren",
|
||||
@@ -46,10 +47,15 @@
|
||||
"Bit rate": "Bit rate",
|
||||
"Bold font": "Bold font",
|
||||
"Broadcast": "Broadcast",
|
||||
"Browse Headphone Profiles": "Browse Headphone Profiles",
|
||||
"Cancel": "Annuleren",
|
||||
"Cannot Delete": "Cannot Delete",
|
||||
"Cannot delete builtin presets": "Cannot delete builtin presets",
|
||||
"Cannot use the name of a builtin preset": "Cannot use the name of a builtin preset",
|
||||
"Cast to device": "Cast naar apparaat",
|
||||
"Channels": "Channels",
|
||||
"Check for Updates": "Controleer op updates",
|
||||
"Check network connection and try again": "Check network connection and try again",
|
||||
"Clear caches": "Clear caches",
|
||||
"Close": "Sluiten",
|
||||
"Close to system tray": "Sluiten naar systeemvak",
|
||||
@@ -70,7 +76,10 @@
|
||||
"DJ-Mix": "DJ-Mix",
|
||||
"Date added": "Date added",
|
||||
"Dec": "Dec",
|
||||
"Delete": "Delete",
|
||||
"Delete Playlist": "Wis afspeellijst",
|
||||
"Delete Preset": "Delete Preset",
|
||||
"Delete preset '%s'?": "Delete preset '%s'?",
|
||||
"Demo": "Demo",
|
||||
"Disable automatic DPI adjustment": "Automatische DPI-aanpassing uitschakelen",
|
||||
"Disable server transcoding": "Schakel server transcodering uit",
|
||||
@@ -81,6 +90,22 @@
|
||||
"Duration": "Duur",
|
||||
"EP": "EP",
|
||||
"EPs": "EPs",
|
||||
"EQ Acoustic": "Acoustic",
|
||||
"EQ Bass Boost": "Bass Boost",
|
||||
"EQ Classical": "Classical",
|
||||
"EQ Electronic": "Electronic",
|
||||
"EQ Flat": "Flat",
|
||||
"EQ Jazz": "Jazz",
|
||||
"EQ Loudness": "Loudness",
|
||||
"EQ Pop": "Pop",
|
||||
"EQ Preamp": "Pre",
|
||||
"EQ Preset": "Preset",
|
||||
"EQ Preset:": "Preset:",
|
||||
"EQ R\u0026B": "R\u0026B",
|
||||
"EQ Rock": "Rock",
|
||||
"EQ Treble Boost": "Treble Boost",
|
||||
"EQ Type:": "EQ Type:",
|
||||
"EQ Vocal": "Vocal",
|
||||
"Edit": "Bewerken",
|
||||
"Edit Playlist": "Bewerk afspeellijst",
|
||||
"Edit server": "Bewerk server",
|
||||
@@ -92,9 +117,11 @@
|
||||
"Equalizer": "Equalizer",
|
||||
"Error": "Fout",
|
||||
"Error creating playlist": "Error creating playlist",
|
||||
"Error loading AutoEQ profiles": "Error loading AutoEQ profiles",
|
||||
"Error updating playlist": "Error updating playlist",
|
||||
"Exclusive mode": "Exclusieve modus",
|
||||
"Fade out on pause": "Fade out on pause",
|
||||
"Failed to load profile": "Failed to load profile",
|
||||
"Fav.": "Fav.",
|
||||
"Favorites": "Favorieten",
|
||||
"Feb": "Feb",
|
||||
@@ -118,6 +145,7 @@
|
||||
"In order": "In order",
|
||||
"Internet Radio Stations": "Internet Radio Stations",
|
||||
"Interview": "Interview",
|
||||
"Invalid Name": "Invalid Name",
|
||||
"Is favorite": "Is favoriet",
|
||||
"Is not favorite": "Is niet favoriet",
|
||||
"Jan": "Jan",
|
||||
@@ -142,9 +170,11 @@
|
||||
"My Server": "Mijn server",
|
||||
"Name": "Naam",
|
||||
"Name (A-Z)": "Naam (A-Z)",
|
||||
"Network error. Check connection.": "Network error. Check connection.",
|
||||
"New Playlist": "New Playlist",
|
||||
"Next": "Volgende",
|
||||
"Nickname": "Nickname",
|
||||
"No Preset Selected": "No Preset Selected",
|
||||
"No new version found": "Geen nieuwe versie gevonden",
|
||||
"No radio stations available": "Geen radio stations beschikbaar",
|
||||
"None": "Geen",
|
||||
@@ -154,6 +184,7 @@
|
||||
"Now Playing": "Nu aan het afspelen",
|
||||
"OK": "OK",
|
||||
"Oct": "Okt",
|
||||
"Overwrite Preset": "Overwrite Preset",
|
||||
"Owner": "Eigenaar",
|
||||
"Password": "Wachtwoord",
|
||||
"Pause": "Pauzeer",
|
||||
@@ -174,10 +205,15 @@
|
||||
"Playlist": "Afspeellijst",
|
||||
"Playlists": "Afspeellijsten",
|
||||
"Plays": "Afgespeeld",
|
||||
"Please select a preset to delete": "Please select a preset to delete",
|
||||
"Preset '%s' already exists. Overwrite?": "Preset '%s' already exists. Overwrite?",
|
||||
"Preset name": "Preset name",
|
||||
"Prevent clipping": "Voorkom clipping",
|
||||
"Prevent screensaver on Now Playing page": "Prevent screensaver on Now Playing page",
|
||||
"Previous": "Vorige",
|
||||
"Private playlist by": "Private afspeellijst door",
|
||||
"Profile": "Profile",
|
||||
"Profile not found": "Profile not found",
|
||||
"Public": "Public",
|
||||
"Public playlist by": "Publieke afspeellijst door",
|
||||
"Quit": "Beëindigen",
|
||||
@@ -194,13 +230,19 @@
|
||||
"ReplayGain mode": "ReplayGain mode",
|
||||
"ReplayGain preamp": "ReplayGain preamp",
|
||||
"Rescan Library": "Bibliotheek opnieuw scannen",
|
||||
"Reset": "Reset",
|
||||
"Restart required": "Herstart vereist",
|
||||
"Sample rate": "Sample rate",
|
||||
"Save": "Save",
|
||||
"Save As": "Save As",
|
||||
"Save Preset": "Save Preset",
|
||||
"Save Preset As": "Save Preset As",
|
||||
"Save play queue": "Bewaar wachtrij bij afsluiten",
|
||||
"Saved at": "Opgeslaan op",
|
||||
"Scrobble when": "Scrobble als",
|
||||
"Search": "Zoeken",
|
||||
"Search Everywhere": "Overal zoeken",
|
||||
"Search headphones...": "Search headphones...",
|
||||
"Search page": "Zoekpagina",
|
||||
"Search playlists or new playlist name": "Zoek afspeellijsten of nieuwe afspeellijst naam",
|
||||
"Select Library": "Select Library",
|
||||
@@ -315,4 +357,4 @@
|
||||
"x_minutes_ago": {
|
||||
"other": "59 minutes ago"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@
|
||||
"Audiobook": "Audiobook",
|
||||
"Authentication failed": "Błąd autoryzacji",
|
||||
"Auto": "Auto",
|
||||
"AutoEQ": "AutoEQ",
|
||||
"Automatically check for updates": "Automatycznie sprawdzaj aktualizacje",
|
||||
"Autoplay": "Auto odtwarzanie",
|
||||
"Autoselect device": "Automatyczny wybór",
|
||||
@@ -43,10 +44,15 @@
|
||||
"Bit rate": "Bit rate",
|
||||
"Bold font": "Bold font",
|
||||
"Broadcast": "Broadcast",
|
||||
"Browse Headphone Profiles": "Browse Headphone Profiles",
|
||||
"Cancel": "Anuluj",
|
||||
"Cannot Delete": "Cannot Delete",
|
||||
"Cannot delete builtin presets": "Cannot delete builtin presets",
|
||||
"Cannot use the name of a builtin preset": "Cannot use the name of a builtin preset",
|
||||
"Cast to device": "Cast to device",
|
||||
"Channels": "Channels",
|
||||
"Check for Updates": "Sprawdź aktualizacje",
|
||||
"Check network connection and try again": "Check network connection and try again",
|
||||
"Clear caches": "Clear caches",
|
||||
"Close": "Zamknij",
|
||||
"Close to system tray": "Zamknij do zasobnika systemowego",
|
||||
@@ -66,7 +72,10 @@
|
||||
"Create new playlist": "Utwórz nową playlistę",
|
||||
"DJ-Mix": "DJ-Mix",
|
||||
"Date added": "Date added",
|
||||
"Delete": "Delete",
|
||||
"Delete Playlist": "Usuń playlistę",
|
||||
"Delete Preset": "Delete Preset",
|
||||
"Delete preset '%s'?": "Delete preset '%s'?",
|
||||
"Demo": "Demo",
|
||||
"Disable automatic DPI adjustment": "Wyłącz automatyczną zmianę DPI",
|
||||
"Disable server transcoding": "Wyłącz transkodowanie serwera",
|
||||
@@ -77,6 +86,22 @@
|
||||
"Duration": "Czas trwania",
|
||||
"EP": "EP",
|
||||
"EPs": "EPs",
|
||||
"EQ Acoustic": "Acoustic",
|
||||
"EQ Bass Boost": "Bass Boost",
|
||||
"EQ Classical": "Classical",
|
||||
"EQ Electronic": "Electronic",
|
||||
"EQ Flat": "Flat",
|
||||
"EQ Jazz": "Jazz",
|
||||
"EQ Loudness": "Loudness",
|
||||
"EQ Pop": "Pop",
|
||||
"EQ Preamp": "Pre",
|
||||
"EQ Preset": "Preset",
|
||||
"EQ Preset:": "Preset:",
|
||||
"EQ R\u0026B": "R\u0026B",
|
||||
"EQ Rock": "Rock",
|
||||
"EQ Treble Boost": "Treble Boost",
|
||||
"EQ Type:": "EQ Type:",
|
||||
"EQ Vocal": "Vocal",
|
||||
"Edit": "Edytuj",
|
||||
"Edit Playlist": "Edytuj playlistę",
|
||||
"Edit server": "Edytuj serwer",
|
||||
@@ -88,9 +113,11 @@
|
||||
"Equalizer": "Equalizer",
|
||||
"Error": "Error",
|
||||
"Error creating playlist": "Error creating playlist",
|
||||
"Error loading AutoEQ profiles": "Error loading AutoEQ profiles",
|
||||
"Error updating playlist": "Error updating playlist",
|
||||
"Exclusive mode": "Tryb exclusive",
|
||||
"Fade out on pause": "Fade out on pause",
|
||||
"Failed to load profile": "Failed to load profile",
|
||||
"Fav.": "Ulub.",
|
||||
"Favorites": "Ulubione",
|
||||
"Field Recording": "Field Recording",
|
||||
@@ -113,6 +140,7 @@
|
||||
"In order": "In order",
|
||||
"Internet Radio Stations": "Internetowe Stacje Radiowe",
|
||||
"Interview": "Wywiad",
|
||||
"Invalid Name": "Invalid Name",
|
||||
"Is favorite": "Jest ulubione",
|
||||
"Is not favorite": "Nie jest ulubione",
|
||||
"Language": "Język",
|
||||
@@ -132,9 +160,11 @@
|
||||
"My Server": "Mój serwer",
|
||||
"Name": "Nazwa",
|
||||
"Name (A-Z)": "Nazwa (A-Z)",
|
||||
"Network error. Check connection.": "Network error. Check connection.",
|
||||
"New Playlist": "New Playlist",
|
||||
"Next": "Następny",
|
||||
"Nickname": "Nazwa serwera",
|
||||
"No Preset Selected": "No Preset Selected",
|
||||
"No new version found": "Nie znaleziono nowych wersji",
|
||||
"No radio stations available": "Brak dostępnych radio stacji",
|
||||
"None": "Nic",
|
||||
@@ -142,6 +172,7 @@
|
||||
"Normal font": "Normal font",
|
||||
"Now Playing": "Teraz Odtwarzane",
|
||||
"OK": "OK",
|
||||
"Overwrite Preset": "Overwrite Preset",
|
||||
"Owner": "Właściciel",
|
||||
"Password": "Hasło",
|
||||
"Pause": "Pauza",
|
||||
@@ -162,10 +193,15 @@
|
||||
"Playlist": "Playlista",
|
||||
"Playlists": "Playlisty",
|
||||
"Plays": "Ilość odtworzeń",
|
||||
"Please select a preset to delete": "Please select a preset to delete",
|
||||
"Preset '%s' already exists. Overwrite?": "Preset '%s' already exists. Overwrite?",
|
||||
"Preset name": "Preset name",
|
||||
"Prevent clipping": "Zapobiegnij przycicnaniu",
|
||||
"Prevent screensaver on Now Playing page": "Prevent screensaver on Now Playing page",
|
||||
"Previous": "Poprzednie",
|
||||
"Private playlist by": "Prywatna playlista przez",
|
||||
"Profile": "Profile",
|
||||
"Profile not found": "Profile not found",
|
||||
"Public": "Public",
|
||||
"Public playlist by": "Publiczne playlista przez",
|
||||
"Quit": "Wyjdź",
|
||||
@@ -182,13 +218,19 @@
|
||||
"ReplayGain mode": "ReplayGain mode",
|
||||
"ReplayGain preamp": "ReplayGain preamp",
|
||||
"Rescan Library": "Przeszukaj ponownie bibliotekę",
|
||||
"Reset": "Reset",
|
||||
"Restart required": "Wymagany restart",
|
||||
"Sample rate": "Sample rate",
|
||||
"Save": "Save",
|
||||
"Save As": "Save As",
|
||||
"Save Preset": "Save Preset",
|
||||
"Save Preset As": "Save Preset As",
|
||||
"Save play queue": "Zapisz kolejkę odtwarzania po zamknięciu",
|
||||
"Saved at": "Zapisano w",
|
||||
"Scrobble when": "Wyślij gdy",
|
||||
"Search": "Szukaj",
|
||||
"Search Everywhere": "Szukaj wszędzie",
|
||||
"Search headphones...": "Search headphones...",
|
||||
"Search page": "Przeszukaj stronę",
|
||||
"Search playlists or new playlist name": "Szukaj playlisty lub",
|
||||
"Select Library": "Select Library",
|
||||
@@ -301,4 +343,4 @@
|
||||
"x_minutes_ago": {
|
||||
"other": "59 minutes ago"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@
|
||||
"Aug": "Ago",
|
||||
"Authentication failed": "A autenticação falhou",
|
||||
"Auto": "Auto",
|
||||
"AutoEQ": "AutoEQ",
|
||||
"Automatically check for updates": "Procurar atualizações automaticamente",
|
||||
"Autoplay": "Autoplay",
|
||||
"Autoselect device": "Selecionar dispositivo automaticamente",
|
||||
@@ -46,10 +47,15 @@
|
||||
"Bit rate": "Taxa de bits",
|
||||
"Bold font": "Fonte em negrito",
|
||||
"Broadcast": "Transmissão",
|
||||
"Browse Headphone Profiles": "Browse Headphone Profiles",
|
||||
"Cancel": "Cancelar",
|
||||
"Cannot Delete": "Cannot Delete",
|
||||
"Cannot delete builtin presets": "Cannot delete builtin presets",
|
||||
"Cannot use the name of a builtin preset": "Cannot use the name of a builtin preset",
|
||||
"Cast to device": "Cast to device",
|
||||
"Channels": "Channels",
|
||||
"Check for Updates": "Buscar atualizações",
|
||||
"Check network connection and try again": "Check network connection and try again",
|
||||
"Clear caches": "Clear caches",
|
||||
"Close": "Fechar",
|
||||
"Close to system tray": "Fechar para a bandeja do sistema",
|
||||
@@ -70,7 +76,10 @@
|
||||
"DJ-Mix": "DJ-Mix",
|
||||
"Date added": "Date added",
|
||||
"Dec": "Dez",
|
||||
"Delete": "Delete",
|
||||
"Delete Playlist": "Remover lista de reprodução",
|
||||
"Delete Preset": "Delete Preset",
|
||||
"Delete preset '%s'?": "Delete preset '%s'?",
|
||||
"Demo": "Demo",
|
||||
"Disable automatic DPI adjustment": "Desabilitar ajuste automático de DPI",
|
||||
"Disable server transcoding": "Desabilitar transcodificação no servidor",
|
||||
@@ -81,6 +90,22 @@
|
||||
"Duration": "Duração",
|
||||
"EP": "EP",
|
||||
"EPs": "EPs",
|
||||
"EQ Acoustic": "Acoustic",
|
||||
"EQ Bass Boost": "Bass Boost",
|
||||
"EQ Classical": "Classical",
|
||||
"EQ Electronic": "Electronic",
|
||||
"EQ Flat": "Flat",
|
||||
"EQ Jazz": "Jazz",
|
||||
"EQ Loudness": "Loudness",
|
||||
"EQ Pop": "Pop",
|
||||
"EQ Preamp": "Pre",
|
||||
"EQ Preset": "Preset",
|
||||
"EQ Preset:": "Preset:",
|
||||
"EQ R\u0026B": "R\u0026B",
|
||||
"EQ Rock": "Rock",
|
||||
"EQ Treble Boost": "Treble Boost",
|
||||
"EQ Type:": "EQ Type:",
|
||||
"EQ Vocal": "Vocal",
|
||||
"Edit": "Editar",
|
||||
"Edit Playlist": "Editar lista de reprodução",
|
||||
"Edit server": "Editar servidor",
|
||||
@@ -92,9 +117,11 @@
|
||||
"Equalizer": "Equalizador",
|
||||
"Error": "Erro",
|
||||
"Error creating playlist": "Error creating playlist",
|
||||
"Error loading AutoEQ profiles": "Error loading AutoEQ profiles",
|
||||
"Error updating playlist": "Error updating playlist",
|
||||
"Exclusive mode": "Modo exclusivo",
|
||||
"Fade out on pause": "Fade out on pause",
|
||||
"Failed to load profile": "Failed to load profile",
|
||||
"Fav.": "Fav.",
|
||||
"Favorites": "Favoritos",
|
||||
"Feb": "Fev",
|
||||
@@ -118,6 +145,7 @@
|
||||
"In order": "In order",
|
||||
"Internet Radio Stations": "Estacões de Rádio da Internet",
|
||||
"Interview": "Entrevista",
|
||||
"Invalid Name": "Invalid Name",
|
||||
"Is favorite": "É favorito",
|
||||
"Is not favorite": "Não é favorito",
|
||||
"Jan": "Jan",
|
||||
@@ -142,9 +170,11 @@
|
||||
"My Server": "Meu Servidor",
|
||||
"Name": "Nome",
|
||||
"Name (A-Z)": "Nome (A-Z)",
|
||||
"Network error. Check connection.": "Network error. Check connection.",
|
||||
"New Playlist": "New Playlist",
|
||||
"Next": "Próximo",
|
||||
"Nickname": "Apelido",
|
||||
"No Preset Selected": "No Preset Selected",
|
||||
"No new version found": "Nenhuma versão nova encontrada",
|
||||
"No radio stations available": "Nenhuma estação de rádio disponível",
|
||||
"None": "Nenhum",
|
||||
@@ -154,6 +184,7 @@
|
||||
"Now Playing": "Tocando agora",
|
||||
"OK": "OK",
|
||||
"Oct": "Out",
|
||||
"Overwrite Preset": "Overwrite Preset",
|
||||
"Owner": "Proprietário",
|
||||
"Password": "Senha",
|
||||
"Pause": "Pausar",
|
||||
@@ -174,10 +205,15 @@
|
||||
"Playlist": "Lista de reprodução",
|
||||
"Playlists": "Listas de reprodução",
|
||||
"Plays": "Reproduções",
|
||||
"Please select a preset to delete": "Please select a preset to delete",
|
||||
"Preset '%s' already exists. Overwrite?": "Preset '%s' already exists. Overwrite?",
|
||||
"Preset name": "Preset name",
|
||||
"Prevent clipping": "Evitar clipping",
|
||||
"Prevent screensaver on Now Playing page": "Prevent screensaver on Now Playing page",
|
||||
"Previous": "Anterior",
|
||||
"Private playlist by": "Lista de reprodução privada por",
|
||||
"Profile": "Profile",
|
||||
"Profile not found": "Profile not found",
|
||||
"Public": "Public",
|
||||
"Public playlist by": "Lista de reprodução pública por",
|
||||
"Quit": "Sair",
|
||||
@@ -194,13 +230,19 @@
|
||||
"ReplayGain mode": "Modo do ReplayGain",
|
||||
"ReplayGain preamp": "Pré-amp. do ReplayGain",
|
||||
"Rescan Library": "Reescanear biblioteca",
|
||||
"Reset": "Reset",
|
||||
"Restart required": "Requer reinício",
|
||||
"Sample rate": "Sample rate",
|
||||
"Save": "Save",
|
||||
"Save As": "Save As",
|
||||
"Save Preset": "Save Preset",
|
||||
"Save Preset As": "Save Preset As",
|
||||
"Save play queue": "Salvar fila de reprodução",
|
||||
"Saved at": "Salvo em",
|
||||
"Scrobble when": "Fazer scrobble quando",
|
||||
"Search": "Pesquisar",
|
||||
"Search Everywhere": "Pesquisar em todos os lugares",
|
||||
"Search headphones...": "Search headphones...",
|
||||
"Search page": "Pesquisar na página",
|
||||
"Search playlists or new playlist name": "Pesquisar listas de reprodução ou criar uma nova",
|
||||
"Select Library": "Select Library",
|
||||
@@ -314,4 +356,4 @@
|
||||
"x_minutes_ago": {
|
||||
"other": "59 minutes ago"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@
|
||||
"Aug": "Aug",
|
||||
"Authentication failed": "Autentificare eșuată",
|
||||
"Auto": "Auto",
|
||||
"AutoEQ": "AutoEQ",
|
||||
"Automatically check for updates": "Automatically check for updates",
|
||||
"Autoplay": "Autoplay",
|
||||
"Autoselect device": "Selectează automat dispozitiv",
|
||||
@@ -45,10 +46,15 @@
|
||||
"Bit rate": "Bit rate",
|
||||
"Bold font": "Bold font",
|
||||
"Broadcast": "Transmisiune",
|
||||
"Browse Headphone Profiles": "Browse Headphone Profiles",
|
||||
"Cancel": "Anulează",
|
||||
"Cannot Delete": "Cannot Delete",
|
||||
"Cannot delete builtin presets": "Cannot delete builtin presets",
|
||||
"Cannot use the name of a builtin preset": "Cannot use the name of a builtin preset",
|
||||
"Cast to device": "Cast to device",
|
||||
"Channels": "Channels",
|
||||
"Check for Updates": "Verifică actualizări",
|
||||
"Check network connection and try again": "Check network connection and try again",
|
||||
"Clear caches": "Clear caches",
|
||||
"Close": "Închide",
|
||||
"Close to system tray": "Închide în bara de sistem",
|
||||
@@ -69,7 +75,10 @@
|
||||
"DJ-Mix": "DJ-Mix",
|
||||
"Date added": "Date added",
|
||||
"Dec": "Dec",
|
||||
"Delete": "Delete",
|
||||
"Delete Playlist": "Șterge playlist",
|
||||
"Delete Preset": "Delete Preset",
|
||||
"Delete preset '%s'?": "Delete preset '%s'?",
|
||||
"Demo": "Demo",
|
||||
"Disable automatic DPI adjustment": "Disable automatic DPI adjustment",
|
||||
"Disable server transcoding": "Dezactivează transcodare pe server",
|
||||
@@ -80,6 +89,22 @@
|
||||
"Duration": "Durată",
|
||||
"EP": "EP",
|
||||
"EPs": "EPs",
|
||||
"EQ Acoustic": "Acoustic",
|
||||
"EQ Bass Boost": "Bass Boost",
|
||||
"EQ Classical": "Classical",
|
||||
"EQ Electronic": "Electronic",
|
||||
"EQ Flat": "Flat",
|
||||
"EQ Jazz": "Jazz",
|
||||
"EQ Loudness": "Loudness",
|
||||
"EQ Pop": "Pop",
|
||||
"EQ Preamp": "Pre",
|
||||
"EQ Preset": "Preset",
|
||||
"EQ Preset:": "Preset:",
|
||||
"EQ R\u0026B": "R\u0026B",
|
||||
"EQ Rock": "Rock",
|
||||
"EQ Treble Boost": "Treble Boost",
|
||||
"EQ Type:": "EQ Type:",
|
||||
"EQ Vocal": "Vocal",
|
||||
"Edit": "Editare",
|
||||
"Edit Playlist": "Editează playlist",
|
||||
"Edit server": "Editează server",
|
||||
@@ -91,9 +116,11 @@
|
||||
"Equalizer": "Egalizator",
|
||||
"Error": "Error",
|
||||
"Error creating playlist": "Error creating playlist",
|
||||
"Error loading AutoEQ profiles": "Error loading AutoEQ profiles",
|
||||
"Error updating playlist": "Error updating playlist",
|
||||
"Exclusive mode": "Mod exclusiv",
|
||||
"Fade out on pause": "Fade out on pause",
|
||||
"Failed to load profile": "Failed to load profile",
|
||||
"Fav.": "Fav.",
|
||||
"Favorites": "Favorite",
|
||||
"Feb": "Feb",
|
||||
@@ -117,6 +144,7 @@
|
||||
"In order": "In order",
|
||||
"Internet Radio Stations": "Stații radio pe internet",
|
||||
"Interview": "Interviu",
|
||||
"Invalid Name": "Invalid Name",
|
||||
"Is favorite": "Este favorit",
|
||||
"Is not favorite": "Nu este favorit",
|
||||
"Jan": "Ian",
|
||||
@@ -141,9 +169,11 @@
|
||||
"My Server": "Serverul meu",
|
||||
"Name": "Nume",
|
||||
"Name (A-Z)": "Nume (A-Z)",
|
||||
"Network error. Check connection.": "Network error. Check connection.",
|
||||
"New Playlist": "New Playlist",
|
||||
"Next": "Următorul",
|
||||
"Nickname": "Supranume",
|
||||
"No Preset Selected": "No Preset Selected",
|
||||
"No new version found": "Nicio versiune nouă",
|
||||
"No radio stations available": "Nicio stație radio disponibilă",
|
||||
"None": "Nimic",
|
||||
@@ -152,6 +182,7 @@
|
||||
"Nov": "Noiem",
|
||||
"OK": "OK",
|
||||
"Oct": "Oct",
|
||||
"Overwrite Preset": "Overwrite Preset",
|
||||
"Owner": "Deținător",
|
||||
"Password": "Parolă",
|
||||
"Pause": "Pauză",
|
||||
@@ -172,10 +203,15 @@
|
||||
"Playlist": "Playlist",
|
||||
"Playlists": "Playlist-uri",
|
||||
"Plays": "Redări",
|
||||
"Please select a preset to delete": "Please select a preset to delete",
|
||||
"Preset '%s' already exists. Overwrite?": "Preset '%s' already exists. Overwrite?",
|
||||
"Preset name": "Preset name",
|
||||
"Prevent clipping": "Previne distorsiunea",
|
||||
"Prevent screensaver on Now Playing page": "Prevent screensaver on Now Playing page",
|
||||
"Previous": "Anterior",
|
||||
"Private playlist by": "Playlist privat de",
|
||||
"Profile": "Profile",
|
||||
"Profile not found": "Profile not found",
|
||||
"Public": "Public",
|
||||
"Public playlist by": "Playlist public de",
|
||||
"Quit": "Ieșire",
|
||||
@@ -192,13 +228,19 @@
|
||||
"ReplayGain mode": "Mod ReplayGain",
|
||||
"ReplayGain preamp": "Preamplificare ReplayGain",
|
||||
"Rescan Library": "Rescanează librăria",
|
||||
"Reset": "Reset",
|
||||
"Restart required": "Repornire necesară",
|
||||
"Sample rate": "Sample rate",
|
||||
"Save": "Save",
|
||||
"Save As": "Save As",
|
||||
"Save Preset": "Save Preset",
|
||||
"Save Preset As": "Save Preset As",
|
||||
"Save play queue": "Salvează listă de redare",
|
||||
"Saved at": "Salvat la",
|
||||
"Scrobble when": "Scrobblare când",
|
||||
"Search": "Caută",
|
||||
"Search Everywhere": "Caută peste tot",
|
||||
"Search headphones...": "Search headphones...",
|
||||
"Search page": "Caută pagină",
|
||||
"Search playlists or new playlist name": "Caută playlisturi sau nume de playlist nou",
|
||||
"Select Library": "Select Library",
|
||||
@@ -312,4 +354,4 @@
|
||||
"x_minutes_ago": {
|
||||
"other": "59 minutes ago"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@
|
||||
"Aug": "Авг",
|
||||
"Authentication failed": "Ошибка аутентикации",
|
||||
"Auto": "Автоматически",
|
||||
"AutoEQ": "AutoEQ",
|
||||
"Automatically check for updates": "Проверять обновления автоматически",
|
||||
"Autoplay": "Автоматически",
|
||||
"Autoselect device": "Автоматически",
|
||||
@@ -46,10 +47,15 @@
|
||||
"Bit rate": "Битрейт",
|
||||
"Bold font": "Полужирный",
|
||||
"Broadcast": "Радиопередача",
|
||||
"Browse Headphone Profiles": "Browse Headphone Profiles",
|
||||
"Cancel": "Отменить",
|
||||
"Cannot Delete": "Cannot Delete",
|
||||
"Cannot delete builtin presets": "Cannot delete builtin presets",
|
||||
"Cannot use the name of a builtin preset": "Cannot use the name of a builtin preset",
|
||||
"Cast to device": "Отправить на устройство",
|
||||
"Channels": "Channels",
|
||||
"Check for Updates": "Проверить обновления",
|
||||
"Check network connection and try again": "Check network connection and try again",
|
||||
"Clear caches": "Clear caches",
|
||||
"Close": "Закрыть",
|
||||
"Close to system tray": "Закрывать в область уведомлений",
|
||||
@@ -70,7 +76,10 @@
|
||||
"DJ-Mix": "DJ-микс",
|
||||
"Date added": "Date added",
|
||||
"Dec": "Дек",
|
||||
"Delete": "Delete",
|
||||
"Delete Playlist": "Удалить",
|
||||
"Delete Preset": "Delete Preset",
|
||||
"Delete preset '%s'?": "Delete preset '%s'?",
|
||||
"Demo": "Демозапись",
|
||||
"Disable automatic DPI adjustment": "Отключить автоматическую подгонку DPI",
|
||||
"Disable server transcoding": "Отключить перекодирование на стороне сервера",
|
||||
@@ -81,6 +90,22 @@
|
||||
"Duration": "Длительность",
|
||||
"EP": "EP",
|
||||
"EPs": "EPs",
|
||||
"EQ Acoustic": "Acoustic",
|
||||
"EQ Bass Boost": "Bass Boost",
|
||||
"EQ Classical": "Classical",
|
||||
"EQ Electronic": "Electronic",
|
||||
"EQ Flat": "Flat",
|
||||
"EQ Jazz": "Jazz",
|
||||
"EQ Loudness": "Loudness",
|
||||
"EQ Pop": "Pop",
|
||||
"EQ Preamp": "Pre",
|
||||
"EQ Preset": "Preset",
|
||||
"EQ Preset:": "Preset:",
|
||||
"EQ R\u0026B": "R\u0026B",
|
||||
"EQ Rock": "Rock",
|
||||
"EQ Treble Boost": "Treble Boost",
|
||||
"EQ Type:": "EQ Type:",
|
||||
"EQ Vocal": "Vocal",
|
||||
"Edit": "Изменить",
|
||||
"Edit Playlist": "Изменить список воспроизведения",
|
||||
"Edit server": "Изменить сервер",
|
||||
@@ -92,9 +117,11 @@
|
||||
"Equalizer": "Эквалайзер",
|
||||
"Error": "Ошибка",
|
||||
"Error creating playlist": "Error creating playlist",
|
||||
"Error loading AutoEQ profiles": "Error loading AutoEQ profiles",
|
||||
"Error updating playlist": "Error updating playlist",
|
||||
"Exclusive mode": "Исключительное использование",
|
||||
"Fade out on pause": "Fade out on pause",
|
||||
"Failed to load profile": "Failed to load profile",
|
||||
"Fav.": "Избр.",
|
||||
"Favorites": "Избранное",
|
||||
"Feb": "Фев",
|
||||
@@ -118,6 +145,7 @@
|
||||
"In order": "In order",
|
||||
"Internet Radio Stations": "Интернет-радио",
|
||||
"Interview": "Интервью",
|
||||
"Invalid Name": "Invalid Name",
|
||||
"Is favorite": "В избранном",
|
||||
"Is not favorite": "Не в избранном",
|
||||
"Jan": "Янв",
|
||||
@@ -142,9 +170,11 @@
|
||||
"My Server": "Мой сервер",
|
||||
"Name": "Название",
|
||||
"Name (A-Z)": "По названию (A-Z)",
|
||||
"Network error. Check connection.": "Network error. Check connection.",
|
||||
"New Playlist": "New Playlist",
|
||||
"Next": "Следующий",
|
||||
"Nickname": "Название сервера",
|
||||
"No Preset Selected": "No Preset Selected",
|
||||
"No new version found": "Нет новой версии",
|
||||
"No radio stations available": "Радиостанции не доступны",
|
||||
"None": "Нет",
|
||||
@@ -154,6 +184,7 @@
|
||||
"Now Playing": "Играет сейчас",
|
||||
"OK": "OK",
|
||||
"Oct": "Окт",
|
||||
"Overwrite Preset": "Overwrite Preset",
|
||||
"Owner": "Составитель",
|
||||
"Password": "Пароль",
|
||||
"Pause": "Остановить",
|
||||
@@ -174,10 +205,15 @@
|
||||
"Playlist": "Список воспроизведения",
|
||||
"Playlists": "Списки воспроизведения",
|
||||
"Plays": "Прослушивания",
|
||||
"Please select a preset to delete": "Please select a preset to delete",
|
||||
"Preset '%s' already exists. Overwrite?": "Preset '%s' already exists. Overwrite?",
|
||||
"Preset name": "Preset name",
|
||||
"Prevent clipping": "Защита от перегрузки",
|
||||
"Prevent screensaver on Now Playing page": "Prevent screensaver on Now Playing page",
|
||||
"Previous": "Предыдущий",
|
||||
"Private playlist by": "Приватный список вопроизведения",
|
||||
"Profile": "Profile",
|
||||
"Profile not found": "Profile not found",
|
||||
"Public": "Public",
|
||||
"Public playlist by": "Публичный список воспроизведения",
|
||||
"Quit": "Выйти",
|
||||
@@ -194,13 +230,19 @@
|
||||
"ReplayGain mode": "Режим",
|
||||
"ReplayGain preamp": "Предусиление",
|
||||
"Rescan Library": "Перечитать библиотеку",
|
||||
"Reset": "Reset",
|
||||
"Restart required": "Требуется перезапуск",
|
||||
"Sample rate": "Sample rate",
|
||||
"Save": "Save",
|
||||
"Save As": "Save As",
|
||||
"Save Preset": "Save Preset",
|
||||
"Save Preset As": "Save Preset As",
|
||||
"Save play queue": "Сохранять очередь воспроизведения при выходе",
|
||||
"Saved at": "Сохранено в",
|
||||
"Scrobble when": "Отправлять когда",
|
||||
"Search": "Искать",
|
||||
"Search Everywhere": "Искать везде",
|
||||
"Search headphones...": "Search headphones...",
|
||||
"Search page": "Искать на странице",
|
||||
"Search playlists or new playlist name": "Найти или создать новый",
|
||||
"Select Library": "Select Library",
|
||||
@@ -329,4 +371,4 @@
|
||||
"one": "{{.trackCount}} композиция",
|
||||
"other": "{{.trackCount}} композиций"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@
|
||||
"Aug": "八月",
|
||||
"Authentication failed": "认证失败",
|
||||
"Auto": "自动",
|
||||
"AutoEQ": "AutoEQ",
|
||||
"Automatically check for updates": "自动检查更新",
|
||||
"Autoplay": "自动播放",
|
||||
"Autoselect device": "自动选择设备",
|
||||
@@ -46,10 +47,15 @@
|
||||
"Bit rate": "比特率",
|
||||
"Bold font": "粗体字体",
|
||||
"Broadcast": "广播",
|
||||
"Browse Headphone Profiles": "Browse Headphone Profiles",
|
||||
"Cancel": "取消",
|
||||
"Cannot Delete": "Cannot Delete",
|
||||
"Cannot delete builtin presets": "Cannot delete builtin presets",
|
||||
"Cannot use the name of a builtin preset": "Cannot use the name of a builtin preset",
|
||||
"Cast to device": "投屏到设备",
|
||||
"Channels": "频道",
|
||||
"Check for Updates": "检查更新",
|
||||
"Check network connection and try again": "Check network connection and try again",
|
||||
"Clear caches": "清除缓存",
|
||||
"Close": "关闭",
|
||||
"Close to system tray": "关闭到系统托盘",
|
||||
@@ -70,7 +76,10 @@
|
||||
"DJ-Mix": "DJ混音",
|
||||
"Date added": "添加日期",
|
||||
"Dec": "十二月",
|
||||
"Delete": "Delete",
|
||||
"Delete Playlist": "删除播放列表",
|
||||
"Delete Preset": "Delete Preset",
|
||||
"Delete preset '%s'?": "Delete preset '%s'?",
|
||||
"Demo": "演示",
|
||||
"Disable automatic DPI adjustment": "禁用自动 DPI 调整",
|
||||
"Disable server transcoding": "禁用服务器转码",
|
||||
@@ -81,6 +90,22 @@
|
||||
"Duration": "持续时间",
|
||||
"EP": "EP",
|
||||
"EPs": "EPs",
|
||||
"EQ Acoustic": "Acoustic",
|
||||
"EQ Bass Boost": "Bass Boost",
|
||||
"EQ Classical": "Classical",
|
||||
"EQ Electronic": "Electronic",
|
||||
"EQ Flat": "Flat",
|
||||
"EQ Jazz": "Jazz",
|
||||
"EQ Loudness": "Loudness",
|
||||
"EQ Pop": "Pop",
|
||||
"EQ Preamp": "Pre",
|
||||
"EQ Preset": "Preset",
|
||||
"EQ Preset:": "Preset:",
|
||||
"EQ R\u0026B": "R\u0026B",
|
||||
"EQ Rock": "Rock",
|
||||
"EQ Treble Boost": "Treble Boost",
|
||||
"EQ Type:": "EQ Type:",
|
||||
"EQ Vocal": "Vocal",
|
||||
"Edit": "编辑",
|
||||
"Edit Playlist": "编辑播放列表",
|
||||
"Edit server": "编辑服务器",
|
||||
@@ -92,9 +117,11 @@
|
||||
"Equalizer": "均衡器",
|
||||
"Error": "错误",
|
||||
"Error creating playlist": "创建播放列表时出错",
|
||||
"Error loading AutoEQ profiles": "Error loading AutoEQ profiles",
|
||||
"Error updating playlist": "更新播放列表时出错",
|
||||
"Exclusive mode": "独占模式",
|
||||
"Fade out on pause": "暂停时淡出",
|
||||
"Failed to load profile": "Failed to load profile",
|
||||
"Fav.": "收藏",
|
||||
"Favorites": "收藏",
|
||||
"Feb": "二月",
|
||||
@@ -118,6 +145,7 @@
|
||||
"In order": "按顺序",
|
||||
"Internet Radio Stations": "网络电台",
|
||||
"Interview": "采访",
|
||||
"Invalid Name": "Invalid Name",
|
||||
"Is favorite": "已收藏",
|
||||
"Is not favorite": "未收藏",
|
||||
"Jan": "一月",
|
||||
@@ -142,9 +170,11 @@
|
||||
"My Server": "我的服务器",
|
||||
"Name": "名称",
|
||||
"Name (A-Z)": "名称 (A-Z)",
|
||||
"Network error. Check connection.": "Network error. Check connection.",
|
||||
"New Playlist": "新播放列表",
|
||||
"Next": "下一个",
|
||||
"Nickname": "昵称",
|
||||
"No Preset Selected": "No Preset Selected",
|
||||
"No new version found": "未找到新版本",
|
||||
"No radio stations available": "没有可用的电台",
|
||||
"None": "无",
|
||||
@@ -154,6 +184,7 @@
|
||||
"Now Playing": "正在播放",
|
||||
"OK": "确定",
|
||||
"Oct": "十月",
|
||||
"Overwrite Preset": "Overwrite Preset",
|
||||
"Owner": "拥有者",
|
||||
"Password": "密码",
|
||||
"Pause": "暂停",
|
||||
@@ -174,10 +205,15 @@
|
||||
"Playlist": "播放列表",
|
||||
"Playlists": "播放列表",
|
||||
"Plays": "播放",
|
||||
"Please select a preset to delete": "Please select a preset to delete",
|
||||
"Preset '%s' already exists. Overwrite?": "Preset '%s' already exists. Overwrite?",
|
||||
"Preset name": "Preset name",
|
||||
"Prevent clipping": "防止削波",
|
||||
"Prevent screensaver on Now Playing page": "禁止在“正在播放”页面显示屏幕保护程序",
|
||||
"Previous": "上一个",
|
||||
"Private playlist by": "私人播放列表",
|
||||
"Profile": "Profile",
|
||||
"Profile not found": "Profile not found",
|
||||
"Public": "公开",
|
||||
"Public playlist by": "公开播放列表",
|
||||
"Quit": "退出",
|
||||
@@ -194,13 +230,19 @@
|
||||
"ReplayGain mode": "重播增益模式",
|
||||
"ReplayGain preamp": "重播增益前置放大",
|
||||
"Rescan Library": "重新扫描库",
|
||||
"Reset": "Reset",
|
||||
"Restart required": "需要重启",
|
||||
"Sample rate": "采样率",
|
||||
"Save": "Save",
|
||||
"Save As": "Save As",
|
||||
"Save Preset": "Save Preset",
|
||||
"Save Preset As": "Save Preset As",
|
||||
"Save play queue": "退出时保存播放队列",
|
||||
"Saved at": "保存于",
|
||||
"Scrobble when": "记录播放时",
|
||||
"Search": "搜索",
|
||||
"Search Everywhere": "搜索所有位置",
|
||||
"Search headphones...": "Search headphones...",
|
||||
"Search page": "搜索页面",
|
||||
"Search playlists or new playlist name": "搜索播放列表或新播放列表名称",
|
||||
"Select Library": "选择库",
|
||||
@@ -340,4 +382,4 @@
|
||||
"one": "{{.trackCount}} 曲目",
|
||||
"other": "{{.trackCount}} 曲目"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@
|
||||
"Aug": "八月",
|
||||
"Authentication failed": "认证失败",
|
||||
"Auto": "自动",
|
||||
"AutoEQ": "AutoEQ",
|
||||
"Automatically check for updates": "自动检查更新",
|
||||
"Autoplay": "自动播放",
|
||||
"Autoselect device": "自动选择设备",
|
||||
@@ -46,10 +47,15 @@
|
||||
"Bit rate": "比特率",
|
||||
"Bold font": "粗体字体",
|
||||
"Broadcast": "广播",
|
||||
"Browse Headphone Profiles": "Browse Headphone Profiles",
|
||||
"Cancel": "取消",
|
||||
"Cannot Delete": "Cannot Delete",
|
||||
"Cannot delete builtin presets": "Cannot delete builtin presets",
|
||||
"Cannot use the name of a builtin preset": "Cannot use the name of a builtin preset",
|
||||
"Cast to device": "投屏到设备",
|
||||
"Channels": "频道",
|
||||
"Check for Updates": "检查更新",
|
||||
"Check network connection and try again": "Check network connection and try again",
|
||||
"Clear caches": "清除缓存",
|
||||
"Close": "关闭",
|
||||
"Close to system tray": "关闭到系统托盘",
|
||||
@@ -70,7 +76,10 @@
|
||||
"DJ-Mix": "DJ混音",
|
||||
"Date added": "添加日期",
|
||||
"Dec": "十二月",
|
||||
"Delete": "Delete",
|
||||
"Delete Playlist": "删除播放列表",
|
||||
"Delete Preset": "Delete Preset",
|
||||
"Delete preset '%s'?": "Delete preset '%s'?",
|
||||
"Demo": "演示",
|
||||
"Disable automatic DPI adjustment": "禁用自动 DPI 调整",
|
||||
"Disable server transcoding": "禁用服务器转码",
|
||||
@@ -81,6 +90,22 @@
|
||||
"Duration": "持续时间",
|
||||
"EP": "EP",
|
||||
"EPs": "EPs",
|
||||
"EQ Acoustic": "Acoustic",
|
||||
"EQ Bass Boost": "Bass Boost",
|
||||
"EQ Classical": "Classical",
|
||||
"EQ Electronic": "Electronic",
|
||||
"EQ Flat": "Flat",
|
||||
"EQ Jazz": "Jazz",
|
||||
"EQ Loudness": "Loudness",
|
||||
"EQ Pop": "Pop",
|
||||
"EQ Preamp": "Pre",
|
||||
"EQ Preset": "Preset",
|
||||
"EQ Preset:": "Preset:",
|
||||
"EQ R\u0026B": "R\u0026B",
|
||||
"EQ Rock": "Rock",
|
||||
"EQ Treble Boost": "Treble Boost",
|
||||
"EQ Type:": "EQ Type:",
|
||||
"EQ Vocal": "Vocal",
|
||||
"Edit": "编辑",
|
||||
"Edit Playlist": "编辑播放列表",
|
||||
"Edit server": "编辑服务器",
|
||||
@@ -92,9 +117,11 @@
|
||||
"Equalizer": "均衡器",
|
||||
"Error": "错误",
|
||||
"Error creating playlist": "创建播放列表时出错",
|
||||
"Error loading AutoEQ profiles": "Error loading AutoEQ profiles",
|
||||
"Error updating playlist": "更新播放列表时出错",
|
||||
"Exclusive mode": "独占模式",
|
||||
"Fade out on pause": "暂停时淡出",
|
||||
"Failed to load profile": "Failed to load profile",
|
||||
"Fav.": "收藏",
|
||||
"Favorites": "收藏",
|
||||
"Feb": "二月",
|
||||
@@ -118,6 +145,7 @@
|
||||
"In order": "按顺序",
|
||||
"Internet Radio Stations": "网络电台",
|
||||
"Interview": "采访",
|
||||
"Invalid Name": "Invalid Name",
|
||||
"Is favorite": "已收藏",
|
||||
"Is not favorite": "未收藏",
|
||||
"Jan": "一月",
|
||||
@@ -142,9 +170,11 @@
|
||||
"My Server": "我的服务器",
|
||||
"Name": "名称",
|
||||
"Name (A-Z)": "名称 (A-Z)",
|
||||
"Network error. Check connection.": "Network error. Check connection.",
|
||||
"New Playlist": "新播放列表",
|
||||
"Next": "下一个",
|
||||
"Nickname": "昵称",
|
||||
"No Preset Selected": "No Preset Selected",
|
||||
"No new version found": "未找到新版本",
|
||||
"No radio stations available": "没有可用的电台",
|
||||
"None": "无",
|
||||
@@ -154,6 +184,7 @@
|
||||
"Now Playing": "正在播放",
|
||||
"OK": "确定",
|
||||
"Oct": "十月",
|
||||
"Overwrite Preset": "Overwrite Preset",
|
||||
"Owner": "拥有者",
|
||||
"Password": "密码",
|
||||
"Pause": "暂停",
|
||||
@@ -174,10 +205,15 @@
|
||||
"Playlist": "播放列表",
|
||||
"Playlists": "播放列表",
|
||||
"Plays": "播放",
|
||||
"Please select a preset to delete": "Please select a preset to delete",
|
||||
"Preset '%s' already exists. Overwrite?": "Preset '%s' already exists. Overwrite?",
|
||||
"Preset name": "Preset name",
|
||||
"Prevent clipping": "防止削波",
|
||||
"Prevent screensaver on Now Playing page": "禁止在“正在播放”页面显示屏幕保护程序",
|
||||
"Previous": "上一个",
|
||||
"Private playlist by": "私人播放列表",
|
||||
"Profile": "Profile",
|
||||
"Profile not found": "Profile not found",
|
||||
"Public": "公开",
|
||||
"Public playlist by": "公开播放列表",
|
||||
"Quit": "退出",
|
||||
@@ -194,13 +230,19 @@
|
||||
"ReplayGain mode": "重播增益模式",
|
||||
"ReplayGain preamp": "重播增益前置放大",
|
||||
"Rescan Library": "重新扫描库",
|
||||
"Reset": "Reset",
|
||||
"Restart required": "需要重启",
|
||||
"Sample rate": "采样率",
|
||||
"Save": "Save",
|
||||
"Save As": "Save As",
|
||||
"Save Preset": "Save Preset",
|
||||
"Save Preset As": "Save Preset As",
|
||||
"Save play queue": "退出时保存播放队列",
|
||||
"Saved at": "保存于",
|
||||
"Scrobble when": "记录播放时",
|
||||
"Search": "搜索",
|
||||
"Search Everywhere": "搜索所有位置",
|
||||
"Search headphones...": "Search headphones...",
|
||||
"Search page": "搜索页面",
|
||||
"Search playlists or new playlist name": "搜索播放列表或新播放列表名称",
|
||||
"Select Library": "选择库",
|
||||
@@ -340,4 +382,4 @@
|
||||
"one": "{{.trackCount}} 曲目",
|
||||
"other": "{{.trackCount}} 曲目"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@
|
||||
"Audiobook": "有聲書",
|
||||
"Authentication failed": "認證失敗",
|
||||
"Auto": "自動",
|
||||
"AutoEQ": "AutoEQ",
|
||||
"Automatically check for updates": "Automatically check for updates",
|
||||
"Autoplay": "Autoplay",
|
||||
"Autoselect device": "自動選擇裝置",
|
||||
@@ -43,10 +44,15 @@
|
||||
"Bit rate": "比特率",
|
||||
"Bold font": "Bold font",
|
||||
"Broadcast": "廣播",
|
||||
"Browse Headphone Profiles": "Browse Headphone Profiles",
|
||||
"Cancel": "取消",
|
||||
"Cannot Delete": "Cannot Delete",
|
||||
"Cannot delete builtin presets": "Cannot delete builtin presets",
|
||||
"Cannot use the name of a builtin preset": "Cannot use the name of a builtin preset",
|
||||
"Cast to device": "Cast to device",
|
||||
"Channels": "Channels",
|
||||
"Check for Updates": "檢查更新",
|
||||
"Check network connection and try again": "Check network connection and try again",
|
||||
"Clear caches": "Clear caches",
|
||||
"Close": "關閉",
|
||||
"Close to system tray": "關閉到系統匣",
|
||||
@@ -66,7 +72,10 @@
|
||||
"Create new playlist": "建立新播放清單",
|
||||
"DJ-Mix": "DJ混音",
|
||||
"Date added": "Date added",
|
||||
"Delete": "Delete",
|
||||
"Delete Playlist": "刪除播放清單",
|
||||
"Delete Preset": "Delete Preset",
|
||||
"Delete preset '%s'?": "Delete preset '%s'?",
|
||||
"Demo": "示範",
|
||||
"Disable automatic DPI adjustment": "Disable automatic DPI adjustment",
|
||||
"Disable server transcoding": "禁用伺服器轉碼",
|
||||
@@ -77,6 +86,22 @@
|
||||
"Duration": "持續時間",
|
||||
"EP": "EP",
|
||||
"EPs": "EPs",
|
||||
"EQ Acoustic": "Acoustic",
|
||||
"EQ Bass Boost": "Bass Boost",
|
||||
"EQ Classical": "Classical",
|
||||
"EQ Electronic": "Electronic",
|
||||
"EQ Flat": "Flat",
|
||||
"EQ Jazz": "Jazz",
|
||||
"EQ Loudness": "Loudness",
|
||||
"EQ Pop": "Pop",
|
||||
"EQ Preamp": "Pre",
|
||||
"EQ Preset": "Preset",
|
||||
"EQ Preset:": "Preset:",
|
||||
"EQ R\u0026B": "R\u0026B",
|
||||
"EQ Rock": "Rock",
|
||||
"EQ Treble Boost": "Treble Boost",
|
||||
"EQ Type:": "EQ Type:",
|
||||
"EQ Vocal": "Vocal",
|
||||
"Edit": "編輯",
|
||||
"Edit Playlist": "編輯播放清單",
|
||||
"Edit server": "編輯伺服器",
|
||||
@@ -88,9 +113,11 @@
|
||||
"Equalizer": "均衡器",
|
||||
"Error": "Error",
|
||||
"Error creating playlist": "Error creating playlist",
|
||||
"Error loading AutoEQ profiles": "Error loading AutoEQ profiles",
|
||||
"Error updating playlist": "Error updating playlist",
|
||||
"Exclusive mode": "獨佔模式",
|
||||
"Fade out on pause": "Fade out on pause",
|
||||
"Failed to load profile": "Failed to load profile",
|
||||
"Fav.": "收藏",
|
||||
"Favorites": "收藏",
|
||||
"Field Recording": "現場錄音",
|
||||
@@ -113,6 +140,7 @@
|
||||
"In order": "In order",
|
||||
"Internet Radio Stations": "網路電台",
|
||||
"Interview": "訪談",
|
||||
"Invalid Name": "Invalid Name",
|
||||
"Is favorite": "已收藏",
|
||||
"Is not favorite": "未收藏",
|
||||
"Language": "語言",
|
||||
@@ -132,15 +160,18 @@
|
||||
"My Server": "我的伺服器",
|
||||
"Name": "名稱",
|
||||
"Name (A-Z)": "名稱 (A-Z)",
|
||||
"Network error. Check connection.": "Network error. Check connection.",
|
||||
"New Playlist": "New Playlist",
|
||||
"Next": "下一個",
|
||||
"Nickname": "暱稱",
|
||||
"No Preset Selected": "No Preset Selected",
|
||||
"No new version found": "未找到新版本",
|
||||
"No radio stations available": "沒有可用的電台",
|
||||
"None": "無",
|
||||
"Normal": "Normal",
|
||||
"Normal font": "Normal font",
|
||||
"OK": "確定",
|
||||
"Overwrite Preset": "Overwrite Preset",
|
||||
"Owner": "擁有者",
|
||||
"Password": "密碼",
|
||||
"Pause": "暫停",
|
||||
@@ -161,10 +192,15 @@
|
||||
"Playlist": "播放清單",
|
||||
"Playlists": "播放清單",
|
||||
"Plays": "播放",
|
||||
"Please select a preset to delete": "Please select a preset to delete",
|
||||
"Preset '%s' already exists. Overwrite?": "Preset '%s' already exists. Overwrite?",
|
||||
"Preset name": "Preset name",
|
||||
"Prevent clipping": "防止削波",
|
||||
"Prevent screensaver on Now Playing page": "Prevent screensaver on Now Playing page",
|
||||
"Previous": "上一個",
|
||||
"Private playlist by": "私人 播放清單作者",
|
||||
"Profile": "Profile",
|
||||
"Profile not found": "Profile not found",
|
||||
"Public": "Public",
|
||||
"Public playlist by": "公開 播放清單作者",
|
||||
"Quit": "退出",
|
||||
@@ -181,13 +217,19 @@
|
||||
"ReplayGain mode": "重播增益模式",
|
||||
"ReplayGain preamp": "重播增益前置放大",
|
||||
"Rescan Library": "重新掃描庫",
|
||||
"Reset": "Reset",
|
||||
"Restart required": "需要重新啟動",
|
||||
"Sample rate": "Sample rate",
|
||||
"Save": "Save",
|
||||
"Save As": "Save As",
|
||||
"Save Preset": "Save Preset",
|
||||
"Save Preset As": "Save Preset As",
|
||||
"Save play queue": "退出時保存播放佇列",
|
||||
"Saved at": "保存於",
|
||||
"Scrobble when": "記錄播放時",
|
||||
"Search": "搜索",
|
||||
"Search Everywhere": "搜尋所有地方",
|
||||
"Search headphones...": "Search headphones...",
|
||||
"Search page": "搜尋頁面",
|
||||
"Search playlists or new playlist name": "搜尋播放清單或新播放清單名稱",
|
||||
"Select Library": "Select Library",
|
||||
@@ -299,4 +341,4 @@
|
||||
"x_minutes_ago": {
|
||||
"other": "59 minutes ago"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -327,7 +327,11 @@ func (c *Controller) ShowSettingsDialog(themeUpdateCallbk func(), themeFiles map
|
||||
devs, themeFiles, bands,
|
||||
c.App.ServerManager.Server.ClientDecidesScrobble(),
|
||||
isLocalPlayer, isReplayGainPlayer, isEqualizerPlayer, canSavePlayQueue,
|
||||
c.MainWindow)
|
||||
c.App.EQPresetManager,
|
||||
c.MainWindow,
|
||||
c.App.AutoEQManager,
|
||||
c.App.ImageManager,
|
||||
c.ToastProvider)
|
||||
dlg.OnReplayGainSettingsChanged = func() {
|
||||
c.App.PlaybackManager.SetReplayGainOptions(c.App.Config.ReplayGain)
|
||||
}
|
||||
@@ -342,11 +346,31 @@ func (c *Controller) ShowSettingsDialog(themeUpdateCallbk func(), themeFiles map
|
||||
}
|
||||
dlg.OnThemeSettingChanged = themeUpdateCallbk
|
||||
dlg.OnEqualizerSettingsChanged = func() {
|
||||
// currently we only have one equalizer type
|
||||
eq := c.App.LocalPlayer.Equalizer().(*mpv.ISO15BandEqualizer)
|
||||
eq.Disabled = !c.App.Config.LocalPlayback.EqualizerEnabled
|
||||
eq.EQPreamp = c.App.Config.LocalPlayback.EqualizerPreamp
|
||||
copy(eq.BandGains[:], c.App.Config.LocalPlayback.GraphicEqualizerBands)
|
||||
// Create the appropriate equalizer type based on config
|
||||
var eq mpv.Equalizer
|
||||
if c.App.Config.LocalPlayback.EqualizerType == "ISO10Band" {
|
||||
eq10 := &mpv.ISO10BandEqualizer{
|
||||
Disabled: !c.App.Config.LocalPlayback.EqualizerEnabled,
|
||||
EQPreamp: c.App.Config.LocalPlayback.EqualizerPreamp,
|
||||
}
|
||||
// Copy up to 10 bands
|
||||
numBands := min(len(c.App.Config.LocalPlayback.GraphicEqualizerBands), 10)
|
||||
for i := 0; i < numBands; i++ {
|
||||
eq10.BandGains[i] = c.App.Config.LocalPlayback.GraphicEqualizerBands[i]
|
||||
}
|
||||
eq = eq10
|
||||
} else {
|
||||
eq15 := &mpv.ISO15BandEqualizer{
|
||||
Disabled: !c.App.Config.LocalPlayback.EqualizerEnabled,
|
||||
EQPreamp: c.App.Config.LocalPlayback.EqualizerPreamp,
|
||||
}
|
||||
// Copy up to 15 bands
|
||||
numBands := min(len(c.App.Config.LocalPlayback.GraphicEqualizerBands), 15)
|
||||
for i := 0; i < numBands; i++ {
|
||||
eq15.BandGains[i] = c.App.Config.LocalPlayback.GraphicEqualizerBands[i]
|
||||
}
|
||||
eq = eq15
|
||||
}
|
||||
c.App.LocalPlayer.SetEqualizer(eq)
|
||||
}
|
||||
dlg.OnPageNeedsRefresh = c.RefreshPageFunc
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
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
|
||||
toastProvider ToastProvider
|
||||
allProfileResults []*mediaprovider.SearchResult
|
||||
OnProfileSelected func(*backend.AutoEQProfile)
|
||||
}
|
||||
|
||||
func NewAutoEQBrowser(manager *backend.AutoEQManager, im util.ImageFetcher, toastProvider ToastProvider) *AutoEQBrowser {
|
||||
ab := &AutoEQBrowser{
|
||||
manager: manager,
|
||||
toastProvider: toastProvider,
|
||||
}
|
||||
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 {
|
||||
// Show empty results on error
|
||||
ab.allProfileResults = []*mediaprovider.SearchResult{}
|
||||
return fmt.Errorf("failed to fetch AutoEQ index: %w", err)
|
||||
}
|
||||
|
||||
// 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)
|
||||
fyne.Do(func() {
|
||||
ab.toastProvider.ShowErrorToast(lang.L("Error loading AutoEQ profiles"))
|
||||
})
|
||||
return []*mediaprovider.SearchResult{}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
go func() {
|
||||
// Fetch the full profile data
|
||||
profile, err := ab.manager.FetchProfile(context.Background(), profilePath)
|
||||
fyne.Do(func() {
|
||||
if err != nil {
|
||||
log.Printf("Error loading AutoEQ profile: %v", err)
|
||||
ab.toastProvider.ShowErrorToast(lang.L("Error loading AutoEQ profile"))
|
||||
} else {
|
||||
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()
|
||||
}
|
||||
+537
-15
@@ -2,13 +2,17 @@ package dialogs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"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 +21,208 @@ 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")
|
||||
isDirty bool // true when sliders modified since last preset load/save
|
||||
loadedPreset *backend.EQPreset // currently loaded preset (nil if none)
|
||||
saveBtn *ttwidget.Button // reference for enable/disable control
|
||||
}
|
||||
|
||||
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)
|
||||
// Populate loadedPreset and detect dirty state on dialog reopen
|
||||
for i, p := range g.eqPresets {
|
||||
if p.Name == activePresetName {
|
||||
g.loadedPreset = &g.eqPresets[i]
|
||||
g.isDirty = !g.matchesPreset(p)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
g.updateSaveButtonState()
|
||||
|
||||
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 (overwrites current loaded preset)
|
||||
g.saveBtn = ttwidget.NewButtonWithIcon("", myTheme.SaveIcon, func() {
|
||||
g.saveCurrentPreset()
|
||||
})
|
||||
g.saveBtn.Disable() // starts disabled
|
||||
g.saveBtn.SetToolTip(lang.L("Save"))
|
||||
|
||||
// Save As button (always enabled, opens name-entry dialog)
|
||||
saveAsBtn := ttwidget.NewButtonWithIcon("", myTheme.SaveAsIcon, func() {
|
||||
g.showSaveAsDialog()
|
||||
})
|
||||
saveAsBtn.SetToolTip(lang.L("Save As"))
|
||||
|
||||
// Delete button
|
||||
deleteBtn := ttwidget.NewButtonWithIcon("", theme.DeleteIcon(), func() {
|
||||
g.showDeletePresetDialog()
|
||||
})
|
||||
deleteBtn.SetToolTip(lang.L("Delete"))
|
||||
|
||||
// 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,
|
||||
layout.NewSpacer(),
|
||||
g.saveBtn,
|
||||
saveAsBtn,
|
||||
deleteBtn,
|
||||
resetBtn,
|
||||
g.autoEQBtn,
|
||||
),
|
||||
// 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.isDirty = true
|
||||
g.updateSaveButtonState()
|
||||
if 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 +235,21 @@ func (g *GraphicEqualizer) buildSliders(preamp float64, bands []string, bandGain
|
||||
g.OnChanged(_i, f)
|
||||
}
|
||||
g.bandSliders[_i].UpdateToolTip()
|
||||
if !g.isApplyingPreset {
|
||||
g.isDirty = true
|
||||
g.updateSaveButtonState()
|
||||
if 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 +263,351 @@ 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()
|
||||
|
||||
// Clear loaded preset when EQ type changes
|
||||
g.loadedPreset = nil
|
||||
g.isDirty = false
|
||||
g.updateSaveButtonState()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track the loaded preset and clear dirty state
|
||||
presetCopy := preset
|
||||
g.loadedPreset = &presetCopy
|
||||
g.isDirty = false
|
||||
g.updateSaveButtonState()
|
||||
}
|
||||
|
||||
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) updateSaveButtonState() {
|
||||
if g.saveBtn == nil {
|
||||
return
|
||||
}
|
||||
if g.loadedPreset != nil && !g.loadedPreset.IsBuiltin && g.isDirty {
|
||||
g.saveBtn.Enable()
|
||||
} else {
|
||||
g.saveBtn.Disable()
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GraphicEqualizer) saveCurrentPreset() {
|
||||
if g.loadedPreset == nil || g.loadedPreset.IsBuiltin {
|
||||
return
|
||||
}
|
||||
g.savePresetWithName(g.loadedPreset.Name)
|
||||
}
|
||||
|
||||
func (g *GraphicEqualizer) savePresetWithName(name string) {
|
||||
preset := g.getCurrentSettings()
|
||||
preset.Name = name
|
||||
preset.IsBuiltin = false
|
||||
|
||||
if err := g.presetManager.SavePreset(preset); err != nil {
|
||||
dialog.ShowError(err, g.parentWindow)
|
||||
return
|
||||
}
|
||||
|
||||
// Update loaded preset and clear dirty state
|
||||
g.loadedPreset = &preset
|
||||
g.isDirty = false
|
||||
g.updateSaveButtonState()
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GraphicEqualizer) showSaveAsDialog() {
|
||||
nameEntry := widget.NewEntry()
|
||||
nameEntry.SetPlaceHolder(lang.L("Preset name"))
|
||||
|
||||
// Pre-fill with loaded preset name if it's a custom preset
|
||||
if g.loadedPreset != nil && !g.loadedPreset.IsBuiltin {
|
||||
nameEntry.SetText(g.loadedPreset.Name)
|
||||
}
|
||||
|
||||
formDialog := dialog.NewForm(
|
||||
lang.L("Save Preset As"),
|
||||
lang.L("Save"),
|
||||
lang.L("Cancel"),
|
||||
[]*widget.FormItem{
|
||||
widget.NewFormItem(lang.L("Name"), nameEntry),
|
||||
},
|
||||
func(confirmed bool) {
|
||||
if !confirmed || nameEntry.Text == "" {
|
||||
return
|
||||
}
|
||||
|
||||
name := nameEntry.Text
|
||||
|
||||
// Check if name matches a builtin preset
|
||||
for _, p := range g.eqPresets {
|
||||
if p.Name == name && p.IsBuiltin {
|
||||
dialog.ShowInformation(
|
||||
lang.L("Invalid Name"),
|
||||
lang.L("Cannot use the name of a builtin preset"),
|
||||
g.parentWindow,
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check if name matches an existing custom preset
|
||||
for _, p := range g.eqPresets {
|
||||
if p.Name == name && !p.IsBuiltin {
|
||||
dialog.ShowConfirm(
|
||||
lang.L("Overwrite Preset"),
|
||||
fmt.Sprintf(lang.L("Preset '%s' already exists. Overwrite?"), name),
|
||||
func(overwrite bool) {
|
||||
if overwrite {
|
||||
g.savePresetWithName(name)
|
||||
}
|
||||
},
|
||||
g.parentWindow,
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
g.savePresetWithName(name)
|
||||
},
|
||||
g.parentWindow,
|
||||
)
|
||||
|
||||
formDialog.Resize(fyne.NewSize(400, 150))
|
||||
formDialog.Show()
|
||||
}
|
||||
|
||||
// matchesPreset compares current slider values against a preset
|
||||
func (g *GraphicEqualizer) matchesPreset(preset backend.EQPreset) bool {
|
||||
if g.preampSlider == nil {
|
||||
return false
|
||||
}
|
||||
if math.Abs(g.preampSlider.Value-preset.Preamp) > 0.05 {
|
||||
return false
|
||||
}
|
||||
if len(g.bandSliders) != len(preset.Bands) {
|
||||
return false
|
||||
}
|
||||
for i, slider := range g.bandSliders {
|
||||
if math.Abs(slider.Value-preset.Bands[i]) > 0.05 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ClearLoadedPresetState clears the loaded preset and dirty state
|
||||
func (g *GraphicEqualizer) ClearLoadedPresetState() {
|
||||
g.loadedPreset = nil
|
||||
g.isDirty = false
|
||||
g.updateSaveButtonState()
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
+24
-10
@@ -272,7 +272,11 @@ func (s *searchResult) Update(result *mediaprovider.SearchResult) {
|
||||
}
|
||||
s.id = result.ID
|
||||
s.contentType = result.Type
|
||||
s.image.PlaceholderIcon = placeholderIconForContentType(result.Type)
|
||||
if result.Icon != nil {
|
||||
s.image.PlaceholderIcon = result.Icon
|
||||
} else {
|
||||
s.image.PlaceholderIcon = placeholderIconForContentType(result.Type)
|
||||
}
|
||||
s.imageLoader.Load(result.CoverID)
|
||||
s.title.SetText(result.Name)
|
||||
|
||||
@@ -300,19 +304,29 @@ func (s *searchResult) Update(result *mediaprovider.SearchResult) {
|
||||
} else {
|
||||
secondaryText = ""
|
||||
}
|
||||
case mediaprovider.ContentTypeOther:
|
||||
secondaryText = result.ArtistName
|
||||
}
|
||||
s.secondary.Segments = []widget.RichTextSegment{
|
||||
&widget.TextSegment{
|
||||
Text: result.Type.String(),
|
||||
Style: widget.RichTextStyle{SizeName: theme.SizeNameCaptionText, TextStyle: fyne.TextStyle{Bold: true}, Inline: true},
|
||||
},
|
||||
|
||||
if result.Type == mediaprovider.ContentTypeOther {
|
||||
s.secondary.Segments = []widget.RichTextSegment{}
|
||||
} else {
|
||||
s.secondary.Segments = []widget.RichTextSegment{
|
||||
&widget.TextSegment{
|
||||
Text: result.Type.String(),
|
||||
Style: widget.RichTextStyle{SizeName: theme.SizeNameCaptionText, TextStyle: fyne.TextStyle{Bold: true}, Inline: true},
|
||||
},
|
||||
}
|
||||
}
|
||||
if secondaryText != "" {
|
||||
if len(s.secondary.Segments) > 0 {
|
||||
s.secondary.Segments = append(s.secondary.Segments,
|
||||
&widget.TextSegment{
|
||||
Text: " · ",
|
||||
Style: widget.RichTextStyle{SizeName: theme.SizeNameCaptionText, Inline: true},
|
||||
})
|
||||
}
|
||||
s.secondary.Segments = append(s.secondary.Segments,
|
||||
&widget.TextSegment{
|
||||
Text: " · ",
|
||||
Style: widget.RichTextStyle{SizeName: theme.SizeNameCaptionText, Inline: true},
|
||||
},
|
||||
&widget.TextSegment{
|
||||
Text: secondaryText,
|
||||
Style: widget.RichTextStyle{SizeName: theme.SizeNameCaptionText, Inline: true},
|
||||
|
||||
@@ -2,6 +2,7 @@ package dialogs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
"slices"
|
||||
@@ -41,16 +42,26 @@ 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
|
||||
toastProvider ToastProvider
|
||||
|
||||
clientDecidesScrobble bool
|
||||
|
||||
content fyne.CanvasObject
|
||||
}
|
||||
|
||||
type ToastProvider interface {
|
||||
ShowSuccessToast(message string)
|
||||
ShowErrorToast(message string)
|
||||
}
|
||||
|
||||
// TODO: having this depend on the mpv package for the AudioDevice type is kinda gross. Refactor.
|
||||
func NewSettingsDialog(
|
||||
config *backend.Config,
|
||||
@@ -62,9 +73,23 @@ func NewSettingsDialog(
|
||||
isReplayGainPlayer bool,
|
||||
isEqualizerPlayer bool,
|
||||
canSavePlayQueue bool,
|
||||
eqPresetMgr *backend.EQPresetManager,
|
||||
window fyne.Window,
|
||||
autoEQManager *backend.AutoEQManager,
|
||||
imageManager util.ImageFetcher,
|
||||
toastProvider ToastProvider,
|
||||
) *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,
|
||||
toastProvider: toastProvider,
|
||||
}
|
||||
s.ExtendBaseWidget(s)
|
||||
|
||||
// TODO: It may be a nicer UX to always create the equalizer tab,
|
||||
@@ -465,7 +490,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 +508,150 @@ func (s *SettingsDialog) createEqualizerTab(eqBands []string) *container.TabItem
|
||||
s.config.LocalPlayback.EqualizerPreamp = g
|
||||
debouncer()
|
||||
}
|
||||
geq.OnManualAdjustment = func() {
|
||||
// Clear AutoEQ profile when user manually adjusts sliders
|
||||
// Preset name persists so Save button can overwrite the loaded preset
|
||||
s.config.LocalPlayback.AutoEQProfilePath = ""
|
||||
s.config.LocalPlayback.AutoEQProfileName = ""
|
||||
geq.ClearProfileLabel()
|
||||
}
|
||||
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.InterpolateEQ15BandTo10Band(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.InterpolateEQ10To15Band(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, s.toastProvider)
|
||||
|
||||
// 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 and loaded preset state since AutoEQ is now active
|
||||
geq.ClearPresetSelection()
|
||||
geq.ClearLoadedPresetState()
|
||||
|
||||
// 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{""}
|
||||
|
||||
+3
-1
@@ -54,7 +54,7 @@ var (
|
||||
RadioIcon fyne.Resource = theme.NewThemedResource(res.ResBroadcastSvg)
|
||||
FavoriteIcon fyne.Resource = theme.NewThemedResource(res.ResHeartFilledSvg)
|
||||
NotFavoriteIcon fyne.Resource = theme.NewThemedResource(res.ResHeartOutlineSvg)
|
||||
NowPlayingIcon fyne.Resource = theme.NewThemedResource(res.ResHeadphonesSvg)
|
||||
HeadphonesIcon fyne.Resource = theme.NewThemedResource(res.ResHeadphonesSvg)
|
||||
PlaylistIcon fyne.Resource = theme.NewThemedResource(res.ResPlaylistSvg)
|
||||
PlayNextIcon fyne.Resource = theme.NewThemedResource(res.ResPlaylistAddNextSvg)
|
||||
PlayQueueIcon fyne.Resource = theme.NewThemedResource(res.ResPlayqueueSvg)
|
||||
@@ -69,6 +69,8 @@ var (
|
||||
SortIcon fyne.Resource = theme.NewThemedResource(res.ResUpdownarrowSvg)
|
||||
VisualizationIcon fyne.Resource = theme.NewThemedResource(res.ResOscilloscopeSvg)
|
||||
LibraryIcon fyne.Resource = theme.NewThemedResource(res.ResLibrarySvg)
|
||||
SaveIcon fyne.Resource = theme.NewThemedResource(res.ResSaveSvg)
|
||||
SaveAsIcon fyne.Resource = theme.NewThemedResource(res.ResSaveasSvg)
|
||||
)
|
||||
|
||||
type AppearanceMode string
|
||||
|
||||
+1
-1
@@ -146,7 +146,7 @@ func (t *Toolbar) CreateRenderer() fyne.WidgetRenderer {
|
||||
}
|
||||
|
||||
func (t *Toolbar) setupNavigationButtons(navigateFn func(controller.Route)) {
|
||||
t.addNavigationButton(myTheme.NowPlayingIcon, controller.NowPlaying, func() {
|
||||
t.addNavigationButton(myTheme.HeadphonesIcon, controller.NowPlaying, func() {
|
||||
navigateFn(controller.NowPlayingRoute())
|
||||
})
|
||||
t.addNavigationButton(myTheme.FavoriteIcon, controller.Favorites, func() {
|
||||
|
||||
Reference in New Issue
Block a user