From 1546d7da22eed485b51495e4702a47b9e002d14a Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 19 Jan 2026 18:57:27 +0700 Subject: [PATCH] feat: add external LRC lyrics file support and fix locale parsing - Add lyrics mode setting (embed/external/both) for saving lyrics - Implement SaveLRCFile() in Go backend for all providers (Tidal, Qobuz, Amazon) - Fix locale parsing in app.dart to handle country codes (e.g., pt_PT -> Locale('pt', 'PT')) - Change Portuguese label from Portugal to Brasil in language settings --- CHANGELOG.md | 14 +++ go_backend/amazon.go | 31 ++++- go_backend/exports.go | 2 + go_backend/lyrics.go | 28 +++++ go_backend/qobuz.go | 31 ++++- go_backend/tidal.go | 31 ++++- lib/app.dart | 7 +- lib/l10n/app_localizations.dart | 54 +++++++++ lib/l10n/app_localizations_de.dart | 29 +++++ lib/l10n/app_localizations_en.dart | 29 +++++ lib/l10n/app_localizations_es.dart | 29 +++++ lib/l10n/app_localizations_fr.dart | 29 +++++ lib/l10n/app_localizations_hi.dart | 29 +++++ lib/l10n/app_localizations_id.dart | 29 +++++ lib/l10n/app_localizations_ja.dart | 29 +++++ lib/l10n/app_localizations_ko.dart | 29 +++++ lib/l10n/app_localizations_nl.dart | 29 +++++ lib/l10n/app_localizations_pt.dart | 29 +++++ lib/l10n/app_localizations_ru.dart | 29 +++++ lib/l10n/app_localizations_zh.dart | 29 +++++ lib/l10n/arb/app_en.arb | 20 ++++ lib/models/settings.dart | 4 + lib/models/settings.g.dart | 2 + lib/providers/download_queue_provider.dart | 2 + lib/providers/settings_provider.dart | 8 ++ .../settings/appearance_settings_page.dart | 2 +- .../settings/download_settings_page.dart | 112 +++++++++++++++++- lib/services/platform_bridge.dart | 8 ++ 28 files changed, 680 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37e62052..bd8f606f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [3.1.3] - 2026-01-19 + +### Added + +- **External LRC Lyrics File Support**: Option to save lyrics as separate .lrc files for compatibility with external music players + - New "Lyrics Mode" setting in Settings > Download > Lyrics section + - Three modes available: + - **Embed in file** (default): Lyrics stored inside FLAC metadata + - **External .lrc file**: Save lyrics as separate .lrc file next to audio file + - **Both**: Embed and save external .lrc file + - Perfect for players like Samsung Music that prefer external .lrc files + - LRC files include metadata headers (title, artist, by:SpotiFLAC-Mobile) + - Works with all download services (Tidal, Qobuz, Amazon) + ## [3.1.2] - 2026-01-19 ### Added diff --git a/go_backend/amazon.go b/go_backend/amazon.go index f5e6dc69..cb130bd4 100644 --- a/go_backend/amazon.go +++ b/go_backend/amazon.go @@ -580,13 +580,32 @@ func downloadFromAmazon(req DownloadRequest) (AmazonDownloadResult, error) { fmt.Printf("Warning: failed to embed metadata: %v\n", err) } - // Embed lyrics from parallel fetch + // Handle lyrics based on LyricsMode setting + // Mode: "embed" (default), "external" (.lrc file), "both" if req.EmbedLyrics && parallelResult != nil && parallelResult.LyricsLRC != "" { - GoLog("[Amazon] Embedding parallel-fetched lyrics (%d lines)...\n", len(parallelResult.LyricsData.Lines)) - if embedErr := EmbedLyrics(outputPath, parallelResult.LyricsLRC); embedErr != nil { - GoLog("[Amazon] Warning: failed to embed lyrics: %v\n", embedErr) - } else { - fmt.Println("[Amazon] Lyrics embedded successfully") + lyricsMode := req.LyricsMode + if lyricsMode == "" { + lyricsMode = "embed" // default + } + + // Save external .lrc file if mode is "external" or "both" + if lyricsMode == "external" || lyricsMode == "both" { + GoLog("[Amazon] Saving external LRC file...\n") + if lrcPath, lrcErr := SaveLRCFile(outputPath, parallelResult.LyricsLRC); lrcErr != nil { + GoLog("[Amazon] Warning: failed to save LRC file: %v\n", lrcErr) + } else { + GoLog("[Amazon] LRC file saved: %s\n", lrcPath) + } + } + + // Embed lyrics if mode is "embed" or "both" + if lyricsMode == "embed" || lyricsMode == "both" { + GoLog("[Amazon] Embedding parallel-fetched lyrics (%d lines)...\n", len(parallelResult.LyricsData.Lines)) + if embedErr := EmbedLyrics(outputPath, parallelResult.LyricsLRC); embedErr != nil { + GoLog("[Amazon] Warning: failed to embed lyrics: %v\n", embedErr) + } else { + fmt.Println("[Amazon] Lyrics embedded successfully") + } } } else if req.EmbedLyrics { fmt.Println("[Amazon] No lyrics available from parallel fetch") diff --git a/go_backend/exports.go b/go_backend/exports.go index 6d65ef0c..1468660f 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -161,6 +161,8 @@ type DownloadRequest struct { TidalID string `json:"tidal_id,omitempty"` QobuzID string `json:"qobuz_id,omitempty"` DeezerID string `json:"deezer_id,omitempty"` + // Lyrics mode: "embed" (default), "external" (.lrc file), "both" + LyricsMode string `json:"lyrics_mode,omitempty"` } // DownloadResponse represents the result of a download diff --git a/go_backend/lyrics.go b/go_backend/lyrics.go index 97254ff7..46d959d7 100644 --- a/go_backend/lyrics.go +++ b/go_backend/lyrics.go @@ -6,6 +6,8 @@ import ( "math" "net/http" "net/url" + "os" + "path/filepath" "regexp" "strconv" "strings" @@ -485,3 +487,29 @@ func simplifyTrackName(name string) string { return strings.TrimSpace(result) } + +// SaveLRCFile saves lyrics as a .lrc file next to the audio file +// audioFilePath: path to the audio file (e.g., /path/to/song.flac) +// lrcContent: the LRC format lyrics content +// Returns the path to the saved .lrc file, or error +func SaveLRCFile(audioFilePath, lrcContent string) (string, error) { + if lrcContent == "" { + return "", fmt.Errorf("empty LRC content") + } + + // Get the directory and base name without extension + dir := filepath.Dir(audioFilePath) + ext := filepath.Ext(audioFilePath) + baseName := strings.TrimSuffix(filepath.Base(audioFilePath), ext) + + // Create the .lrc file path + lrcFilePath := filepath.Join(dir, baseName+".lrc") + + // Write the LRC content to the file + if err := os.WriteFile(lrcFilePath, []byte(lrcContent), 0644); err != nil { + return "", fmt.Errorf("failed to write LRC file: %w", err) + } + + GoLog("[Lyrics] Saved LRC file: %s\n", lrcFilePath) + return lrcFilePath, nil +} diff --git a/go_backend/qobuz.go b/go_backend/qobuz.go index 5165c127..0adb4a9d 100644 --- a/go_backend/qobuz.go +++ b/go_backend/qobuz.go @@ -1135,13 +1135,32 @@ func downloadFromQobuz(req DownloadRequest) (QobuzDownloadResult, error) { fmt.Printf("Warning: failed to embed metadata: %v\n", err) } - // Embed lyrics from parallel fetch + // Handle lyrics based on LyricsMode setting + // Mode: "embed" (default), "external" (.lrc file), "both" if req.EmbedLyrics && parallelResult != nil && parallelResult.LyricsLRC != "" { - GoLog("[Qobuz] Embedding parallel-fetched lyrics (%d lines)...\n", len(parallelResult.LyricsData.Lines)) - if embedErr := EmbedLyrics(outputPath, parallelResult.LyricsLRC); embedErr != nil { - GoLog("[Qobuz] Warning: failed to embed lyrics: %v\n", embedErr) - } else { - fmt.Println("[Qobuz] Lyrics embedded successfully") + lyricsMode := req.LyricsMode + if lyricsMode == "" { + lyricsMode = "embed" // default + } + + // Save external .lrc file if mode is "external" or "both" + if lyricsMode == "external" || lyricsMode == "both" { + GoLog("[Qobuz] Saving external LRC file...\n") + if lrcPath, lrcErr := SaveLRCFile(outputPath, parallelResult.LyricsLRC); lrcErr != nil { + GoLog("[Qobuz] Warning: failed to save LRC file: %v\n", lrcErr) + } else { + GoLog("[Qobuz] LRC file saved: %s\n", lrcPath) + } + } + + // Embed lyrics if mode is "embed" or "both" + if lyricsMode == "embed" || lyricsMode == "both" { + GoLog("[Qobuz] Embedding parallel-fetched lyrics (%d lines)...\n", len(parallelResult.LyricsData.Lines)) + if embedErr := EmbedLyrics(outputPath, parallelResult.LyricsLRC); embedErr != nil { + GoLog("[Qobuz] Warning: failed to embed lyrics: %v\n", embedErr) + } else { + fmt.Println("[Qobuz] Lyrics embedded successfully") + } } } else if req.EmbedLyrics { fmt.Println("[Qobuz] No lyrics available from parallel fetch") diff --git a/go_backend/tidal.go b/go_backend/tidal.go index 45a92ca2..3b89015f 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -1733,13 +1733,32 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) { fmt.Printf("Warning: failed to embed metadata: %v\n", err) } - // Embed lyrics from parallel fetch + // Handle lyrics based on LyricsMode setting + // Mode: "embed" (default), "external" (.lrc file), "both" if req.EmbedLyrics && parallelResult != nil && parallelResult.LyricsLRC != "" { - GoLog("[Tidal] Embedding parallel-fetched lyrics (%d lines)...\n", len(parallelResult.LyricsData.Lines)) - if embedErr := EmbedLyrics(actualOutputPath, parallelResult.LyricsLRC); embedErr != nil { - GoLog("[Tidal] Warning: failed to embed lyrics: %v\n", embedErr) - } else { - fmt.Println("[Tidal] Lyrics embedded successfully") + lyricsMode := req.LyricsMode + if lyricsMode == "" { + lyricsMode = "embed" // default + } + + // Save external .lrc file if mode is "external" or "both" + if lyricsMode == "external" || lyricsMode == "both" { + GoLog("[Tidal] Saving external LRC file...\n") + if lrcPath, lrcErr := SaveLRCFile(actualOutputPath, parallelResult.LyricsLRC); lrcErr != nil { + GoLog("[Tidal] Warning: failed to save LRC file: %v\n", lrcErr) + } else { + GoLog("[Tidal] LRC file saved: %s\n", lrcPath) + } + } + + // Embed lyrics if mode is "embed" or "both" + if lyricsMode == "embed" || lyricsMode == "both" { + GoLog("[Tidal] Embedding parallel-fetched lyrics (%d lines)...\n", len(parallelResult.LyricsData.Lines)) + if embedErr := EmbedLyrics(actualOutputPath, parallelResult.LyricsLRC); embedErr != nil { + GoLog("[Tidal] Warning: failed to embed lyrics: %v\n", embedErr) + } else { + fmt.Println("[Tidal] Lyrics embedded successfully") + } } } else if req.EmbedLyrics { fmt.Println("[Tidal] No lyrics available from parallel fetch") diff --git a/lib/app.dart b/lib/app.dart index df0f2158..9d6c7b8b 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -36,7 +36,12 @@ class SpotiFLACApp extends ConsumerWidget { Locale? locale; if (localeString != 'system') { - locale = Locale(localeString); + if (localeString.contains('_')) { + final parts = localeString.split('_'); + locale = Locale(parts[0], parts[1]); + } else { + locale = Locale(localeString); + } } return DynamicColorWrapper( diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 05bcad9f..da89b490 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2612,6 +2612,60 @@ abstract class AppLocalizations { /// **'File Settings'** String get sectionFileSettings; + /// Settings section header + /// + /// In en, this message translates to: + /// **'Lyrics'** + String get sectionLyrics; + + /// Setting - how to save lyrics + /// + /// In en, this message translates to: + /// **'Lyrics Mode'** + String get lyricsMode; + + /// Lyrics mode picker description + /// + /// In en, this message translates to: + /// **'Choose how lyrics are saved with your downloads'** + String get lyricsModeDescription; + + /// Lyrics mode option - embed in audio file + /// + /// In en, this message translates to: + /// **'Embed in file'** + String get lyricsModeEmbed; + + /// Subtitle for embed option + /// + /// In en, this message translates to: + /// **'Lyrics stored inside FLAC metadata'** + String get lyricsModeEmbedSubtitle; + + /// Lyrics mode option - separate LRC file + /// + /// In en, this message translates to: + /// **'External .lrc file'** + String get lyricsModeExternal; + + /// Subtitle for external option + /// + /// In en, this message translates to: + /// **'Separate .lrc file for players like Samsung Music'** + String get lyricsModeExternalSubtitle; + + /// Lyrics mode option - embed and external + /// + /// In en, this message translates to: + /// **'Both'** + String get lyricsModeBoth; + + /// Subtitle for both option + /// + /// In en, this message translates to: + /// **'Embed and save .lrc file'** + String get lyricsModeBothSubtitle; + /// Settings section header /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index a02bc6c0..f9c1b25b 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1443,6 +1443,35 @@ class AppLocalizationsDe extends AppLocalizations { @override String get sectionFileSettings => 'File Settings'; + @override + String get sectionLyrics => 'Lyrics'; + + @override + String get lyricsMode => 'Lyrics Mode'; + + @override + String get lyricsModeDescription => + 'Choose how lyrics are saved with your downloads'; + + @override + String get lyricsModeEmbed => 'Embed in file'; + + @override + String get lyricsModeEmbedSubtitle => 'Lyrics stored inside FLAC metadata'; + + @override + String get lyricsModeExternal => 'External .lrc file'; + + @override + String get lyricsModeExternalSubtitle => + 'Separate .lrc file for players like Samsung Music'; + + @override + String get lyricsModeBoth => 'Both'; + + @override + String get lyricsModeBothSubtitle => 'Embed and save .lrc file'; + @override String get sectionColor => 'Color'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index eadd58ce..93573f3e 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1430,6 +1430,35 @@ class AppLocalizationsEn extends AppLocalizations { @override String get sectionFileSettings => 'File Settings'; + @override + String get sectionLyrics => 'Lyrics'; + + @override + String get lyricsMode => 'Lyrics Mode'; + + @override + String get lyricsModeDescription => + 'Choose how lyrics are saved with your downloads'; + + @override + String get lyricsModeEmbed => 'Embed in file'; + + @override + String get lyricsModeEmbedSubtitle => 'Lyrics stored inside FLAC metadata'; + + @override + String get lyricsModeExternal => 'External .lrc file'; + + @override + String get lyricsModeExternalSubtitle => + 'Separate .lrc file for players like Samsung Music'; + + @override + String get lyricsModeBoth => 'Both'; + + @override + String get lyricsModeBothSubtitle => 'Embed and save .lrc file'; + @override String get sectionColor => 'Color'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 2acbccf3..5a44e2d9 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1430,6 +1430,35 @@ class AppLocalizationsEs extends AppLocalizations { @override String get sectionFileSettings => 'File Settings'; + @override + String get sectionLyrics => 'Lyrics'; + + @override + String get lyricsMode => 'Lyrics Mode'; + + @override + String get lyricsModeDescription => + 'Choose how lyrics are saved with your downloads'; + + @override + String get lyricsModeEmbed => 'Embed in file'; + + @override + String get lyricsModeEmbedSubtitle => 'Lyrics stored inside FLAC metadata'; + + @override + String get lyricsModeExternal => 'External .lrc file'; + + @override + String get lyricsModeExternalSubtitle => + 'Separate .lrc file for players like Samsung Music'; + + @override + String get lyricsModeBoth => 'Both'; + + @override + String get lyricsModeBothSubtitle => 'Embed and save .lrc file'; + @override String get sectionColor => 'Color'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 77d394b8..d94be538 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1430,6 +1430,35 @@ class AppLocalizationsFr extends AppLocalizations { @override String get sectionFileSettings => 'File Settings'; + @override + String get sectionLyrics => 'Lyrics'; + + @override + String get lyricsMode => 'Lyrics Mode'; + + @override + String get lyricsModeDescription => + 'Choose how lyrics are saved with your downloads'; + + @override + String get lyricsModeEmbed => 'Embed in file'; + + @override + String get lyricsModeEmbedSubtitle => 'Lyrics stored inside FLAC metadata'; + + @override + String get lyricsModeExternal => 'External .lrc file'; + + @override + String get lyricsModeExternalSubtitle => + 'Separate .lrc file for players like Samsung Music'; + + @override + String get lyricsModeBoth => 'Both'; + + @override + String get lyricsModeBothSubtitle => 'Embed and save .lrc file'; + @override String get sectionColor => 'Color'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 640394a4..83569552 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -1430,6 +1430,35 @@ class AppLocalizationsHi extends AppLocalizations { @override String get sectionFileSettings => 'File Settings'; + @override + String get sectionLyrics => 'Lyrics'; + + @override + String get lyricsMode => 'Lyrics Mode'; + + @override + String get lyricsModeDescription => + 'Choose how lyrics are saved with your downloads'; + + @override + String get lyricsModeEmbed => 'Embed in file'; + + @override + String get lyricsModeEmbedSubtitle => 'Lyrics stored inside FLAC metadata'; + + @override + String get lyricsModeExternal => 'External .lrc file'; + + @override + String get lyricsModeExternalSubtitle => + 'Separate .lrc file for players like Samsung Music'; + + @override + String get lyricsModeBoth => 'Both'; + + @override + String get lyricsModeBothSubtitle => 'Embed and save .lrc file'; + @override String get sectionColor => 'Color'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index a15135e6..46ecfbf9 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -1440,6 +1440,35 @@ class AppLocalizationsId extends AppLocalizations { @override String get sectionFileSettings => 'Pengaturan File'; + @override + String get sectionLyrics => 'Lyrics'; + + @override + String get lyricsMode => 'Lyrics Mode'; + + @override + String get lyricsModeDescription => + 'Choose how lyrics are saved with your downloads'; + + @override + String get lyricsModeEmbed => 'Embed in file'; + + @override + String get lyricsModeEmbedSubtitle => 'Lyrics stored inside FLAC metadata'; + + @override + String get lyricsModeExternal => 'External .lrc file'; + + @override + String get lyricsModeExternalSubtitle => + 'Separate .lrc file for players like Samsung Music'; + + @override + String get lyricsModeBoth => 'Both'; + + @override + String get lyricsModeBothSubtitle => 'Embed and save .lrc file'; + @override String get sectionColor => 'Warna'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index e88e31e3..3f301bd2 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -1430,6 +1430,35 @@ class AppLocalizationsJa extends AppLocalizations { @override String get sectionFileSettings => 'File Settings'; + @override + String get sectionLyrics => 'Lyrics'; + + @override + String get lyricsMode => 'Lyrics Mode'; + + @override + String get lyricsModeDescription => + 'Choose how lyrics are saved with your downloads'; + + @override + String get lyricsModeEmbed => 'Embed in file'; + + @override + String get lyricsModeEmbedSubtitle => 'Lyrics stored inside FLAC metadata'; + + @override + String get lyricsModeExternal => 'External .lrc file'; + + @override + String get lyricsModeExternalSubtitle => + 'Separate .lrc file for players like Samsung Music'; + + @override + String get lyricsModeBoth => 'Both'; + + @override + String get lyricsModeBothSubtitle => 'Embed and save .lrc file'; + @override String get sectionColor => 'Color'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 20ab8701..aa747ec2 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -1430,6 +1430,35 @@ class AppLocalizationsKo extends AppLocalizations { @override String get sectionFileSettings => 'File Settings'; + @override + String get sectionLyrics => 'Lyrics'; + + @override + String get lyricsMode => 'Lyrics Mode'; + + @override + String get lyricsModeDescription => + 'Choose how lyrics are saved with your downloads'; + + @override + String get lyricsModeEmbed => 'Embed in file'; + + @override + String get lyricsModeEmbedSubtitle => 'Lyrics stored inside FLAC metadata'; + + @override + String get lyricsModeExternal => 'External .lrc file'; + + @override + String get lyricsModeExternalSubtitle => + 'Separate .lrc file for players like Samsung Music'; + + @override + String get lyricsModeBoth => 'Both'; + + @override + String get lyricsModeBothSubtitle => 'Embed and save .lrc file'; + @override String get sectionColor => 'Color'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index e91357a9..6d4d14da 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1430,6 +1430,35 @@ class AppLocalizationsNl extends AppLocalizations { @override String get sectionFileSettings => 'File Settings'; + @override + String get sectionLyrics => 'Lyrics'; + + @override + String get lyricsMode => 'Lyrics Mode'; + + @override + String get lyricsModeDescription => + 'Choose how lyrics are saved with your downloads'; + + @override + String get lyricsModeEmbed => 'Embed in file'; + + @override + String get lyricsModeEmbedSubtitle => 'Lyrics stored inside FLAC metadata'; + + @override + String get lyricsModeExternal => 'External .lrc file'; + + @override + String get lyricsModeExternalSubtitle => + 'Separate .lrc file for players like Samsung Music'; + + @override + String get lyricsModeBoth => 'Both'; + + @override + String get lyricsModeBothSubtitle => 'Embed and save .lrc file'; + @override String get sectionColor => 'Color'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 25a17d29..a700c861 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1430,6 +1430,35 @@ class AppLocalizationsPt extends AppLocalizations { @override String get sectionFileSettings => 'File Settings'; + @override + String get sectionLyrics => 'Lyrics'; + + @override + String get lyricsMode => 'Lyrics Mode'; + + @override + String get lyricsModeDescription => + 'Choose how lyrics are saved with your downloads'; + + @override + String get lyricsModeEmbed => 'Embed in file'; + + @override + String get lyricsModeEmbedSubtitle => 'Lyrics stored inside FLAC metadata'; + + @override + String get lyricsModeExternal => 'External .lrc file'; + + @override + String get lyricsModeExternalSubtitle => + 'Separate .lrc file for players like Samsung Music'; + + @override + String get lyricsModeBoth => 'Both'; + + @override + String get lyricsModeBothSubtitle => 'Embed and save .lrc file'; + @override String get sectionColor => 'Color'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 7aa28243..1158112b 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1458,6 +1458,35 @@ class AppLocalizationsRu extends AppLocalizations { @override String get sectionFileSettings => 'Настройки файла'; + @override + String get sectionLyrics => 'Lyrics'; + + @override + String get lyricsMode => 'Lyrics Mode'; + + @override + String get lyricsModeDescription => + 'Choose how lyrics are saved with your downloads'; + + @override + String get lyricsModeEmbed => 'Embed in file'; + + @override + String get lyricsModeEmbedSubtitle => 'Lyrics stored inside FLAC metadata'; + + @override + String get lyricsModeExternal => 'External .lrc file'; + + @override + String get lyricsModeExternalSubtitle => + 'Separate .lrc file for players like Samsung Music'; + + @override + String get lyricsModeBoth => 'Both'; + + @override + String get lyricsModeBothSubtitle => 'Embed and save .lrc file'; + @override String get sectionColor => 'Цвет'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index ac47c5fa..68f13857 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1430,6 +1430,35 @@ class AppLocalizationsZh extends AppLocalizations { @override String get sectionFileSettings => 'File Settings'; + @override + String get sectionLyrics => 'Lyrics'; + + @override + String get lyricsMode => 'Lyrics Mode'; + + @override + String get lyricsModeDescription => + 'Choose how lyrics are saved with your downloads'; + + @override + String get lyricsModeEmbed => 'Embed in file'; + + @override + String get lyricsModeEmbedSubtitle => 'Lyrics stored inside FLAC metadata'; + + @override + String get lyricsModeExternal => 'External .lrc file'; + + @override + String get lyricsModeExternalSubtitle => + 'Separate .lrc file for players like Samsung Music'; + + @override + String get lyricsModeBoth => 'Both'; + + @override + String get lyricsModeBothSubtitle => 'Embed and save .lrc file'; + @override String get sectionColor => 'Color'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index d54ab75d..8603a0b1 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1051,6 +1051,26 @@ "@sectionAudioQuality": {"description": "Settings section header"}, "sectionFileSettings": "File Settings", "@sectionFileSettings": {"description": "Settings section header"}, + "sectionLyrics": "Lyrics", + "@sectionLyrics": {"description": "Settings section header"}, + + "lyricsMode": "Lyrics Mode", + "@lyricsMode": {"description": "Setting - how to save lyrics"}, + "lyricsModeDescription": "Choose how lyrics are saved with your downloads", + "@lyricsModeDescription": {"description": "Lyrics mode picker description"}, + "lyricsModeEmbed": "Embed in file", + "@lyricsModeEmbed": {"description": "Lyrics mode option - embed in audio file"}, + "lyricsModeEmbedSubtitle": "Lyrics stored inside FLAC metadata", + "@lyricsModeEmbedSubtitle": {"description": "Subtitle for embed option"}, + "lyricsModeExternal": "External .lrc file", + "@lyricsModeExternal": {"description": "Lyrics mode option - separate LRC file"}, + "lyricsModeExternalSubtitle": "Separate .lrc file for players like Samsung Music", + "@lyricsModeExternalSubtitle": {"description": "Subtitle for external option"}, + "lyricsModeBoth": "Both", + "@lyricsModeBoth": {"description": "Lyrics mode option - embed and external"}, + "lyricsModeBothSubtitle": "Embed and save .lrc file", + "@lyricsModeBothSubtitle": {"description": "Subtitle for both option"}, + "sectionColor": "Color", "@sectionColor": {"description": "Settings section header"}, "sectionTheme": "Theme", diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 0ff45cdb..00e308d0 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -32,6 +32,7 @@ class AppSettings { final bool showExtensionStore; // Show Extension Store tab in navigation final String locale; // App language: 'system', 'en', 'id', etc. final bool enableMp3Option; // Enable MP3 quality option (default off, requires FFmpeg conversion) + final String lyricsMode; // embed, external, both - how to save lyrics const AppSettings({ this.defaultService = 'tidal', @@ -62,6 +63,7 @@ class AppSettings { this.showExtensionStore = true, // Default: show store this.locale = 'system', // Default: follow system language this.enableMp3Option = false, // Default: disabled + this.lyricsMode = 'embed', // Default: embed lyrics into file }); AppSettings copyWith({ @@ -94,6 +96,7 @@ class AppSettings { bool? showExtensionStore, String? locale, bool? enableMp3Option, + String? lyricsMode, }) { return AppSettings( defaultService: defaultService ?? this.defaultService, @@ -124,6 +127,7 @@ class AppSettings { showExtensionStore: showExtensionStore ?? this.showExtensionStore, locale: locale ?? this.locale, enableMp3Option: enableMp3Option ?? this.enableMp3Option, + lyricsMode: lyricsMode ?? this.lyricsMode, ); } diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index 96962fa7..5225c989 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -37,6 +37,7 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings( showExtensionStore: json['showExtensionStore'] as bool? ?? true, locale: json['locale'] as String? ?? 'system', enableMp3Option: json['enableMp3Option'] as bool? ?? false, + lyricsMode: json['lyricsMode'] as String? ?? 'embed', ); Map _$AppSettingsToJson(AppSettings instance) => @@ -69,4 +70,5 @@ Map _$AppSettingsToJson(AppSettings instance) => 'showExtensionStore': instance.showExtensionStore, 'locale': instance.locale, 'enableMp3Option': instance.enableMp3Option, + 'lyricsMode': instance.lyricsMode, }; diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index c78ab9c0..03f35234 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -1628,6 +1628,7 @@ class DownloadQueueNotifier extends Notifier { source: trackToDownload.source, // Pass extension ID that provided this track genre: genre, label: label, + lyricsMode: settings.lyricsMode, ); } else if (state.autoFallback) { _log.d('Using auto-fallback mode'); @@ -1655,6 +1656,7 @@ class DownloadQueueNotifier extends Notifier { trackToDownload.duration, // Duration in ms for verification genre: genre, label: label, + lyricsMode: settings.lyricsMode, ); } else { result = await PlatformBridge.downloadTrack( diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index bef594e9..2de3d839 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -92,6 +92,14 @@ class SettingsNotifier extends Notifier { _saveSettings(); } + void setLyricsMode(String mode) { + // Valid modes: embed, external, both + if (mode == 'embed' || mode == 'external' || mode == 'both') { + state = state.copyWith(lyricsMode: mode); + _saveSettings(); + } + } + void setMaxQualityCover(bool enabled) { state = state.copyWith(maxQualityCover: enabled); _saveSettings(); diff --git a/lib/screens/settings/appearance_settings_page.dart b/lib/screens/settings/appearance_settings_page.dart index e8c3b023..4e039a85 100644 --- a/lib/screens/settings/appearance_settings_page.dart +++ b/lib/screens/settings/appearance_settings_page.dart @@ -707,7 +707,7 @@ static const _allLanguages = [ ('ko', '한국어', Icons.language), ('nl', 'Nederlands', Icons.language), ('pt', 'Português', Icons.language), - ('pt_PT', 'Português (Portugal)', Icons.language), + ('pt_PT', 'Português (Brasil)', Icons.language), ('ru', 'Русский', Icons.language), ('zh', '简体中文', Icons.language), ('zh_CN', '简体中文 (中国)', Icons.language), diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index b0a73f98..14b782a2 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -169,14 +169,35 @@ class DownloadSettingsPage extends ConsumerWidget { ], ), ), - ], ], - ), + ], ), + ), - SliverToBoxAdapter( - child: SettingsSectionHeader(title: context.l10n.sectionFileSettings), + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionLyrics), + ), + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + SettingsItem( + icon: Icons.lyrics_outlined, + title: context.l10n.lyricsMode, + subtitle: _getLyricsModeLabel(context, settings.lyricsMode), + onTap: () => _showLyricsModePicker( + context, + ref, + settings.lyricsMode, + ), + showDivider: false, + ), + ], ), + ), + + SliverToBoxAdapter( + child: SettingsSectionHeader(title: context.l10n.sectionFileSettings), + ), SliverToBoxAdapter( child: SettingsGroup( children: [ @@ -606,6 +627,89 @@ class DownloadSettingsPage extends ConsumerWidget { } } + String _getLyricsModeLabel(BuildContext context, String mode) { + switch (mode) { + case 'external': + return context.l10n.lyricsModeExternal; + case 'both': + return context.l10n.lyricsModeBoth; + default: + return context.l10n.lyricsModeEmbed; + } + } + + void _showLyricsModePicker( + BuildContext context, + WidgetRef ref, + String current, + ) { + final colorScheme = Theme.of(context).colorScheme; + showModalBottomSheet( + context: context, + backgroundColor: colorScheme.surfaceContainerHigh, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(28)), + ), + builder: (context) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), + child: Text( + context.l10n.lyricsMode, + style: Theme.of( + context, + ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 16), + child: Text( + context.l10n.lyricsModeDescription, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ), + ListTile( + leading: const Icon(Icons.audiotrack), + title: Text(context.l10n.lyricsModeEmbed), + subtitle: Text(context.l10n.lyricsModeEmbedSubtitle), + trailing: current == 'embed' ? const Icon(Icons.check) : null, + onTap: () { + ref.read(settingsProvider.notifier).setLyricsMode('embed'); + Navigator.pop(context); + }, + ), + ListTile( + leading: const Icon(Icons.insert_drive_file_outlined), + title: Text(context.l10n.lyricsModeExternal), + subtitle: Text(context.l10n.lyricsModeExternalSubtitle), + trailing: current == 'external' ? const Icon(Icons.check) : null, + onTap: () { + ref.read(settingsProvider.notifier).setLyricsMode('external'); + Navigator.pop(context); + }, + ), + ListTile( + leading: const Icon(Icons.library_music_outlined), + title: Text(context.l10n.lyricsModeBoth), + subtitle: Text(context.l10n.lyricsModeBothSubtitle), + trailing: current == 'both' ? const Icon(Icons.check) : null, + onTap: () { + ref.read(settingsProvider.notifier).setLyricsMode('both'); + Navigator.pop(context); + }, + ), + const SizedBox(height: 16), + ], + ), + ), + ); + } + void _showFolderOrganizationPicker( BuildContext context, WidgetRef ref, diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index 91632c1c..8658a6d7 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -133,6 +133,8 @@ class PlatformBridge { String? genre, String? label, String? copyright, + // Lyrics mode: "embed" (default), "external" (.lrc file), "both" + String lyricsMode = 'embed', }) async { _log.i('downloadWithFallback: "$trackName" by $artistName (preferred: $preferredService)'); final request = jsonEncode({ @@ -159,6 +161,8 @@ class PlatformBridge { 'genre': genre ?? '', 'label': label ?? '', 'copyright': copyright ?? '', + // Lyrics mode + 'lyrics_mode': lyricsMode, }); final result = await _channel.invokeMethod('downloadWithFallback', request); @@ -658,6 +662,8 @@ class PlatformBridge { String? source, // Extension ID that provided this track (prioritize this extension) String? genre, String? label, + // Lyrics mode: "embed" (default), "external" (.lrc file), "both" + String lyricsMode = 'embed', }) async { _log.i('downloadWithExtensions: "$trackName" by $artistName${source != null ? ' (source: $source)' : ''}'); final request = jsonEncode({ @@ -682,6 +688,8 @@ class PlatformBridge { 'source': source ?? '', // Extension ID that provided this track 'genre': genre ?? '', 'label': label ?? '', + // Lyrics mode + 'lyrics_mode': lyricsMode, }); final result = await _channel.invokeMethod('downloadWithExtensions', request);