mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-05-13 04:24:45 +02:00
feat: remove Tidal built-in provider, add extension download dedup/ISRC/Lyrics APIs, and expand l10n/a11y
Remove Tidal from built-in provider registry (metadata, search, download, URL parsing) and delete tidal.go. Introduce extension runtime APIs for lyrics lookup (getLyricsLRC), ISRC existence check (checkISRCExists), and ISRC index management (addToISRCIndex). Refactor extension download response construction into normalizeExtensionDownloadResult/overlayExtensionDownloadMetadata helpers with AlreadyExists support and ISRC indexing. Switch download mirrors to DoRequestWithUserAgent for ISP blocking detection. Add 50+ new localization keys and accessibility labels across all supported locales.
This commit is contained in:
@@ -2897,14 +2897,6 @@ class MainActivity: FlutterFragmentActivity() {
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"getTidalMetadata" -> {
|
||||
val resourceType = call.argument<String>("resource_type") ?: ""
|
||||
val resourceId = call.argument<String>("resource_id") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
Gobackend.getTidalMetadata(resourceType, resourceId)
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"getProviderMetadata" -> {
|
||||
val providerId = call.argument<String>("provider_id") ?: ""
|
||||
val resourceType = call.argument<String>("resource_type") ?: ""
|
||||
@@ -2921,13 +2913,6 @@ class MainActivity: FlutterFragmentActivity() {
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"convertTidalToSpotifyDeezer" -> {
|
||||
val url = call.argument<String>("url") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
Gobackend.convertTidalToSpotifyDeezer(url)
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"searchDeezerByISRC" -> {
|
||||
val isrc = call.argument<String>("isrc") ?: ""
|
||||
val itemId = call.argument<String>("item_id") ?: ""
|
||||
|
||||
+17
-91
@@ -1165,10 +1165,18 @@ func DownloadWithFallback(requestJSON string) (string, error) {
|
||||
return errorResponse("Download cancelled")
|
||||
}
|
||||
|
||||
allServices := []string{"tidal", "qobuz"}
|
||||
allServices := make([]string, 0, len(getBuiltInProviderSpecs()))
|
||||
for _, spec := range getBuiltInProviderSpecs() {
|
||||
if spec.SupportsDownload {
|
||||
allServices = append(allServices, spec.ID)
|
||||
}
|
||||
}
|
||||
if len(allServices) == 0 {
|
||||
return errorResponse("No built-in download providers available")
|
||||
}
|
||||
preferredService := req.Service
|
||||
if !isBuiltInDownloadProvider(preferredService) {
|
||||
preferredService = "tidal"
|
||||
preferredService = allServices[0]
|
||||
}
|
||||
|
||||
GoLog("[DownloadWithFallback] Preferred service from request: '%s'\n", req.Service)
|
||||
@@ -1947,21 +1955,6 @@ func ClearTrackIDCache() {
|
||||
ClearTrackCache()
|
||||
}
|
||||
|
||||
func SearchTidalAll(query string, trackLimit, artistLimit int, filter string) (string, error) {
|
||||
downloader := NewTidalDownloader()
|
||||
results, err := downloader.SearchAll(query, trackLimit, artistLimit, filter)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(results)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(jsonBytes), nil
|
||||
}
|
||||
|
||||
func SearchQobuzAll(query string, trackLimit, artistLimit int, filter string) (string, error) {
|
||||
downloader := NewQobuzDownloader()
|
||||
results, err := downloader.SearchAll(query, trackLimit, artistLimit, filter)
|
||||
@@ -2249,7 +2242,7 @@ func getExtensionProviderMetadataResponse(
|
||||
"artist_info": map[string]interface{}{
|
||||
"id": artist.ID,
|
||||
"name": artist.Name,
|
||||
"images": tidalFirstNonEmpty(artist.HeaderImage, artist.ImageURL),
|
||||
"images": firstNonEmptyTrimmed(artist.HeaderImage, artist.ImageURL),
|
||||
"cover_url": artist.ImageURL,
|
||||
"header_image": artist.HeaderImage,
|
||||
"provider_id": artist.ProviderID,
|
||||
@@ -2284,34 +2277,13 @@ func getExtensionProviderMetadataResponse(
|
||||
}
|
||||
}
|
||||
|
||||
func GetTidalMetadata(resourceType, resourceID string) (string, error) {
|
||||
downloader := NewTidalDownloader()
|
||||
|
||||
var data interface{}
|
||||
var err error
|
||||
|
||||
switch resourceType {
|
||||
case "track":
|
||||
data, err = downloader.GetTrackMetadata(resourceID)
|
||||
case "album":
|
||||
data, err = downloader.GetAlbumMetadata(resourceID)
|
||||
case "artist":
|
||||
data, err = downloader.GetArtistMetadata(resourceID)
|
||||
case "playlist":
|
||||
data, err = downloader.GetPlaylistMetadata(resourceID)
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported Tidal resource type: %s", resourceType)
|
||||
func firstNonEmptyTrimmed(values ...string) string {
|
||||
for _, value := range values {
|
||||
if trimmed := strings.TrimSpace(value); trimmed != "" {
|
||||
return trimmed
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(jsonBytes), nil
|
||||
return ""
|
||||
}
|
||||
|
||||
func GetProviderMetadataJSON(providerID, resourceType, resourceID string) (string, error) {
|
||||
@@ -2380,25 +2352,6 @@ func ParseQobuzURLExport(url string) (string, error) {
|
||||
return string(jsonBytes), nil
|
||||
}
|
||||
|
||||
func ParseTidalURLExport(url string) (string, error) {
|
||||
resourceType, resourceID, err := parseTidalURL(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
result := map[string]string{
|
||||
"type": resourceType,
|
||||
"id": resourceID,
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(jsonBytes), nil
|
||||
}
|
||||
|
||||
func ParseProviderURLJSON(url string) (string, error) {
|
||||
parsers := []struct {
|
||||
providerID string
|
||||
@@ -2406,7 +2359,6 @@ func ParseProviderURLJSON(url string) (string, error) {
|
||||
}{
|
||||
{providerID: "deezer", parse: parseDeezerURL},
|
||||
{providerID: "qobuz", parse: parseQobuzURL},
|
||||
{providerID: "tidal", parse: parseTidalURL},
|
||||
}
|
||||
|
||||
for _, parser := range parsers {
|
||||
@@ -2431,32 +2383,6 @@ func ParseProviderURLJSON(url string) (string, error) {
|
||||
return "", fmt.Errorf("unsupported provider URL")
|
||||
}
|
||||
|
||||
func ConvertTidalToSpotifyDeezer(tidalURL string) (string, error) {
|
||||
client := NewSongLinkClient()
|
||||
availability, err := client.CheckAvailabilityFromURL(tidalURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
result := map[string]string{
|
||||
"spotify_id": availability.SpotifyID,
|
||||
"deezer_id": availability.DeezerID,
|
||||
"deezer_url": availability.DeezerURL,
|
||||
"spotify_url": "",
|
||||
}
|
||||
|
||||
if availability.SpotifyID != "" {
|
||||
result["spotify_url"] = "https://open.spotify.com/track/" + availability.SpotifyID
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(jsonBytes), nil
|
||||
}
|
||||
|
||||
func GetDeezerExtendedMetadata(trackID string) (string, error) {
|
||||
if trackID == "" {
|
||||
return "", fmt.Errorf("empty track ID")
|
||||
|
||||
+204
-203
@@ -109,19 +109,6 @@ type builtInProviderSpec struct {
|
||||
}
|
||||
|
||||
var builtInProviderRegistry = []builtInProviderSpec{
|
||||
{
|
||||
ID: "tidal",
|
||||
DisplayName: "Tidal",
|
||||
SupportsMetadata: true,
|
||||
SupportsDownload: true,
|
||||
SupportsSearch: true,
|
||||
GetMetadata: GetTidalMetadata,
|
||||
SearchAll: SearchTidalAll,
|
||||
SearchTracks: func(query string, limit int) ([]ExtTrackMetadata, error) {
|
||||
return NewTidalDownloader().SearchTracks(query, limit)
|
||||
},
|
||||
Download: downloadWithBuiltInTidal,
|
||||
},
|
||||
{
|
||||
ID: "qobuz",
|
||||
DisplayName: "Qobuz",
|
||||
@@ -185,26 +172,6 @@ func downloadWithBuiltInProvider(providerID string, req DownloadRequest) (Downlo
|
||||
return spec.Download(req)
|
||||
}
|
||||
|
||||
func downloadWithBuiltInTidal(req DownloadRequest) (DownloadResult, error) {
|
||||
result, err := downloadFromTidal(req)
|
||||
if err != nil {
|
||||
return DownloadResult{}, err
|
||||
}
|
||||
return DownloadResult{
|
||||
FilePath: result.FilePath,
|
||||
BitDepth: result.BitDepth,
|
||||
SampleRate: result.SampleRate,
|
||||
Title: result.Title,
|
||||
Artist: result.Artist,
|
||||
Album: result.Album,
|
||||
ReleaseDate: result.ReleaseDate,
|
||||
TrackNumber: result.TrackNumber,
|
||||
DiscNumber: result.DiscNumber,
|
||||
ISRC: result.ISRC,
|
||||
LyricsLRC: result.LyricsLRC,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func downloadWithBuiltInQobuz(req DownloadRequest) (DownloadResult, error) {
|
||||
result, err := downloadFromQobuz(req)
|
||||
if err != nil {
|
||||
@@ -226,6 +193,139 @@ func downloadWithBuiltInQobuz(req DownloadRequest) (DownloadResult, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func normalizeExtensionDownloadResult(result *ExtDownloadResult) (DownloadResult, bool) {
|
||||
if result == nil {
|
||||
return DownloadResult{}, false
|
||||
}
|
||||
|
||||
downloadResult := DownloadResult{
|
||||
FilePath: strings.TrimSpace(result.FilePath),
|
||||
BitDepth: result.BitDepth,
|
||||
SampleRate: result.SampleRate,
|
||||
Title: result.Title,
|
||||
Artist: result.Artist,
|
||||
Album: result.Album,
|
||||
ReleaseDate: result.ReleaseDate,
|
||||
TrackNumber: result.TrackNumber,
|
||||
TotalTracks: result.TotalTracks,
|
||||
DiscNumber: result.DiscNumber,
|
||||
TotalDiscs: result.TotalDiscs,
|
||||
ISRC: result.ISRC,
|
||||
CoverURL: result.CoverURL,
|
||||
Genre: result.Genre,
|
||||
Label: result.Label,
|
||||
Copyright: result.Copyright,
|
||||
Composer: result.Composer,
|
||||
LyricsLRC: result.LyricsLRC,
|
||||
DecryptionKey: result.DecryptionKey,
|
||||
Decryption: normalizeDownloadDecryptionInfo(result.Decryption, result.DecryptionKey),
|
||||
}
|
||||
|
||||
alreadyExists := result.AlreadyExists
|
||||
if strings.HasPrefix(downloadResult.FilePath, "EXISTS:") {
|
||||
alreadyExists = true
|
||||
downloadResult.FilePath = strings.TrimPrefix(downloadResult.FilePath, "EXISTS:")
|
||||
}
|
||||
|
||||
enrichResultQualityFromFile(&downloadResult)
|
||||
return downloadResult, alreadyExists
|
||||
}
|
||||
|
||||
func overlayExtensionDownloadMetadata(resp *DownloadResponse, result *ExtDownloadResult) {
|
||||
if resp == nil || result == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.TrimSpace(resp.Title) == "" && result.Title != "" {
|
||||
resp.Title = result.Title
|
||||
}
|
||||
if strings.TrimSpace(resp.Artist) == "" && result.Artist != "" {
|
||||
resp.Artist = result.Artist
|
||||
}
|
||||
if strings.TrimSpace(resp.Album) == "" && result.Album != "" {
|
||||
resp.Album = result.Album
|
||||
}
|
||||
if strings.TrimSpace(resp.AlbumArtist) == "" && result.AlbumArtist != "" {
|
||||
resp.AlbumArtist = result.AlbumArtist
|
||||
}
|
||||
if resp.TrackNumber == 0 && result.TrackNumber > 0 {
|
||||
resp.TrackNumber = result.TrackNumber
|
||||
}
|
||||
if resp.DiscNumber == 0 && result.DiscNumber > 0 {
|
||||
resp.DiscNumber = result.DiscNumber
|
||||
}
|
||||
if resp.TotalTracks == 0 && result.TotalTracks > 0 {
|
||||
resp.TotalTracks = result.TotalTracks
|
||||
}
|
||||
if resp.TotalDiscs == 0 && result.TotalDiscs > 0 {
|
||||
resp.TotalDiscs = result.TotalDiscs
|
||||
}
|
||||
if strings.TrimSpace(resp.ReleaseDate) == "" && result.ReleaseDate != "" {
|
||||
resp.ReleaseDate = result.ReleaseDate
|
||||
}
|
||||
if strings.TrimSpace(resp.CoverURL) == "" && result.CoverURL != "" {
|
||||
resp.CoverURL = result.CoverURL
|
||||
}
|
||||
if strings.TrimSpace(resp.ISRC) == "" && result.ISRC != "" {
|
||||
resp.ISRC = result.ISRC
|
||||
}
|
||||
if strings.TrimSpace(resp.Genre) == "" && result.Genre != "" {
|
||||
resp.Genre = result.Genre
|
||||
}
|
||||
if strings.TrimSpace(resp.Label) == "" && result.Label != "" {
|
||||
resp.Label = result.Label
|
||||
}
|
||||
if strings.TrimSpace(resp.Copyright) == "" && result.Copyright != "" {
|
||||
resp.Copyright = result.Copyright
|
||||
}
|
||||
if strings.TrimSpace(resp.Composer) == "" && result.Composer != "" {
|
||||
resp.Composer = result.Composer
|
||||
}
|
||||
if result.LyricsLRC != "" {
|
||||
resp.LyricsLRC = result.LyricsLRC
|
||||
}
|
||||
if result.DecryptionKey != "" {
|
||||
resp.DecryptionKey = result.DecryptionKey
|
||||
}
|
||||
if normalized := normalizeDownloadDecryptionInfo(result.Decryption, result.DecryptionKey); normalized != nil {
|
||||
resp.Decryption = normalized
|
||||
}
|
||||
}
|
||||
|
||||
func applyExtensionRequestFallbacks(resp *DownloadResponse, req DownloadRequest) {
|
||||
if resp == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if req.AlbumName != "" && resp.Album == "" {
|
||||
resp.Album = req.AlbumName
|
||||
}
|
||||
if req.AlbumArtist != "" && resp.AlbumArtist == "" {
|
||||
resp.AlbumArtist = req.AlbumArtist
|
||||
}
|
||||
if req.ReleaseDate != "" && resp.ReleaseDate == "" {
|
||||
resp.ReleaseDate = req.ReleaseDate
|
||||
}
|
||||
if req.ISRC != "" && resp.ISRC == "" {
|
||||
resp.ISRC = req.ISRC
|
||||
}
|
||||
if req.TrackNumber > 0 && resp.TrackNumber == 0 {
|
||||
resp.TrackNumber = req.TrackNumber
|
||||
}
|
||||
if req.TotalTracks > 0 && resp.TotalTracks == 0 {
|
||||
resp.TotalTracks = req.TotalTracks
|
||||
}
|
||||
if req.DiscNumber > 0 && resp.DiscNumber == 0 {
|
||||
resp.DiscNumber = req.DiscNumber
|
||||
}
|
||||
if req.TotalDiscs > 0 && resp.TotalDiscs == 0 {
|
||||
resp.TotalDiscs = req.TotalDiscs
|
||||
}
|
||||
if req.CoverURL != "" && resp.CoverURL == "" {
|
||||
resp.CoverURL = req.CoverURL
|
||||
}
|
||||
}
|
||||
|
||||
func shouldStopProviderFallback(availability *ExtAvailabilityResult) bool {
|
||||
return availability != nil && availability.SkipFallback
|
||||
}
|
||||
@@ -262,12 +362,13 @@ type DownloadDecryptionInfo struct {
|
||||
}
|
||||
|
||||
type ExtDownloadResult struct {
|
||||
Success bool `json:"success"`
|
||||
FilePath string `json:"file_path,omitempty"`
|
||||
BitDepth int `json:"bit_depth,omitempty"`
|
||||
SampleRate int `json:"sample_rate,omitempty"`
|
||||
ErrorMessage string `json:"error_message,omitempty"`
|
||||
ErrorType string `json:"error_type,omitempty"`
|
||||
Success bool `json:"success"`
|
||||
FilePath string `json:"file_path,omitempty"`
|
||||
AlreadyExists bool `json:"already_exists,omitempty"`
|
||||
BitDepth int `json:"bit_depth,omitempty"`
|
||||
SampleRate int `json:"sample_rate,omitempty"`
|
||||
ErrorMessage string `json:"error_message,omitempty"`
|
||||
ErrorType string `json:"error_type,omitempty"`
|
||||
|
||||
Title string `json:"title,omitempty"`
|
||||
Artist string `json:"artist,omitempty"`
|
||||
@@ -1728,81 +1829,25 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
|
||||
}
|
||||
|
||||
if err == nil && result.Success {
|
||||
resp := &DownloadResponse{
|
||||
Success: true,
|
||||
Message: "Downloaded from " + req.Source,
|
||||
FilePath: result.FilePath,
|
||||
ActualBitDepth: result.BitDepth,
|
||||
ActualSampleRate: result.SampleRate,
|
||||
Service: req.Source,
|
||||
Genre: req.Genre,
|
||||
Label: req.Label,
|
||||
Copyright: req.Copyright,
|
||||
DecryptionKey: result.DecryptionKey,
|
||||
Decryption: normalizeDownloadDecryptionInfo(result.Decryption, result.DecryptionKey),
|
||||
}
|
||||
|
||||
if req.EmbedMetadata && (req.Genre != "" || req.Label != "") && canEmbedGenreLabel(result.FilePath) {
|
||||
if err := EmbedGenreLabel(result.FilePath, req.Genre, req.Label); err != nil {
|
||||
GoLog("[DownloadWithExtensionFallback] Warning: failed to embed genre/label: %v\n", err)
|
||||
} else {
|
||||
GoLog("[DownloadWithExtensionFallback] Embedded genre=%q label=%q\n", req.Genre, req.Label)
|
||||
}
|
||||
} else if req.EmbedMetadata && (req.Genre != "" || req.Label != "") {
|
||||
GoLog("[DownloadWithExtensionFallback] Skipping genre/label embed for non-local output path: %q\n", result.FilePath)
|
||||
normalizedResult, alreadyExists := normalizeExtensionDownloadResult(result)
|
||||
message := "Downloaded from " + req.Source
|
||||
if alreadyExists {
|
||||
message = "File already exists"
|
||||
}
|
||||
|
||||
resp := buildDownloadSuccessResponse(
|
||||
req,
|
||||
normalizedResult,
|
||||
req.Source,
|
||||
message,
|
||||
normalizedResult.FilePath,
|
||||
alreadyExists,
|
||||
)
|
||||
overlayExtensionDownloadMetadata(&resp, result)
|
||||
if ext.Manifest.SkipMetadataEnrichment {
|
||||
resp.SkipMetadataEnrichment = true
|
||||
if result.Title != "" {
|
||||
resp.Title = result.Title
|
||||
}
|
||||
if result.Artist != "" {
|
||||
resp.Artist = result.Artist
|
||||
}
|
||||
if result.Album != "" {
|
||||
resp.Album = result.Album
|
||||
}
|
||||
if result.AlbumArtist != "" {
|
||||
resp.AlbumArtist = result.AlbumArtist
|
||||
}
|
||||
if result.TrackNumber > 0 {
|
||||
resp.TrackNumber = result.TrackNumber
|
||||
}
|
||||
if result.DiscNumber > 0 {
|
||||
resp.DiscNumber = result.DiscNumber
|
||||
}
|
||||
if result.TotalTracks > 0 {
|
||||
resp.TotalTracks = result.TotalTracks
|
||||
}
|
||||
if result.TotalDiscs > 0 {
|
||||
resp.TotalDiscs = result.TotalDiscs
|
||||
}
|
||||
if result.ReleaseDate != "" {
|
||||
resp.ReleaseDate = result.ReleaseDate
|
||||
}
|
||||
if result.CoverURL != "" {
|
||||
resp.CoverURL = result.CoverURL
|
||||
}
|
||||
if result.ISRC != "" {
|
||||
resp.ISRC = result.ISRC
|
||||
}
|
||||
if result.Genre != "" {
|
||||
resp.Genre = result.Genre
|
||||
}
|
||||
if result.Label != "" {
|
||||
resp.Label = result.Label
|
||||
}
|
||||
if result.Copyright != "" {
|
||||
resp.Copyright = result.Copyright
|
||||
}
|
||||
if result.Composer != "" {
|
||||
resp.Composer = result.Composer
|
||||
}
|
||||
if result.LyricsLRC != "" {
|
||||
resp.LyricsLRC = result.LyricsLRC
|
||||
}
|
||||
}
|
||||
applyExtensionRequestFallbacks(&resp, req)
|
||||
|
||||
if req.TrackName != "" && resp.Title == "" {
|
||||
resp.Title = req.TrackName
|
||||
@@ -1810,38 +1855,31 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
|
||||
if req.ArtistName != "" && resp.Artist == "" {
|
||||
resp.Artist = req.ArtistName
|
||||
}
|
||||
if req.AlbumName != "" && resp.Album == "" {
|
||||
resp.Album = req.AlbumName
|
||||
}
|
||||
if req.AlbumArtist != "" && resp.AlbumArtist == "" {
|
||||
resp.AlbumArtist = req.AlbumArtist
|
||||
}
|
||||
if req.ReleaseDate != "" && resp.ReleaseDate == "" {
|
||||
resp.ReleaseDate = req.ReleaseDate
|
||||
}
|
||||
if req.ISRC != "" && resp.ISRC == "" {
|
||||
resp.ISRC = req.ISRC
|
||||
}
|
||||
if req.TrackNumber > 0 && resp.TrackNumber == 0 {
|
||||
resp.TrackNumber = req.TrackNumber
|
||||
}
|
||||
if req.DiscNumber > 0 && resp.DiscNumber == 0 {
|
||||
resp.DiscNumber = req.DiscNumber
|
||||
}
|
||||
if req.TotalTracks > 0 && resp.TotalTracks == 0 {
|
||||
resp.TotalTracks = req.TotalTracks
|
||||
}
|
||||
if req.TotalDiscs > 0 && resp.TotalDiscs == 0 {
|
||||
resp.TotalDiscs = req.TotalDiscs
|
||||
}
|
||||
if req.CoverURL != "" && resp.CoverURL == "" {
|
||||
resp.CoverURL = req.CoverURL
|
||||
}
|
||||
if req.Composer != "" && resp.Composer == "" {
|
||||
resp.Composer = req.Composer
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
if !alreadyExists && req.EmbedMetadata && (req.Genre != "" || req.Label != "") && canEmbedGenreLabel(normalizedResult.FilePath) {
|
||||
if err := EmbedGenreLabel(normalizedResult.FilePath, req.Genre, req.Label); err != nil {
|
||||
GoLog("[DownloadWithExtensionFallback] Warning: failed to embed genre/label: %v\n", err)
|
||||
} else {
|
||||
GoLog("[DownloadWithExtensionFallback] Embedded genre=%q label=%q\n", req.Genre, req.Label)
|
||||
}
|
||||
} else if !alreadyExists && req.EmbedMetadata && (req.Genre != "" || req.Label != "") {
|
||||
GoLog("[DownloadWithExtensionFallback] Skipping genre/label embed for non-local output path: %q\n", normalizedResult.FilePath)
|
||||
}
|
||||
|
||||
if !alreadyExists && !isFDOutput(req.OutputFD) && strings.TrimSpace(req.OutputDir) != "" {
|
||||
indexISRC := strings.TrimSpace(resp.ISRC)
|
||||
if indexISRC == "" {
|
||||
indexISRC = strings.TrimSpace(req.ISRC)
|
||||
}
|
||||
if indexISRC != "" && strings.TrimSpace(resp.FilePath) != "" {
|
||||
AddToISRCIndex(req.OutputDir, indexISRC, resp.FilePath)
|
||||
}
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -2003,84 +2041,47 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
|
||||
}
|
||||
|
||||
if err == nil && result.Success {
|
||||
resp := &DownloadResponse{
|
||||
Success: true,
|
||||
Message: "Downloaded from " + providerID,
|
||||
FilePath: result.FilePath,
|
||||
ActualBitDepth: result.BitDepth,
|
||||
ActualSampleRate: result.SampleRate,
|
||||
Service: providerID,
|
||||
Genre: req.Genre,
|
||||
Label: req.Label,
|
||||
Copyright: req.Copyright,
|
||||
DecryptionKey: result.DecryptionKey,
|
||||
Decryption: normalizeDownloadDecryptionInfo(result.Decryption, result.DecryptionKey),
|
||||
normalizedResult, alreadyExists := normalizeExtensionDownloadResult(result)
|
||||
message := "Downloaded from " + providerID
|
||||
if alreadyExists {
|
||||
message = "File already exists"
|
||||
}
|
||||
|
||||
if req.EmbedMetadata && (req.Genre != "" || req.Label != "") && canEmbedGenreLabel(result.FilePath) {
|
||||
if err := EmbedGenreLabel(result.FilePath, req.Genre, req.Label); err != nil {
|
||||
resp := buildDownloadSuccessResponse(
|
||||
req,
|
||||
normalizedResult,
|
||||
providerID,
|
||||
message,
|
||||
normalizedResult.FilePath,
|
||||
alreadyExists,
|
||||
)
|
||||
overlayExtensionDownloadMetadata(&resp, result)
|
||||
if ext.Manifest.SkipMetadataEnrichment {
|
||||
resp.SkipMetadataEnrichment = true
|
||||
}
|
||||
applyExtensionRequestFallbacks(&resp, req)
|
||||
|
||||
if !alreadyExists && req.EmbedMetadata && (req.Genre != "" || req.Label != "") && canEmbedGenreLabel(normalizedResult.FilePath) {
|
||||
if err := EmbedGenreLabel(normalizedResult.FilePath, req.Genre, req.Label); err != nil {
|
||||
GoLog("[DownloadWithExtensionFallback] Warning: failed to embed genre/label: %v\n", err)
|
||||
} else {
|
||||
GoLog("[DownloadWithExtensionFallback] Embedded genre=%q label=%q\n", req.Genre, req.Label)
|
||||
}
|
||||
} else if req.EmbedMetadata && (req.Genre != "" || req.Label != "") {
|
||||
GoLog("[DownloadWithExtensionFallback] Skipping genre/label embed for non-local output path: %q\n", result.FilePath)
|
||||
} else if !alreadyExists && req.EmbedMetadata && (req.Genre != "" || req.Label != "") {
|
||||
GoLog("[DownloadWithExtensionFallback] Skipping genre/label embed for non-local output path: %q\n", normalizedResult.FilePath)
|
||||
}
|
||||
|
||||
if ext.Manifest.SkipMetadataEnrichment {
|
||||
resp.SkipMetadataEnrichment = true
|
||||
if result.Title != "" {
|
||||
resp.Title = result.Title
|
||||
if !alreadyExists && !isFDOutput(req.OutputFD) && strings.TrimSpace(req.OutputDir) != "" {
|
||||
indexISRC := strings.TrimSpace(resp.ISRC)
|
||||
if indexISRC == "" {
|
||||
indexISRC = strings.TrimSpace(req.ISRC)
|
||||
}
|
||||
if result.Artist != "" {
|
||||
resp.Artist = result.Artist
|
||||
}
|
||||
if result.Album != "" {
|
||||
resp.Album = result.Album
|
||||
}
|
||||
if result.AlbumArtist != "" {
|
||||
resp.AlbumArtist = result.AlbumArtist
|
||||
}
|
||||
if result.TrackNumber > 0 {
|
||||
resp.TrackNumber = result.TrackNumber
|
||||
}
|
||||
if result.DiscNumber > 0 {
|
||||
resp.DiscNumber = result.DiscNumber
|
||||
}
|
||||
if result.ReleaseDate != "" {
|
||||
resp.ReleaseDate = result.ReleaseDate
|
||||
}
|
||||
if result.CoverURL != "" {
|
||||
resp.CoverURL = result.CoverURL
|
||||
}
|
||||
if result.ISRC != "" {
|
||||
resp.ISRC = result.ISRC
|
||||
if indexISRC != "" && strings.TrimSpace(resp.FilePath) != "" {
|
||||
AddToISRCIndex(req.OutputDir, indexISRC, resp.FilePath)
|
||||
}
|
||||
}
|
||||
|
||||
if req.AlbumName != "" && resp.Album == "" {
|
||||
resp.Album = req.AlbumName
|
||||
}
|
||||
if req.AlbumArtist != "" && resp.AlbumArtist == "" {
|
||||
resp.AlbumArtist = req.AlbumArtist
|
||||
}
|
||||
if req.ReleaseDate != "" && resp.ReleaseDate == "" {
|
||||
resp.ReleaseDate = req.ReleaseDate
|
||||
}
|
||||
if req.ISRC != "" && resp.ISRC == "" {
|
||||
resp.ISRC = req.ISRC
|
||||
}
|
||||
if req.TrackNumber > 0 && resp.TrackNumber == 0 {
|
||||
resp.TrackNumber = req.TrackNumber
|
||||
}
|
||||
if req.DiscNumber > 0 && resp.DiscNumber == 0 {
|
||||
resp.DiscNumber = req.DiscNumber
|
||||
}
|
||||
if req.CoverURL != "" && resp.CoverURL == "" {
|
||||
resp.CoverURL = req.CoverURL
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -11,9 +11,9 @@ func TestSetMetadataProviderPriorityPreservesExplicitProvidersOnly(t *testing.T)
|
||||
original := GetMetadataProviderPriority()
|
||||
defer SetMetadataProviderPriority(original)
|
||||
|
||||
SetMetadataProviderPriority([]string{"tidal"})
|
||||
SetMetadataProviderPriority([]string{"qobuz"})
|
||||
got := GetMetadataProviderPriority()
|
||||
want := []string{"tidal"}
|
||||
want := []string{"qobuz"}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("unexpected priority length: got %v want %v", got, want)
|
||||
}
|
||||
@@ -28,7 +28,7 @@ func TestSetExtensionFallbackProviderIDsSkipsBuiltInsAndDuplicates(t *testing.T)
|
||||
original := GetExtensionFallbackProviderIDs()
|
||||
defer SetExtensionFallbackProviderIDs(original)
|
||||
|
||||
SetExtensionFallbackProviderIDs([]string{"ext-a", "tidal", "ext-a", " ext-b "})
|
||||
SetExtensionFallbackProviderIDs([]string{"ext-a", "qobuz", "ext-a", " ext-b "})
|
||||
|
||||
got := GetExtensionFallbackProviderIDs()
|
||||
want := []string{"ext-a", "ext-b"}
|
||||
@@ -255,7 +255,7 @@ func TestSearchTracksWithMetadataProvidersUsesPriorityAndDedupes(t *testing.T) {
|
||||
searchBuiltInMetadataTracksFunc = originalSearch
|
||||
}()
|
||||
|
||||
SetMetadataProviderPriority([]string{"qobuz", "tidal"})
|
||||
SetMetadataProviderPriority([]string{"qobuz"})
|
||||
|
||||
var calls []string
|
||||
searchBuiltInMetadataTracksFunc = func(providerID, query string, limit int) ([]ExtTrackMetadata, error) {
|
||||
@@ -264,11 +264,8 @@ func TestSearchTracksWithMetadataProvidersUsesPriorityAndDedupes(t *testing.T) {
|
||||
case "qobuz":
|
||||
return []ExtTrackMetadata{
|
||||
{ProviderID: "qobuz", SpotifyID: "qobuz:1", ISRC: "AAA111", Name: "First"},
|
||||
}, nil
|
||||
case "tidal":
|
||||
return []ExtTrackMetadata{
|
||||
{ProviderID: "tidal", SpotifyID: "tidal:2", ISRC: "AAA111", Name: "Duplicate"},
|
||||
{ProviderID: "tidal", SpotifyID: "tidal:3", ISRC: "BBB222", Name: "Second"},
|
||||
{ProviderID: "qobuz", SpotifyID: "qobuz:2", ISRC: "AAA111", Name: "Duplicate"},
|
||||
{ProviderID: "qobuz", SpotifyID: "qobuz:3", ISRC: "BBB222", Name: "Second"},
|
||||
}, nil
|
||||
default:
|
||||
return nil, nil
|
||||
@@ -283,10 +280,10 @@ func TestSearchTracksWithMetadataProvidersUsesPriorityAndDedupes(t *testing.T) {
|
||||
if len(tracks) != 2 {
|
||||
t.Fatalf("unexpected track count: got %d want 2", len(tracks))
|
||||
}
|
||||
if tracks[0].ProviderID != "qobuz" || tracks[1].ProviderID != "tidal" {
|
||||
if tracks[0].ProviderID != "qobuz" || tracks[1].ProviderID != "qobuz" {
|
||||
t.Fatalf("unexpected track provider order: %+v", tracks)
|
||||
}
|
||||
if len(calls) != 2 || calls[0] != "qobuz" || calls[1] != "tidal" {
|
||||
if len(calls) != 1 || calls[0] != "qobuz" {
|
||||
t.Fatalf("unexpected provider call order: %v", calls)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,6 +390,81 @@ func (r *extensionRuntime) RegisterGoBackendAPIs(vm *goja.Runtime) {
|
||||
})
|
||||
})
|
||||
|
||||
obj.Set("getLyricsLRC", func(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) < 3 {
|
||||
return vm.ToValue(map[string]interface{}{
|
||||
"error": "spotifyID, trackName, and artistName are required",
|
||||
})
|
||||
}
|
||||
|
||||
spotifyID := strings.TrimSpace(call.Arguments[0].String())
|
||||
trackName := strings.TrimSpace(call.Arguments[1].String())
|
||||
artistName := strings.TrimSpace(call.Arguments[2].String())
|
||||
filePath := ""
|
||||
if len(call.Arguments) > 3 && !goja.IsUndefined(call.Arguments[3]) && !goja.IsNull(call.Arguments[3]) {
|
||||
filePath = strings.TrimSpace(call.Arguments[3].String())
|
||||
}
|
||||
var durationMs int64
|
||||
if len(call.Arguments) > 4 && !goja.IsUndefined(call.Arguments[4]) && !goja.IsNull(call.Arguments[4]) {
|
||||
durationMs = call.Arguments[4].ToInteger()
|
||||
}
|
||||
|
||||
lyrics, err := GetLyricsLRC(spotifyID, trackName, artistName, filePath, durationMs)
|
||||
if err != nil {
|
||||
return vm.ToValue(map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return vm.ToValue(map[string]interface{}{
|
||||
"lyrics": lyrics,
|
||||
})
|
||||
})
|
||||
|
||||
obj.Set("checkISRCExists", func(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) < 2 {
|
||||
return vm.ToValue(map[string]interface{}{
|
||||
"error": "outputDir and isrc are required",
|
||||
})
|
||||
}
|
||||
|
||||
outputDir := strings.TrimSpace(call.Arguments[0].String())
|
||||
isrc := strings.TrimSpace(call.Arguments[1].String())
|
||||
if outputDir == "" || isrc == "" {
|
||||
return vm.ToValue(map[string]interface{}{
|
||||
"error": "outputDir and isrc are required",
|
||||
})
|
||||
}
|
||||
|
||||
filePath, exists := checkISRCExistsInternal(outputDir, isrc)
|
||||
return vm.ToValue(map[string]interface{}{
|
||||
"exists": exists,
|
||||
"filePath": filePath,
|
||||
})
|
||||
})
|
||||
|
||||
obj.Set("addToISRCIndex", func(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) < 3 {
|
||||
return vm.ToValue(map[string]interface{}{
|
||||
"error": "outputDir, isrc, and filePath are required",
|
||||
})
|
||||
}
|
||||
|
||||
outputDir := strings.TrimSpace(call.Arguments[0].String())
|
||||
isrc := strings.TrimSpace(call.Arguments[1].String())
|
||||
filePath := strings.TrimSpace(call.Arguments[2].String())
|
||||
if outputDir == "" || isrc == "" || filePath == "" {
|
||||
return vm.ToValue(map[string]interface{}{
|
||||
"error": "outputDir, isrc, and filePath are required",
|
||||
})
|
||||
}
|
||||
|
||||
AddToISRCIndex(outputDir, isrc, filePath)
|
||||
return vm.ToValue(map[string]interface{}{
|
||||
"success": true,
|
||||
})
|
||||
})
|
||||
|
||||
obj.Set("buildFilename", func(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) < 2 {
|
||||
return vm.ToValue("")
|
||||
|
||||
+2
-32
@@ -8,9 +8,8 @@ import (
|
||||
)
|
||||
|
||||
type TrackIDCacheEntry struct {
|
||||
TidalTrackID int64
|
||||
QobuzTrackID int64
|
||||
ExpiresAt time.Time
|
||||
QobuzTrackID int64
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
type TrackIDCache struct {
|
||||
@@ -68,25 +67,6 @@ func (c *TrackIDCache) pruneExpiredLocked(now time.Time) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TrackIDCache) SetTidal(isrc string, trackID int64) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
entry, exists := c.cache[isrc]
|
||||
if !exists {
|
||||
entry = &TrackIDCacheEntry{}
|
||||
c.cache[isrc] = entry
|
||||
}
|
||||
entry.TidalTrackID = trackID
|
||||
now := time.Now()
|
||||
entry.ExpiresAt = now.Add(c.ttl)
|
||||
|
||||
if c.cleanupInterval > 0 && (c.lastCleanup.IsZero() || now.Sub(c.lastCleanup) >= c.cleanupInterval) {
|
||||
c.pruneExpiredLocked(now)
|
||||
c.lastCleanup = now
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TrackIDCache) SetQobuz(isrc string, trackID int64) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
@@ -211,8 +191,6 @@ func PreWarmTrackCache(requests []PreWarmCacheRequest) {
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
switch r.Service {
|
||||
case "tidal":
|
||||
preWarmTidalCache(r.ISRC, r.TrackName, r.ArtistName)
|
||||
case "qobuz":
|
||||
preWarmQobuzCache(r.ISRC, r.SpotifyID)
|
||||
}
|
||||
@@ -222,14 +200,6 @@ func PreWarmTrackCache(requests []PreWarmCacheRequest) {
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func preWarmTidalCache(isrc, _, _ string) {
|
||||
downloader := NewTidalDownloader()
|
||||
track, err := downloader.SearchTrackByISRC(isrc)
|
||||
if err == nil && track != nil {
|
||||
GetTrackIDCache().SetTidal(isrc, track.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// preWarmQobuzCache tries to get Qobuz Track ID in the following order:
|
||||
// 1. From SongLink (fast, no Qobuz API call needed)
|
||||
// 2. Direct ISRC search on Qobuz API (slower, may fail if ISRC not in Qobuz database)
|
||||
|
||||
-2547
File diff suppressed because it is too large
Load Diff
@@ -1,222 +0,0 @@
|
||||
package gobackend
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseTidalURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantType string
|
||||
wantID string
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "track url",
|
||||
input: "https://tidal.com/track/77616174",
|
||||
wantType: "track",
|
||||
wantID: "77616174",
|
||||
},
|
||||
{
|
||||
name: "browse album url",
|
||||
input: "https://listen.tidal.com/browse/album/77616169",
|
||||
wantType: "album",
|
||||
wantID: "77616169",
|
||||
},
|
||||
{
|
||||
name: "artist url",
|
||||
input: "https://www.tidal.com/artist/3852143",
|
||||
wantType: "artist",
|
||||
wantID: "3852143",
|
||||
},
|
||||
{
|
||||
name: "playlist url",
|
||||
input: "https://tidal.com/playlist/edf3b7d2-cb42-41d7-93c0-afa2a395521b",
|
||||
wantType: "playlist",
|
||||
wantID: "edf3b7d2-cb42-41d7-93c0-afa2a395521b",
|
||||
},
|
||||
{
|
||||
name: "unsupported host",
|
||||
input: "https://example.com/track/123",
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
gotType, gotID, err := parseTidalURL(test.input)
|
||||
if test.expectErr {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got none")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if gotType != test.wantType || gotID != test.wantID {
|
||||
t.Fatalf("parseTidalURL(%q) = (%q, %q), want (%q, %q)", test.input, gotType, gotID, test.wantType, test.wantID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTidalRequestTrackID(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want int64
|
||||
ok bool
|
||||
}{
|
||||
{input: "40681594", want: 40681594, ok: true},
|
||||
{input: "tidal:40681594", want: 40681594, ok: true},
|
||||
{input: " tidal:40681594 ", want: 40681594, ok: true},
|
||||
{input: "", want: 0, ok: false},
|
||||
{input: "tidal:not-a-number", want: 0, ok: false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
got, ok := parseTidalRequestTrackID(test.input)
|
||||
if got != test.want || ok != test.ok {
|
||||
t.Fatalf("parseTidalRequestTrackID(%q) = (%d, %v), want (%d, %v)", test.input, got, ok, test.want, test.ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTidalImageURL(t *testing.T) {
|
||||
got := tidalImageURL("fc18a64b-d76b-4582-962a-224cb05193f3", "1280x1280")
|
||||
want := "https://resources.tidal.com/images/fc18a64b/d76b/4582/962a/224cb05193f3/1280x1280.jpg"
|
||||
if got != want {
|
||||
t.Fatalf("tidalImageURL() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTidalTrackToTrackMetadata(t *testing.T) {
|
||||
track := &TidalTrack{
|
||||
ID: 77616174,
|
||||
Title: "Bruckner: Symphony No. 5",
|
||||
ISRC: "GBUM71507433",
|
||||
Duration: 1172,
|
||||
TrackNumber: 5,
|
||||
VolumeNumber: 1,
|
||||
URL: "http://www.tidal.com/track/77616174",
|
||||
}
|
||||
track.Artist.ID = 3852143
|
||||
track.Artist.Name = "Staatskapelle Berlin"
|
||||
track.Artists = []struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Picture string `json:"picture"`
|
||||
}{
|
||||
{ID: 3852143, Name: "Staatskapelle Berlin", Type: "MAIN"},
|
||||
{ID: 12430, Name: "Daniel Barenboim", Type: "FEATURED"},
|
||||
}
|
||||
track.Album.ID = 77616169
|
||||
track.Album.Title = "Bruckner: Symphonies 4-9"
|
||||
track.Album.Cover = "fc18a64b-d76b-4582-962a-224cb05193f3"
|
||||
track.Album.ReleaseDate = "2016-02-26"
|
||||
|
||||
got := tidalTrackToTrackMetadata(track)
|
||||
if got.SpotifyID != "tidal:77616174" {
|
||||
t.Fatalf("unexpected track ID: %q", got.SpotifyID)
|
||||
}
|
||||
if got.Artists != "Staatskapelle Berlin, Daniel Barenboim" {
|
||||
t.Fatalf("unexpected artists: %q", got.Artists)
|
||||
}
|
||||
if got.AlbumID != "tidal:77616169" {
|
||||
t.Fatalf("unexpected album ID: %q", got.AlbumID)
|
||||
}
|
||||
if got.ArtistID != "tidal:3852143" {
|
||||
t.Fatalf("unexpected artist ID: %q", got.ArtistID)
|
||||
}
|
||||
if got.Images == "" || got.ExternalURL != "https://www.tidal.com/track/77616174" {
|
||||
t.Fatalf("unexpected image/url: %q / %q", got.Images, got.ExternalURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTidalAlbumToArtistAlbum(t *testing.T) {
|
||||
album := &tidalPublicAlbum{
|
||||
ID: 77616169,
|
||||
Title: "Bruckner: Symphonies 4-9",
|
||||
Type: "ALBUM",
|
||||
Cover: "fc18a64b-d76b-4582-962a-224cb05193f3",
|
||||
ReleaseDate: "2016-02-26",
|
||||
NumberOfTracks: 23,
|
||||
Artists: []tidalPublicArtist{
|
||||
{ID: 3852143, Name: "Staatskapelle Berlin", Type: "MAIN"},
|
||||
{ID: 12430, Name: "Daniel Barenboim", Type: "FEATURED"},
|
||||
},
|
||||
}
|
||||
|
||||
got := tidalAlbumToArtistAlbum(album)
|
||||
if got.ID != "tidal:77616169" {
|
||||
t.Fatalf("unexpected album ID: %q", got.ID)
|
||||
}
|
||||
if got.AlbumType != "album" {
|
||||
t.Fatalf("unexpected album type: %q", got.AlbumType)
|
||||
}
|
||||
if got.Artists != "Staatskapelle Berlin, Daniel Barenboim" {
|
||||
t.Fatalf("unexpected artists: %q", got.Artists)
|
||||
}
|
||||
if got.Images == "" {
|
||||
t.Fatalf("expected image URL, got empty string")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTidalAlbumToArtistAlbumWithFallbackType(t *testing.T) {
|
||||
album := &tidalPublicAlbum{
|
||||
ID: 490623904,
|
||||
Title: "LET 'EM KNOW",
|
||||
Cover: "fc18a64b-d76b-4582-962a-224cb05193f3",
|
||||
NumberOfTracks: 1,
|
||||
}
|
||||
|
||||
got := tidalAlbumToArtistAlbumWithType(album, "single")
|
||||
if got.AlbumType != "single" {
|
||||
t.Fatalf("unexpected fallback album type: %q", got.AlbumType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTidalArtistAlbumTypeFromModuleTitle(t *testing.T) {
|
||||
tests := []struct {
|
||||
title string
|
||||
want string
|
||||
}{
|
||||
{title: "Albums", want: "album"},
|
||||
{title: "EP & Singles", want: "single"},
|
||||
{title: "Compilations", want: "album"},
|
||||
{title: "Appears On", want: "album"},
|
||||
{title: "Unknown", want: ""},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
if got := tidalArtistAlbumTypeFromModuleTitle(test.title); got != test.want {
|
||||
t.Fatalf("tidalArtistAlbumTypeFromModuleTitle(%q) = %q, want %q", test.title, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTidalPlaylistImageUsesOrigin(t *testing.T) {
|
||||
got := tidalImageURL("e6b59fd3-6995-40f0-8a32-174db3a8f4f2", "origin")
|
||||
want := "https://resources.tidal.com/images/e6b59fd3/6995/40f0/8a32/174db3a8f4f2/origin.jpg"
|
||||
if got != want {
|
||||
t.Fatalf("unexpected origin playlist image URL: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTidalPlaylistOwnerName(t *testing.T) {
|
||||
editorial := &tidalPublicPlaylist{Type: "EDITORIAL"}
|
||||
if got := tidalPlaylistOwnerName(editorial); got != "TIDAL" {
|
||||
t.Fatalf("unexpected editorial owner: %q", got)
|
||||
}
|
||||
|
||||
artist := &tidalPublicPlaylist{Type: "ARTIST"}
|
||||
if got := tidalPlaylistOwnerName(artist); got != "Artist" {
|
||||
t.Fatalf("unexpected artist owner: %q", got)
|
||||
}
|
||||
|
||||
user := &tidalPublicPlaylist{}
|
||||
user.Creator.Name = "djtest"
|
||||
if got := tidalPlaylistOwnerName(user); got != "djtest" {
|
||||
t.Fatalf("unexpected creator owner: %q", got)
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,263 @@ func normalizeSymbolOnlyTitle(title string) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func artistsMatch(expectedArtist, foundArtist string) bool {
|
||||
normExpected := normalizeLooseArtistName(expectedArtist)
|
||||
normFound := normalizeLooseArtistName(foundArtist)
|
||||
|
||||
if normExpected == normFound {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.Contains(normExpected, normFound) ||
|
||||
strings.Contains(normFound, normExpected) {
|
||||
return true
|
||||
}
|
||||
|
||||
expectedArtists := splitArtists(normExpected)
|
||||
foundArtists := splitArtists(normFound)
|
||||
|
||||
for _, expected := range expectedArtists {
|
||||
for _, found := range foundArtists {
|
||||
if expected == found {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(expected, found) ||
|
||||
strings.Contains(found, expected) {
|
||||
return true
|
||||
}
|
||||
if sameWordsUnordered(expected, found) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isLatinScript(expectedArtist) != isLatinScript(foundArtist) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func splitArtists(artists string) []string {
|
||||
normalized := artists
|
||||
normalized = strings.ReplaceAll(normalized, " feat. ", "|")
|
||||
normalized = strings.ReplaceAll(normalized, " feat ", "|")
|
||||
normalized = strings.ReplaceAll(normalized, " ft. ", "|")
|
||||
normalized = strings.ReplaceAll(normalized, " ft ", "|")
|
||||
normalized = strings.ReplaceAll(normalized, " & ", "|")
|
||||
normalized = strings.ReplaceAll(normalized, " and ", "|")
|
||||
normalized = strings.ReplaceAll(normalized, ", ", "|")
|
||||
normalized = strings.ReplaceAll(normalized, " x ", "|")
|
||||
|
||||
parts := strings.Split(normalized, "|")
|
||||
result := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
trimmed := strings.TrimSpace(part)
|
||||
if trimmed != "" {
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func sameWordsUnordered(a, b string) bool {
|
||||
wordsA := strings.Fields(a)
|
||||
wordsB := strings.Fields(b)
|
||||
if len(wordsA) != len(wordsB) || len(wordsA) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
sortedA := make([]string, len(wordsA))
|
||||
sortedB := make([]string, len(wordsB))
|
||||
copy(sortedA, wordsA)
|
||||
copy(sortedB, wordsB)
|
||||
|
||||
for i := 0; i < len(sortedA)-1; i++ {
|
||||
for j := i + 1; j < len(sortedA); j++ {
|
||||
if sortedA[i] > sortedA[j] {
|
||||
sortedA[i], sortedA[j] = sortedA[j], sortedA[i]
|
||||
}
|
||||
if sortedB[i] > sortedB[j] {
|
||||
sortedB[i], sortedB[j] = sortedB[j], sortedB[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i := range sortedA {
|
||||
if sortedA[i] != sortedB[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func titlesMatch(expectedTitle, foundTitle string) bool {
|
||||
normExpected := strings.ToLower(strings.TrimSpace(expectedTitle))
|
||||
normFound := strings.ToLower(strings.TrimSpace(foundTitle))
|
||||
|
||||
if normExpected == normFound {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.Contains(normExpected, normFound) ||
|
||||
strings.Contains(normFound, normExpected) {
|
||||
return true
|
||||
}
|
||||
|
||||
cleanExpected := cleanTitle(normExpected)
|
||||
cleanFound := cleanTitle(normFound)
|
||||
if cleanExpected == cleanFound {
|
||||
return true
|
||||
}
|
||||
|
||||
if cleanExpected != "" && cleanFound != "" {
|
||||
if strings.Contains(cleanExpected, cleanFound) ||
|
||||
strings.Contains(cleanFound, cleanExpected) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
coreExpected := extractCoreTitle(normExpected)
|
||||
coreFound := extractCoreTitle(normFound)
|
||||
if coreExpected != "" && coreFound != "" && coreExpected == coreFound {
|
||||
return true
|
||||
}
|
||||
|
||||
looseExpected := normalizeLooseTitle(normExpected)
|
||||
looseFound := normalizeLooseTitle(normFound)
|
||||
if looseExpected != "" && looseFound != "" {
|
||||
if looseExpected == looseFound {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(looseExpected, looseFound) ||
|
||||
strings.Contains(looseFound, looseExpected) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasAlphaNumericRunes(expectedTitle) || !hasAlphaNumericRunes(foundTitle)) &&
|
||||
strings.TrimSpace(expectedTitle) != "" &&
|
||||
strings.TrimSpace(foundTitle) != "" {
|
||||
expectedSymbols := normalizeSymbolOnlyTitle(expectedTitle)
|
||||
foundSymbols := normalizeSymbolOnlyTitle(foundTitle)
|
||||
if expectedSymbols != "" &&
|
||||
foundSymbols != "" &&
|
||||
expectedSymbols == foundSymbols {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func extractCoreTitle(title string) string {
|
||||
parenIdx := strings.Index(title, "(")
|
||||
bracketIdx := strings.Index(title, "[")
|
||||
dashIdx := strings.Index(title, " - ")
|
||||
|
||||
cutIdx := len(title)
|
||||
if parenIdx > 0 && parenIdx < cutIdx {
|
||||
cutIdx = parenIdx
|
||||
}
|
||||
if bracketIdx > 0 && bracketIdx < cutIdx {
|
||||
cutIdx = bracketIdx
|
||||
}
|
||||
if dashIdx > 0 && dashIdx < cutIdx {
|
||||
cutIdx = dashIdx
|
||||
}
|
||||
|
||||
return strings.TrimSpace(title[:cutIdx])
|
||||
}
|
||||
|
||||
func cleanTitle(title string) string {
|
||||
cleaned := title
|
||||
|
||||
versionPatterns := []string{
|
||||
"remaster", "remastered", "deluxe", "bonus", "single",
|
||||
"album version", "radio edit", "original mix", "extended",
|
||||
"club mix", "remix", "live", "acoustic", "demo",
|
||||
}
|
||||
|
||||
for {
|
||||
startParen := strings.LastIndex(cleaned, "(")
|
||||
endParen := strings.LastIndex(cleaned, ")")
|
||||
if startParen >= 0 && endParen > startParen {
|
||||
content := strings.ToLower(cleaned[startParen+1 : endParen])
|
||||
isVersionIndicator := false
|
||||
for _, pattern := range versionPatterns {
|
||||
if strings.Contains(content, pattern) {
|
||||
isVersionIndicator = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isVersionIndicator {
|
||||
cleaned = strings.TrimSpace(cleaned[:startParen]) + cleaned[endParen+1:]
|
||||
continue
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
for {
|
||||
startBracket := strings.LastIndex(cleaned, "[")
|
||||
endBracket := strings.LastIndex(cleaned, "]")
|
||||
if startBracket >= 0 && endBracket > startBracket {
|
||||
content := strings.ToLower(cleaned[startBracket+1 : endBracket])
|
||||
isVersionIndicator := false
|
||||
for _, pattern := range versionPatterns {
|
||||
if strings.Contains(content, pattern) {
|
||||
isVersionIndicator = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isVersionIndicator {
|
||||
cleaned = strings.TrimSpace(cleaned[:startBracket]) + cleaned[endBracket+1:]
|
||||
continue
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
dashPatterns := []string{
|
||||
" - remaster", " - remastered", " - single version", " - radio edit",
|
||||
" - live", " - acoustic", " - demo", " - remix",
|
||||
}
|
||||
for _, pattern := range dashPatterns {
|
||||
if strings.HasSuffix(strings.ToLower(cleaned), pattern) {
|
||||
cleaned = cleaned[:len(cleaned)-len(pattern)]
|
||||
}
|
||||
}
|
||||
|
||||
for strings.Contains(cleaned, " ") {
|
||||
cleaned = strings.ReplaceAll(cleaned, " ", " ")
|
||||
}
|
||||
|
||||
return strings.TrimSpace(cleaned)
|
||||
}
|
||||
|
||||
func isLatinScript(value string) bool {
|
||||
for _, r := range value {
|
||||
if r < 128 {
|
||||
continue
|
||||
}
|
||||
if (r >= 0x0100 && r <= 0x024F) ||
|
||||
(r >= 0x1E00 && r <= 0x1EFF) ||
|
||||
(r >= 0x00C0 && r <= 0x00FF) {
|
||||
continue
|
||||
}
|
||||
if (r >= 0x4E00 && r <= 0x9FFF) ||
|
||||
(r >= 0x3040 && r <= 0x309F) ||
|
||||
(r >= 0x30A0 && r <= 0x30FF) ||
|
||||
(r >= 0xAC00 && r <= 0xD7AF) ||
|
||||
(r >= 0x0600 && r <= 0x06FF) ||
|
||||
(r >= 0x0400 && r <= 0x04FF) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type resolvedTrackInfo struct {
|
||||
Title string
|
||||
ArtistName string
|
||||
|
||||
@@ -456,14 +456,6 @@ import Gobackend // Import Go framework
|
||||
if let error = error { throw error }
|
||||
return response
|
||||
|
||||
case "getTidalMetadata":
|
||||
let args = call.arguments as! [String: Any]
|
||||
let resourceType = args["resource_type"] as! String
|
||||
let resourceId = args["resource_id"] as! String
|
||||
let response = GobackendGetTidalMetadata(resourceType, resourceId, &error)
|
||||
if let error = error { throw error }
|
||||
return response
|
||||
|
||||
case "getProviderMetadata":
|
||||
let args = call.arguments as! [String: Any]
|
||||
let providerId = args["provider_id"] as! String
|
||||
@@ -480,13 +472,6 @@ import Gobackend // Import Go framework
|
||||
if let error = error { throw error }
|
||||
return response
|
||||
|
||||
case "convertTidalToSpotifyDeezer":
|
||||
let args = call.arguments as! [String: Any]
|
||||
let url = args["url"] as! String
|
||||
let response = GobackendConvertTidalToSpotifyDeezer(url, &error)
|
||||
if let error = error { throw error }
|
||||
return response
|
||||
|
||||
case "searchDeezerByISRC":
|
||||
let args = call.arguments as! [String: Any]
|
||||
let isrc = args["isrc"] as! String
|
||||
|
||||
@@ -166,6 +166,18 @@ abstract class AppLocalizations {
|
||||
/// **'Paste a supported URL or search by name'**
|
||||
String get homeSubtitle;
|
||||
|
||||
/// Title shown on home when no providers are available yet
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Home is empty'**
|
||||
String get homeEmptyTitle;
|
||||
|
||||
/// Subtitle shown on home when no providers are available yet
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Install your first extension to unlock search and browsing.'**
|
||||
String get homeEmptySubtitle;
|
||||
|
||||
/// Info text about supported URL types
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -5844,6 +5856,364 @@ abstract class AppLocalizations {
|
||||
/// In en, this message translates to:
|
||||
/// **'Could not download update. Try again later.'**
|
||||
String get notifUpdateFailedBody;
|
||||
|
||||
/// Search filter label - tracks
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Tracks'**
|
||||
String get searchTracks;
|
||||
|
||||
/// Default placeholder for the main search field on Home
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Paste supported URL or search...'**
|
||||
String get homeSearchHintDefault;
|
||||
|
||||
/// Placeholder for the main search field when a provider is selected
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Search with {providerName}...'**
|
||||
String homeSearchHintProvider(String providerName);
|
||||
|
||||
/// Tooltip for importing a CSV file into Home search
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Import CSV'**
|
||||
String get homeImportCsvTooltip;
|
||||
|
||||
/// Tooltip for the Home search provider picker
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Change search provider'**
|
||||
String get homeChangeSearchProviderTooltip;
|
||||
|
||||
/// Generic action - paste from clipboard
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Paste'**
|
||||
String get actionPaste;
|
||||
|
||||
/// Placeholder for the search screen input
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Search tracks...'**
|
||||
String get searchTracksHint;
|
||||
|
||||
/// Empty-state prompt on the search screen
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Search for tracks'**
|
||||
String get searchTracksEmptyPrompt;
|
||||
|
||||
/// Placeholder shown in the tutorial search demo
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Paste or search...'**
|
||||
String get tutorialSearchHint;
|
||||
|
||||
/// Accessibility label for completed download state in tutorial demo
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Download completed'**
|
||||
String get tutorialDownloadCompletedSemantics;
|
||||
|
||||
/// Accessibility label for active download state in tutorial demo
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Download in progress'**
|
||||
String get tutorialDownloadInProgressSemantics;
|
||||
|
||||
/// Accessibility label for idle download button in tutorial demo
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Start download'**
|
||||
String get tutorialStartDownloadSemantics;
|
||||
|
||||
/// Settings toggle title for writing metadata into downloaded files
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Embed Metadata'**
|
||||
String get optionsEmbedMetadata;
|
||||
|
||||
/// Subtitle when metadata embedding is enabled
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Write metadata, cover art, and embedded lyrics to files'**
|
||||
String get optionsEmbedMetadataSubtitleOn;
|
||||
|
||||
/// Subtitle when metadata embedding is disabled
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Disabled (advanced): skip all metadata embedding'**
|
||||
String get optionsEmbedMetadataSubtitleOff;
|
||||
|
||||
/// Subtitle for max quality cover when metadata embedding is disabled
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Disabled when metadata embedding is off'**
|
||||
String get optionsMaxQualityCoverSubtitleDisabled;
|
||||
|
||||
/// Example placeholder for the download filename format input
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{artist} - {title}'**
|
||||
String downloadFilenameHintExample(Object artist, Object title);
|
||||
|
||||
/// Message shown when a track file has no embedded cover art
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No embedded album art found'**
|
||||
String get trackCoverNoEmbeddedArt;
|
||||
|
||||
/// Button label for replacing selected cover art
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Replace Cover'**
|
||||
String get trackCoverReplace;
|
||||
|
||||
/// Button label for selecting cover art
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Pick Cover'**
|
||||
String get trackCoverPick;
|
||||
|
||||
/// Tooltip for clearing the newly selected cover art
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Clear selected cover'**
|
||||
String get trackCoverClearSelected;
|
||||
|
||||
/// Label for the currently embedded cover preview
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Current cover'**
|
||||
String get trackCoverCurrent;
|
||||
|
||||
/// Label for the newly selected cover preview
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Selected cover'**
|
||||
String get trackCoverSelected;
|
||||
|
||||
/// Notice shown when a new cover has been selected but not saved yet
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The selected cover will replace the current embedded cover when you tap Save.'**
|
||||
String get trackCoverReplaceNotice;
|
||||
|
||||
/// Generic action - stop
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Stop'**
|
||||
String get actionStop;
|
||||
|
||||
/// Accessibility label for a queue item that is finalizing
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Finalizing download'**
|
||||
String get queueFinalizingDownload;
|
||||
|
||||
/// Accessibility label when a downloaded file is missing from disk
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Downloaded file missing'**
|
||||
String get queueDownloadedFileMissing;
|
||||
|
||||
/// Accessibility label for completed download state in queue
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Download completed'**
|
||||
String get queueDownloadCompleted;
|
||||
|
||||
/// Accessibility label for picking an accent color
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Select accent color {hex}'**
|
||||
String appearanceSelectAccentColor(String hex);
|
||||
|
||||
/// Tooltip when auto-scroll is enabled on the log screen
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Auto-scroll ON'**
|
||||
String get logAutoScrollOn;
|
||||
|
||||
/// Tooltip when auto-scroll is disabled on the log screen
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Auto-scroll OFF'**
|
||||
String get logAutoScrollOff;
|
||||
|
||||
/// Tooltip for copying logs
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Copy logs'**
|
||||
String get logCopyLogs;
|
||||
|
||||
/// Tooltip for clearing the log search field
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Clear search'**
|
||||
String get logClearSearch;
|
||||
|
||||
/// Diagnostic badge label when ISP blocking is detected
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'ISP BLOCKING DETECTED'**
|
||||
String get logIssueIspBlockingLabel;
|
||||
|
||||
/// Diagnostic badge description for ISP blocking
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your ISP may be blocking access to download services'**
|
||||
String get logIssueIspBlockingDescription;
|
||||
|
||||
/// Diagnostic badge suggestion for ISP blocking
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8'**
|
||||
String get logIssueIspBlockingSuggestion;
|
||||
|
||||
/// Diagnostic badge label when the service rate limits requests
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'RATE LIMITED'**
|
||||
String get logIssueRateLimitedLabel;
|
||||
|
||||
/// Diagnostic badge description for rate limiting
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Too many requests to the service'**
|
||||
String get logIssueRateLimitedDescription;
|
||||
|
||||
/// Diagnostic badge suggestion for rate limiting
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Wait a few minutes before trying again'**
|
||||
String get logIssueRateLimitedSuggestion;
|
||||
|
||||
/// Diagnostic badge label for generic network errors
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'NETWORK ERROR'**
|
||||
String get logIssueNetworkErrorLabel;
|
||||
|
||||
/// Diagnostic badge description for generic network errors
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Connection issues detected'**
|
||||
String get logIssueNetworkErrorDescription;
|
||||
|
||||
/// Diagnostic badge suggestion for generic network errors
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Check your internet connection'**
|
||||
String get logIssueNetworkErrorSuggestion;
|
||||
|
||||
/// Diagnostic badge label when a track is unavailable
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'TRACK NOT FOUND'**
|
||||
String get logIssueTrackNotFoundLabel;
|
||||
|
||||
/// Diagnostic badge description when a track is unavailable
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Some tracks could not be found on download services'**
|
||||
String get logIssueTrackNotFoundDescription;
|
||||
|
||||
/// Diagnostic badge suggestion when a track is unavailable
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The track may not be available in lossless quality'**
|
||||
String get logIssueTrackNotFoundSuggestion;
|
||||
|
||||
/// Snackbar shown while clickable artist metadata is being resolved
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Looking up artist...'**
|
||||
String get clickableLookingUpArtist;
|
||||
|
||||
/// Snackbar shown when clickable metadata cannot open a destination
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{type} information not available'**
|
||||
String clickableInformationUnavailable(String type);
|
||||
|
||||
/// Section title for extension tags
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Tags'**
|
||||
String get extensionDetailsTags;
|
||||
|
||||
/// Section title for extension metadata information
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Information'**
|
||||
String get extensionDetailsInformation;
|
||||
|
||||
/// Capability label for utility-only extensions
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Utility Functions'**
|
||||
String get extensionUtilityFunctions;
|
||||
|
||||
/// Generic action - dismiss
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Dismiss'**
|
||||
String get actionDismiss;
|
||||
|
||||
/// Tooltip for editing the selected download folder
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Change folder'**
|
||||
String get setupChangeFolderTooltip;
|
||||
|
||||
/// Accessibility label for opening a track item
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Open track {trackName} by {artistName}'**
|
||||
String a11yOpenTrackByArtist(String trackName, String artistName);
|
||||
|
||||
/// Accessibility label for opening a generic item
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Open {itemType} {name}'**
|
||||
String a11yOpenItem(String itemType, String name);
|
||||
|
||||
/// Accessibility label for opening a grouped item with count
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Open {title}, {count} {count, plural, =1{item} other{items}}'**
|
||||
String a11yOpenItemCount(String title, int count);
|
||||
|
||||
/// Accessibility label for opening an album item with track count
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Open album {albumName} by {artistName}, {trackCount} tracks'**
|
||||
String a11yOpenAlbumByArtistTrackCount(
|
||||
String albumName,
|
||||
String artistName,
|
||||
int trackCount,
|
||||
);
|
||||
|
||||
/// Accessibility label for a queue or list track item
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{trackName} by {artistName}'**
|
||||
String a11yTrackByArtist(String trackName, String artistName);
|
||||
|
||||
/// Accessibility label for selecting an album
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Select album {albumName}'**
|
||||
String a11ySelectAlbum(String albumName);
|
||||
|
||||
/// Accessibility label for opening an album
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Open album {albumName}'**
|
||||
String a11yOpenAlbum(String albumName);
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
||||
@@ -29,6 +29,13 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get homeSubtitle => 'Spotify-Link einfügen oder nach Namen suchen';
|
||||
|
||||
@override
|
||||
String get homeEmptyTitle => 'Home is empty';
|
||||
|
||||
@override
|
||||
String get homeEmptySubtitle =>
|
||||
'Install your first extension to unlock search and browsing.';
|
||||
|
||||
@override
|
||||
String get homeSupports =>
|
||||
'Unterstützt: Titel, Album, Playlist, Künstler-URLs';
|
||||
@@ -3456,4 +3463,223 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get notifUpdateFailedBody =>
|
||||
'Could not download update. Try again later.';
|
||||
|
||||
@override
|
||||
String get searchTracks => 'Tracks';
|
||||
|
||||
@override
|
||||
String get homeSearchHintDefault => 'Paste supported URL or search...';
|
||||
|
||||
@override
|
||||
String homeSearchHintProvider(String providerName) {
|
||||
return 'Search with $providerName...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get homeImportCsvTooltip => 'Import CSV';
|
||||
|
||||
@override
|
||||
String get homeChangeSearchProviderTooltip => 'Change search provider';
|
||||
|
||||
@override
|
||||
String get actionPaste => 'Paste';
|
||||
|
||||
@override
|
||||
String get searchTracksHint => 'Search tracks...';
|
||||
|
||||
@override
|
||||
String get searchTracksEmptyPrompt => 'Search for tracks';
|
||||
|
||||
@override
|
||||
String get tutorialSearchHint => 'Paste or search...';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadCompletedSemantics => 'Download completed';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadInProgressSemantics => 'Download in progress';
|
||||
|
||||
@override
|
||||
String get tutorialStartDownloadSemantics => 'Start download';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadata => 'Embed Metadata';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadataSubtitleOn =>
|
||||
'Write metadata, cover art, and embedded lyrics to files';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadataSubtitleOff =>
|
||||
'Disabled (advanced): skip all metadata embedding';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCoverSubtitleDisabled =>
|
||||
'Disabled when metadata embedding is off';
|
||||
|
||||
@override
|
||||
String downloadFilenameHintExample(Object artist, Object title) {
|
||||
return '$artist - $title';
|
||||
}
|
||||
|
||||
@override
|
||||
String get trackCoverNoEmbeddedArt => 'No embedded album art found';
|
||||
|
||||
@override
|
||||
String get trackCoverReplace => 'Replace Cover';
|
||||
|
||||
@override
|
||||
String get trackCoverPick => 'Pick Cover';
|
||||
|
||||
@override
|
||||
String get trackCoverClearSelected => 'Clear selected cover';
|
||||
|
||||
@override
|
||||
String get trackCoverCurrent => 'Current cover';
|
||||
|
||||
@override
|
||||
String get trackCoverSelected => 'Selected cover';
|
||||
|
||||
@override
|
||||
String get trackCoverReplaceNotice =>
|
||||
'The selected cover will replace the current embedded cover when you tap Save.';
|
||||
|
||||
@override
|
||||
String get actionStop => 'Stop';
|
||||
|
||||
@override
|
||||
String get queueFinalizingDownload => 'Finalizing download';
|
||||
|
||||
@override
|
||||
String get queueDownloadedFileMissing => 'Downloaded file missing';
|
||||
|
||||
@override
|
||||
String get queueDownloadCompleted => 'Download completed';
|
||||
|
||||
@override
|
||||
String appearanceSelectAccentColor(String hex) {
|
||||
return 'Select accent color $hex';
|
||||
}
|
||||
|
||||
@override
|
||||
String get logAutoScrollOn => 'Auto-scroll ON';
|
||||
|
||||
@override
|
||||
String get logAutoScrollOff => 'Auto-scroll OFF';
|
||||
|
||||
@override
|
||||
String get logCopyLogs => 'Copy logs';
|
||||
|
||||
@override
|
||||
String get logClearSearch => 'Clear search';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingLabel => 'ISP BLOCKING DETECTED';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingDescription =>
|
||||
'Your ISP may be blocking access to download services';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingSuggestion =>
|
||||
'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedLabel => 'RATE LIMITED';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedDescription =>
|
||||
'Too many requests to the service';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedSuggestion =>
|
||||
'Wait a few minutes before trying again';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorLabel => 'NETWORK ERROR';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorDescription => 'Connection issues detected';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorSuggestion => 'Check your internet connection';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundLabel => 'TRACK NOT FOUND';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundDescription =>
|
||||
'Some tracks could not be found on download services';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundSuggestion =>
|
||||
'The track may not be available in lossless quality';
|
||||
|
||||
@override
|
||||
String get clickableLookingUpArtist => 'Looking up artist...';
|
||||
|
||||
@override
|
||||
String clickableInformationUnavailable(String type) {
|
||||
return '$type information not available';
|
||||
}
|
||||
|
||||
@override
|
||||
String get extensionDetailsTags => 'Tags';
|
||||
|
||||
@override
|
||||
String get extensionDetailsInformation => 'Information';
|
||||
|
||||
@override
|
||||
String get extensionUtilityFunctions => 'Utility Functions';
|
||||
|
||||
@override
|
||||
String get actionDismiss => 'Dismiss';
|
||||
|
||||
@override
|
||||
String get setupChangeFolderTooltip => 'Change folder';
|
||||
|
||||
@override
|
||||
String a11yOpenTrackByArtist(String trackName, String artistName) {
|
||||
return 'Open track $trackName by $artistName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenItem(String itemType, String name) {
|
||||
return 'Open $itemType $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenItemCount(String title, int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'items',
|
||||
one: 'item',
|
||||
);
|
||||
return 'Open $title, $count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenAlbumByArtistTrackCount(
|
||||
String albumName,
|
||||
String artistName,
|
||||
int trackCount,
|
||||
) {
|
||||
return 'Open album $albumName by $artistName, $trackCount tracks';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yTrackByArtist(String trackName, String artistName) {
|
||||
return '$trackName by $artistName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11ySelectAlbum(String albumName) {
|
||||
return 'Select album $albumName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenAlbum(String albumName) {
|
||||
return 'Open album $albumName';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get homeSubtitle => 'Paste a supported URL or search by name';
|
||||
|
||||
@override
|
||||
String get homeEmptyTitle => 'Home is empty';
|
||||
|
||||
@override
|
||||
String get homeEmptySubtitle =>
|
||||
'Install your first extension to unlock search and browsing.';
|
||||
|
||||
@override
|
||||
String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs';
|
||||
|
||||
@@ -3424,4 +3431,223 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get notifUpdateFailedBody =>
|
||||
'Could not download update. Try again later.';
|
||||
|
||||
@override
|
||||
String get searchTracks => 'Tracks';
|
||||
|
||||
@override
|
||||
String get homeSearchHintDefault => 'Paste supported URL or search...';
|
||||
|
||||
@override
|
||||
String homeSearchHintProvider(String providerName) {
|
||||
return 'Search with $providerName...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get homeImportCsvTooltip => 'Import CSV';
|
||||
|
||||
@override
|
||||
String get homeChangeSearchProviderTooltip => 'Change search provider';
|
||||
|
||||
@override
|
||||
String get actionPaste => 'Paste';
|
||||
|
||||
@override
|
||||
String get searchTracksHint => 'Search tracks...';
|
||||
|
||||
@override
|
||||
String get searchTracksEmptyPrompt => 'Search for tracks';
|
||||
|
||||
@override
|
||||
String get tutorialSearchHint => 'Paste or search...';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadCompletedSemantics => 'Download completed';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadInProgressSemantics => 'Download in progress';
|
||||
|
||||
@override
|
||||
String get tutorialStartDownloadSemantics => 'Start download';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadata => 'Embed Metadata';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadataSubtitleOn =>
|
||||
'Write metadata, cover art, and embedded lyrics to files';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadataSubtitleOff =>
|
||||
'Disabled (advanced): skip all metadata embedding';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCoverSubtitleDisabled =>
|
||||
'Disabled when metadata embedding is off';
|
||||
|
||||
@override
|
||||
String downloadFilenameHintExample(Object artist, Object title) {
|
||||
return '$artist - $title';
|
||||
}
|
||||
|
||||
@override
|
||||
String get trackCoverNoEmbeddedArt => 'No embedded album art found';
|
||||
|
||||
@override
|
||||
String get trackCoverReplace => 'Replace Cover';
|
||||
|
||||
@override
|
||||
String get trackCoverPick => 'Pick Cover';
|
||||
|
||||
@override
|
||||
String get trackCoverClearSelected => 'Clear selected cover';
|
||||
|
||||
@override
|
||||
String get trackCoverCurrent => 'Current cover';
|
||||
|
||||
@override
|
||||
String get trackCoverSelected => 'Selected cover';
|
||||
|
||||
@override
|
||||
String get trackCoverReplaceNotice =>
|
||||
'The selected cover will replace the current embedded cover when you tap Save.';
|
||||
|
||||
@override
|
||||
String get actionStop => 'Stop';
|
||||
|
||||
@override
|
||||
String get queueFinalizingDownload => 'Finalizing download';
|
||||
|
||||
@override
|
||||
String get queueDownloadedFileMissing => 'Downloaded file missing';
|
||||
|
||||
@override
|
||||
String get queueDownloadCompleted => 'Download completed';
|
||||
|
||||
@override
|
||||
String appearanceSelectAccentColor(String hex) {
|
||||
return 'Select accent color $hex';
|
||||
}
|
||||
|
||||
@override
|
||||
String get logAutoScrollOn => 'Auto-scroll ON';
|
||||
|
||||
@override
|
||||
String get logAutoScrollOff => 'Auto-scroll OFF';
|
||||
|
||||
@override
|
||||
String get logCopyLogs => 'Copy logs';
|
||||
|
||||
@override
|
||||
String get logClearSearch => 'Clear search';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingLabel => 'ISP BLOCKING DETECTED';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingDescription =>
|
||||
'Your ISP may be blocking access to download services';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingSuggestion =>
|
||||
'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedLabel => 'RATE LIMITED';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedDescription =>
|
||||
'Too many requests to the service';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedSuggestion =>
|
||||
'Wait a few minutes before trying again';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorLabel => 'NETWORK ERROR';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorDescription => 'Connection issues detected';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorSuggestion => 'Check your internet connection';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundLabel => 'TRACK NOT FOUND';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundDescription =>
|
||||
'Some tracks could not be found on download services';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundSuggestion =>
|
||||
'The track may not be available in lossless quality';
|
||||
|
||||
@override
|
||||
String get clickableLookingUpArtist => 'Looking up artist...';
|
||||
|
||||
@override
|
||||
String clickableInformationUnavailable(String type) {
|
||||
return '$type information not available';
|
||||
}
|
||||
|
||||
@override
|
||||
String get extensionDetailsTags => 'Tags';
|
||||
|
||||
@override
|
||||
String get extensionDetailsInformation => 'Information';
|
||||
|
||||
@override
|
||||
String get extensionUtilityFunctions => 'Utility Functions';
|
||||
|
||||
@override
|
||||
String get actionDismiss => 'Dismiss';
|
||||
|
||||
@override
|
||||
String get setupChangeFolderTooltip => 'Change folder';
|
||||
|
||||
@override
|
||||
String a11yOpenTrackByArtist(String trackName, String artistName) {
|
||||
return 'Open track $trackName by $artistName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenItem(String itemType, String name) {
|
||||
return 'Open $itemType $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenItemCount(String title, int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'items',
|
||||
one: 'item',
|
||||
);
|
||||
return 'Open $title, $count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenAlbumByArtistTrackCount(
|
||||
String albumName,
|
||||
String artistName,
|
||||
int trackCount,
|
||||
) {
|
||||
return 'Open album $albumName by $artistName, $trackCount tracks';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yTrackByArtist(String trackName, String artistName) {
|
||||
return '$trackName by $artistName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11ySelectAlbum(String albumName) {
|
||||
return 'Select album $albumName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenAlbum(String albumName) {
|
||||
return 'Open album $albumName';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,13 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get homeSubtitle => 'Paste a Spotify link or search by name';
|
||||
|
||||
@override
|
||||
String get homeEmptyTitle => 'Home is empty';
|
||||
|
||||
@override
|
||||
String get homeEmptySubtitle =>
|
||||
'Install your first extension to unlock search and browsing.';
|
||||
|
||||
@override
|
||||
String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs';
|
||||
|
||||
@@ -3424,6 +3431,225 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get notifUpdateFailedBody =>
|
||||
'Could not download update. Try again later.';
|
||||
|
||||
@override
|
||||
String get searchTracks => 'Tracks';
|
||||
|
||||
@override
|
||||
String get homeSearchHintDefault => 'Paste supported URL or search...';
|
||||
|
||||
@override
|
||||
String homeSearchHintProvider(String providerName) {
|
||||
return 'Search with $providerName...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get homeImportCsvTooltip => 'Import CSV';
|
||||
|
||||
@override
|
||||
String get homeChangeSearchProviderTooltip => 'Change search provider';
|
||||
|
||||
@override
|
||||
String get actionPaste => 'Paste';
|
||||
|
||||
@override
|
||||
String get searchTracksHint => 'Search tracks...';
|
||||
|
||||
@override
|
||||
String get searchTracksEmptyPrompt => 'Search for tracks';
|
||||
|
||||
@override
|
||||
String get tutorialSearchHint => 'Paste or search...';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadCompletedSemantics => 'Download completed';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadInProgressSemantics => 'Download in progress';
|
||||
|
||||
@override
|
||||
String get tutorialStartDownloadSemantics => 'Start download';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadata => 'Embed Metadata';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadataSubtitleOn =>
|
||||
'Write metadata, cover art, and embedded lyrics to files';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadataSubtitleOff =>
|
||||
'Disabled (advanced): skip all metadata embedding';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCoverSubtitleDisabled =>
|
||||
'Disabled when metadata embedding is off';
|
||||
|
||||
@override
|
||||
String downloadFilenameHintExample(Object artist, Object title) {
|
||||
return '$artist - $title';
|
||||
}
|
||||
|
||||
@override
|
||||
String get trackCoverNoEmbeddedArt => 'No embedded album art found';
|
||||
|
||||
@override
|
||||
String get trackCoverReplace => 'Replace Cover';
|
||||
|
||||
@override
|
||||
String get trackCoverPick => 'Pick Cover';
|
||||
|
||||
@override
|
||||
String get trackCoverClearSelected => 'Clear selected cover';
|
||||
|
||||
@override
|
||||
String get trackCoverCurrent => 'Current cover';
|
||||
|
||||
@override
|
||||
String get trackCoverSelected => 'Selected cover';
|
||||
|
||||
@override
|
||||
String get trackCoverReplaceNotice =>
|
||||
'The selected cover will replace the current embedded cover when you tap Save.';
|
||||
|
||||
@override
|
||||
String get actionStop => 'Stop';
|
||||
|
||||
@override
|
||||
String get queueFinalizingDownload => 'Finalizing download';
|
||||
|
||||
@override
|
||||
String get queueDownloadedFileMissing => 'Downloaded file missing';
|
||||
|
||||
@override
|
||||
String get queueDownloadCompleted => 'Download completed';
|
||||
|
||||
@override
|
||||
String appearanceSelectAccentColor(String hex) {
|
||||
return 'Select accent color $hex';
|
||||
}
|
||||
|
||||
@override
|
||||
String get logAutoScrollOn => 'Auto-scroll ON';
|
||||
|
||||
@override
|
||||
String get logAutoScrollOff => 'Auto-scroll OFF';
|
||||
|
||||
@override
|
||||
String get logCopyLogs => 'Copy logs';
|
||||
|
||||
@override
|
||||
String get logClearSearch => 'Clear search';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingLabel => 'ISP BLOCKING DETECTED';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingDescription =>
|
||||
'Your ISP may be blocking access to download services';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingSuggestion =>
|
||||
'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedLabel => 'RATE LIMITED';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedDescription =>
|
||||
'Too many requests to the service';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedSuggestion =>
|
||||
'Wait a few minutes before trying again';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorLabel => 'NETWORK ERROR';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorDescription => 'Connection issues detected';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorSuggestion => 'Check your internet connection';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundLabel => 'TRACK NOT FOUND';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundDescription =>
|
||||
'Some tracks could not be found on download services';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundSuggestion =>
|
||||
'The track may not be available in lossless quality';
|
||||
|
||||
@override
|
||||
String get clickableLookingUpArtist => 'Looking up artist...';
|
||||
|
||||
@override
|
||||
String clickableInformationUnavailable(String type) {
|
||||
return '$type information not available';
|
||||
}
|
||||
|
||||
@override
|
||||
String get extensionDetailsTags => 'Tags';
|
||||
|
||||
@override
|
||||
String get extensionDetailsInformation => 'Information';
|
||||
|
||||
@override
|
||||
String get extensionUtilityFunctions => 'Utility Functions';
|
||||
|
||||
@override
|
||||
String get actionDismiss => 'Dismiss';
|
||||
|
||||
@override
|
||||
String get setupChangeFolderTooltip => 'Change folder';
|
||||
|
||||
@override
|
||||
String a11yOpenTrackByArtist(String trackName, String artistName) {
|
||||
return 'Open track $trackName by $artistName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenItem(String itemType, String name) {
|
||||
return 'Open $itemType $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenItemCount(String title, int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'items',
|
||||
one: 'item',
|
||||
);
|
||||
return 'Open $title, $count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenAlbumByArtistTrackCount(
|
||||
String albumName,
|
||||
String artistName,
|
||||
int trackCount,
|
||||
) {
|
||||
return 'Open album $albumName by $artistName, $trackCount tracks';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yTrackByArtist(String trackName, String artistName) {
|
||||
return '$trackName by $artistName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11ySelectAlbum(String albumName) {
|
||||
return 'Select album $albumName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenAlbum(String albumName) {
|
||||
return 'Open album $albumName';
|
||||
}
|
||||
}
|
||||
|
||||
/// The translations for Spanish Castilian, as used in Spain (`es_ES`).
|
||||
|
||||
@@ -29,6 +29,13 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get homeSubtitle => 'Coller un lien Spotify ou rechercher par nom';
|
||||
|
||||
@override
|
||||
String get homeEmptyTitle => 'Home is empty';
|
||||
|
||||
@override
|
||||
String get homeEmptySubtitle =>
|
||||
'Install your first extension to unlock search and browsing.';
|
||||
|
||||
@override
|
||||
String get homeSupports => 'Supports: Piste, Album, Playlist, Artiste URLs';
|
||||
|
||||
@@ -3425,4 +3432,223 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get notifUpdateFailedBody =>
|
||||
'Could not download update. Try again later.';
|
||||
|
||||
@override
|
||||
String get searchTracks => 'Tracks';
|
||||
|
||||
@override
|
||||
String get homeSearchHintDefault => 'Paste supported URL or search...';
|
||||
|
||||
@override
|
||||
String homeSearchHintProvider(String providerName) {
|
||||
return 'Search with $providerName...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get homeImportCsvTooltip => 'Import CSV';
|
||||
|
||||
@override
|
||||
String get homeChangeSearchProviderTooltip => 'Change search provider';
|
||||
|
||||
@override
|
||||
String get actionPaste => 'Paste';
|
||||
|
||||
@override
|
||||
String get searchTracksHint => 'Search tracks...';
|
||||
|
||||
@override
|
||||
String get searchTracksEmptyPrompt => 'Search for tracks';
|
||||
|
||||
@override
|
||||
String get tutorialSearchHint => 'Paste or search...';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadCompletedSemantics => 'Download completed';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadInProgressSemantics => 'Download in progress';
|
||||
|
||||
@override
|
||||
String get tutorialStartDownloadSemantics => 'Start download';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadata => 'Embed Metadata';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadataSubtitleOn =>
|
||||
'Write metadata, cover art, and embedded lyrics to files';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadataSubtitleOff =>
|
||||
'Disabled (advanced): skip all metadata embedding';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCoverSubtitleDisabled =>
|
||||
'Disabled when metadata embedding is off';
|
||||
|
||||
@override
|
||||
String downloadFilenameHintExample(Object artist, Object title) {
|
||||
return '$artist - $title';
|
||||
}
|
||||
|
||||
@override
|
||||
String get trackCoverNoEmbeddedArt => 'No embedded album art found';
|
||||
|
||||
@override
|
||||
String get trackCoverReplace => 'Replace Cover';
|
||||
|
||||
@override
|
||||
String get trackCoverPick => 'Pick Cover';
|
||||
|
||||
@override
|
||||
String get trackCoverClearSelected => 'Clear selected cover';
|
||||
|
||||
@override
|
||||
String get trackCoverCurrent => 'Current cover';
|
||||
|
||||
@override
|
||||
String get trackCoverSelected => 'Selected cover';
|
||||
|
||||
@override
|
||||
String get trackCoverReplaceNotice =>
|
||||
'The selected cover will replace the current embedded cover when you tap Save.';
|
||||
|
||||
@override
|
||||
String get actionStop => 'Stop';
|
||||
|
||||
@override
|
||||
String get queueFinalizingDownload => 'Finalizing download';
|
||||
|
||||
@override
|
||||
String get queueDownloadedFileMissing => 'Downloaded file missing';
|
||||
|
||||
@override
|
||||
String get queueDownloadCompleted => 'Download completed';
|
||||
|
||||
@override
|
||||
String appearanceSelectAccentColor(String hex) {
|
||||
return 'Select accent color $hex';
|
||||
}
|
||||
|
||||
@override
|
||||
String get logAutoScrollOn => 'Auto-scroll ON';
|
||||
|
||||
@override
|
||||
String get logAutoScrollOff => 'Auto-scroll OFF';
|
||||
|
||||
@override
|
||||
String get logCopyLogs => 'Copy logs';
|
||||
|
||||
@override
|
||||
String get logClearSearch => 'Clear search';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingLabel => 'ISP BLOCKING DETECTED';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingDescription =>
|
||||
'Your ISP may be blocking access to download services';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingSuggestion =>
|
||||
'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedLabel => 'RATE LIMITED';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedDescription =>
|
||||
'Too many requests to the service';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedSuggestion =>
|
||||
'Wait a few minutes before trying again';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorLabel => 'NETWORK ERROR';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorDescription => 'Connection issues detected';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorSuggestion => 'Check your internet connection';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundLabel => 'TRACK NOT FOUND';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundDescription =>
|
||||
'Some tracks could not be found on download services';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundSuggestion =>
|
||||
'The track may not be available in lossless quality';
|
||||
|
||||
@override
|
||||
String get clickableLookingUpArtist => 'Looking up artist...';
|
||||
|
||||
@override
|
||||
String clickableInformationUnavailable(String type) {
|
||||
return '$type information not available';
|
||||
}
|
||||
|
||||
@override
|
||||
String get extensionDetailsTags => 'Tags';
|
||||
|
||||
@override
|
||||
String get extensionDetailsInformation => 'Information';
|
||||
|
||||
@override
|
||||
String get extensionUtilityFunctions => 'Utility Functions';
|
||||
|
||||
@override
|
||||
String get actionDismiss => 'Dismiss';
|
||||
|
||||
@override
|
||||
String get setupChangeFolderTooltip => 'Change folder';
|
||||
|
||||
@override
|
||||
String a11yOpenTrackByArtist(String trackName, String artistName) {
|
||||
return 'Open track $trackName by $artistName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenItem(String itemType, String name) {
|
||||
return 'Open $itemType $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenItemCount(String title, int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'items',
|
||||
one: 'item',
|
||||
);
|
||||
return 'Open $title, $count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenAlbumByArtistTrackCount(
|
||||
String albumName,
|
||||
String artistName,
|
||||
int trackCount,
|
||||
) {
|
||||
return 'Open album $albumName by $artistName, $trackCount tracks';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yTrackByArtist(String trackName, String artistName) {
|
||||
return '$trackName by $artistName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11ySelectAlbum(String albumName) {
|
||||
return 'Select album $albumName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenAlbum(String albumName) {
|
||||
return 'Open album $albumName';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,13 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
@override
|
||||
String get homeSubtitle => 'Paste a Spotify link or search by name';
|
||||
|
||||
@override
|
||||
String get homeEmptyTitle => 'Home is empty';
|
||||
|
||||
@override
|
||||
String get homeEmptySubtitle =>
|
||||
'Install your first extension to unlock search and browsing.';
|
||||
|
||||
@override
|
||||
String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs';
|
||||
|
||||
@@ -3423,4 +3430,223 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
@override
|
||||
String get notifUpdateFailedBody =>
|
||||
'Could not download update. Try again later.';
|
||||
|
||||
@override
|
||||
String get searchTracks => 'Tracks';
|
||||
|
||||
@override
|
||||
String get homeSearchHintDefault => 'Paste supported URL or search...';
|
||||
|
||||
@override
|
||||
String homeSearchHintProvider(String providerName) {
|
||||
return 'Search with $providerName...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get homeImportCsvTooltip => 'Import CSV';
|
||||
|
||||
@override
|
||||
String get homeChangeSearchProviderTooltip => 'Change search provider';
|
||||
|
||||
@override
|
||||
String get actionPaste => 'Paste';
|
||||
|
||||
@override
|
||||
String get searchTracksHint => 'Search tracks...';
|
||||
|
||||
@override
|
||||
String get searchTracksEmptyPrompt => 'Search for tracks';
|
||||
|
||||
@override
|
||||
String get tutorialSearchHint => 'Paste or search...';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadCompletedSemantics => 'Download completed';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadInProgressSemantics => 'Download in progress';
|
||||
|
||||
@override
|
||||
String get tutorialStartDownloadSemantics => 'Start download';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadata => 'Embed Metadata';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadataSubtitleOn =>
|
||||
'Write metadata, cover art, and embedded lyrics to files';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadataSubtitleOff =>
|
||||
'Disabled (advanced): skip all metadata embedding';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCoverSubtitleDisabled =>
|
||||
'Disabled when metadata embedding is off';
|
||||
|
||||
@override
|
||||
String downloadFilenameHintExample(Object artist, Object title) {
|
||||
return '$artist - $title';
|
||||
}
|
||||
|
||||
@override
|
||||
String get trackCoverNoEmbeddedArt => 'No embedded album art found';
|
||||
|
||||
@override
|
||||
String get trackCoverReplace => 'Replace Cover';
|
||||
|
||||
@override
|
||||
String get trackCoverPick => 'Pick Cover';
|
||||
|
||||
@override
|
||||
String get trackCoverClearSelected => 'Clear selected cover';
|
||||
|
||||
@override
|
||||
String get trackCoverCurrent => 'Current cover';
|
||||
|
||||
@override
|
||||
String get trackCoverSelected => 'Selected cover';
|
||||
|
||||
@override
|
||||
String get trackCoverReplaceNotice =>
|
||||
'The selected cover will replace the current embedded cover when you tap Save.';
|
||||
|
||||
@override
|
||||
String get actionStop => 'Stop';
|
||||
|
||||
@override
|
||||
String get queueFinalizingDownload => 'Finalizing download';
|
||||
|
||||
@override
|
||||
String get queueDownloadedFileMissing => 'Downloaded file missing';
|
||||
|
||||
@override
|
||||
String get queueDownloadCompleted => 'Download completed';
|
||||
|
||||
@override
|
||||
String appearanceSelectAccentColor(String hex) {
|
||||
return 'Select accent color $hex';
|
||||
}
|
||||
|
||||
@override
|
||||
String get logAutoScrollOn => 'Auto-scroll ON';
|
||||
|
||||
@override
|
||||
String get logAutoScrollOff => 'Auto-scroll OFF';
|
||||
|
||||
@override
|
||||
String get logCopyLogs => 'Copy logs';
|
||||
|
||||
@override
|
||||
String get logClearSearch => 'Clear search';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingLabel => 'ISP BLOCKING DETECTED';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingDescription =>
|
||||
'Your ISP may be blocking access to download services';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingSuggestion =>
|
||||
'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedLabel => 'RATE LIMITED';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedDescription =>
|
||||
'Too many requests to the service';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedSuggestion =>
|
||||
'Wait a few minutes before trying again';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorLabel => 'NETWORK ERROR';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorDescription => 'Connection issues detected';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorSuggestion => 'Check your internet connection';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundLabel => 'TRACK NOT FOUND';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundDescription =>
|
||||
'Some tracks could not be found on download services';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundSuggestion =>
|
||||
'The track may not be available in lossless quality';
|
||||
|
||||
@override
|
||||
String get clickableLookingUpArtist => 'Looking up artist...';
|
||||
|
||||
@override
|
||||
String clickableInformationUnavailable(String type) {
|
||||
return '$type information not available';
|
||||
}
|
||||
|
||||
@override
|
||||
String get extensionDetailsTags => 'Tags';
|
||||
|
||||
@override
|
||||
String get extensionDetailsInformation => 'Information';
|
||||
|
||||
@override
|
||||
String get extensionUtilityFunctions => 'Utility Functions';
|
||||
|
||||
@override
|
||||
String get actionDismiss => 'Dismiss';
|
||||
|
||||
@override
|
||||
String get setupChangeFolderTooltip => 'Change folder';
|
||||
|
||||
@override
|
||||
String a11yOpenTrackByArtist(String trackName, String artistName) {
|
||||
return 'Open track $trackName by $artistName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenItem(String itemType, String name) {
|
||||
return 'Open $itemType $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenItemCount(String title, int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'items',
|
||||
one: 'item',
|
||||
);
|
||||
return 'Open $title, $count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenAlbumByArtistTrackCount(
|
||||
String albumName,
|
||||
String artistName,
|
||||
int trackCount,
|
||||
) {
|
||||
return 'Open album $albumName by $artistName, $trackCount tracks';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yTrackByArtist(String trackName, String artistName) {
|
||||
return '$trackName by $artistName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11ySelectAlbum(String albumName) {
|
||||
return 'Select album $albumName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenAlbum(String albumName) {
|
||||
return 'Open album $albumName';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,13 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
String get homeSubtitle =>
|
||||
'Tempel URL yang didukung atau cari berdasarkan nama';
|
||||
|
||||
@override
|
||||
String get homeEmptyTitle => 'Home is empty';
|
||||
|
||||
@override
|
||||
String get homeEmptySubtitle =>
|
||||
'Install your first extension to unlock search and browsing.';
|
||||
|
||||
@override
|
||||
String get homeSupports => 'Mendukung: URL Track, Album, Playlist, Artis';
|
||||
|
||||
@@ -3434,4 +3441,223 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
@override
|
||||
String get notifUpdateFailedBody =>
|
||||
'Could not download update. Try again later.';
|
||||
|
||||
@override
|
||||
String get searchTracks => 'Tracks';
|
||||
|
||||
@override
|
||||
String get homeSearchHintDefault => 'Paste supported URL or search...';
|
||||
|
||||
@override
|
||||
String homeSearchHintProvider(String providerName) {
|
||||
return 'Search with $providerName...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get homeImportCsvTooltip => 'Import CSV';
|
||||
|
||||
@override
|
||||
String get homeChangeSearchProviderTooltip => 'Change search provider';
|
||||
|
||||
@override
|
||||
String get actionPaste => 'Paste';
|
||||
|
||||
@override
|
||||
String get searchTracksHint => 'Search tracks...';
|
||||
|
||||
@override
|
||||
String get searchTracksEmptyPrompt => 'Search for tracks';
|
||||
|
||||
@override
|
||||
String get tutorialSearchHint => 'Paste or search...';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadCompletedSemantics => 'Download completed';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadInProgressSemantics => 'Download in progress';
|
||||
|
||||
@override
|
||||
String get tutorialStartDownloadSemantics => 'Start download';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadata => 'Embed Metadata';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadataSubtitleOn =>
|
||||
'Write metadata, cover art, and embedded lyrics to files';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadataSubtitleOff =>
|
||||
'Disabled (advanced): skip all metadata embedding';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCoverSubtitleDisabled =>
|
||||
'Disabled when metadata embedding is off';
|
||||
|
||||
@override
|
||||
String downloadFilenameHintExample(Object artist, Object title) {
|
||||
return '$artist - $title';
|
||||
}
|
||||
|
||||
@override
|
||||
String get trackCoverNoEmbeddedArt => 'No embedded album art found';
|
||||
|
||||
@override
|
||||
String get trackCoverReplace => 'Replace Cover';
|
||||
|
||||
@override
|
||||
String get trackCoverPick => 'Pick Cover';
|
||||
|
||||
@override
|
||||
String get trackCoverClearSelected => 'Clear selected cover';
|
||||
|
||||
@override
|
||||
String get trackCoverCurrent => 'Current cover';
|
||||
|
||||
@override
|
||||
String get trackCoverSelected => 'Selected cover';
|
||||
|
||||
@override
|
||||
String get trackCoverReplaceNotice =>
|
||||
'The selected cover will replace the current embedded cover when you tap Save.';
|
||||
|
||||
@override
|
||||
String get actionStop => 'Stop';
|
||||
|
||||
@override
|
||||
String get queueFinalizingDownload => 'Finalizing download';
|
||||
|
||||
@override
|
||||
String get queueDownloadedFileMissing => 'Downloaded file missing';
|
||||
|
||||
@override
|
||||
String get queueDownloadCompleted => 'Download completed';
|
||||
|
||||
@override
|
||||
String appearanceSelectAccentColor(String hex) {
|
||||
return 'Select accent color $hex';
|
||||
}
|
||||
|
||||
@override
|
||||
String get logAutoScrollOn => 'Auto-scroll ON';
|
||||
|
||||
@override
|
||||
String get logAutoScrollOff => 'Auto-scroll OFF';
|
||||
|
||||
@override
|
||||
String get logCopyLogs => 'Copy logs';
|
||||
|
||||
@override
|
||||
String get logClearSearch => 'Clear search';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingLabel => 'ISP BLOCKING DETECTED';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingDescription =>
|
||||
'Your ISP may be blocking access to download services';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingSuggestion =>
|
||||
'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedLabel => 'RATE LIMITED';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedDescription =>
|
||||
'Too many requests to the service';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedSuggestion =>
|
||||
'Wait a few minutes before trying again';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorLabel => 'NETWORK ERROR';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorDescription => 'Connection issues detected';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorSuggestion => 'Check your internet connection';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundLabel => 'TRACK NOT FOUND';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundDescription =>
|
||||
'Some tracks could not be found on download services';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundSuggestion =>
|
||||
'The track may not be available in lossless quality';
|
||||
|
||||
@override
|
||||
String get clickableLookingUpArtist => 'Looking up artist...';
|
||||
|
||||
@override
|
||||
String clickableInformationUnavailable(String type) {
|
||||
return '$type information not available';
|
||||
}
|
||||
|
||||
@override
|
||||
String get extensionDetailsTags => 'Tags';
|
||||
|
||||
@override
|
||||
String get extensionDetailsInformation => 'Information';
|
||||
|
||||
@override
|
||||
String get extensionUtilityFunctions => 'Utility Functions';
|
||||
|
||||
@override
|
||||
String get actionDismiss => 'Dismiss';
|
||||
|
||||
@override
|
||||
String get setupChangeFolderTooltip => 'Change folder';
|
||||
|
||||
@override
|
||||
String a11yOpenTrackByArtist(String trackName, String artistName) {
|
||||
return 'Open track $trackName by $artistName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenItem(String itemType, String name) {
|
||||
return 'Open $itemType $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenItemCount(String title, int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'items',
|
||||
one: 'item',
|
||||
);
|
||||
return 'Open $title, $count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenAlbumByArtistTrackCount(
|
||||
String albumName,
|
||||
String artistName,
|
||||
int trackCount,
|
||||
) {
|
||||
return 'Open album $albumName by $artistName, $trackCount tracks';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yTrackByArtist(String trackName, String artistName) {
|
||||
return '$trackName by $artistName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11ySelectAlbum(String albumName) {
|
||||
return 'Select album $albumName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenAlbum(String albumName) {
|
||||
return 'Open album $albumName';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,13 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get homeSubtitle => 'Spotify のリンクを貼り付けるか、名前で検索します';
|
||||
|
||||
@override
|
||||
String get homeEmptyTitle => 'Home is empty';
|
||||
|
||||
@override
|
||||
String get homeEmptySubtitle =>
|
||||
'Install your first extension to unlock search and browsing.';
|
||||
|
||||
@override
|
||||
String get homeSupports => 'サポート: トラック、アルバム、プレイリスト、アーティスト、URL';
|
||||
|
||||
@@ -3410,4 +3417,223 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get notifUpdateFailedBody =>
|
||||
'Could not download update. Try again later.';
|
||||
|
||||
@override
|
||||
String get searchTracks => 'Tracks';
|
||||
|
||||
@override
|
||||
String get homeSearchHintDefault => 'Paste supported URL or search...';
|
||||
|
||||
@override
|
||||
String homeSearchHintProvider(String providerName) {
|
||||
return 'Search with $providerName...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get homeImportCsvTooltip => 'Import CSV';
|
||||
|
||||
@override
|
||||
String get homeChangeSearchProviderTooltip => 'Change search provider';
|
||||
|
||||
@override
|
||||
String get actionPaste => 'Paste';
|
||||
|
||||
@override
|
||||
String get searchTracksHint => 'Search tracks...';
|
||||
|
||||
@override
|
||||
String get searchTracksEmptyPrompt => 'Search for tracks';
|
||||
|
||||
@override
|
||||
String get tutorialSearchHint => 'Paste or search...';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadCompletedSemantics => 'Download completed';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadInProgressSemantics => 'Download in progress';
|
||||
|
||||
@override
|
||||
String get tutorialStartDownloadSemantics => 'Start download';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadata => 'Embed Metadata';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadataSubtitleOn =>
|
||||
'Write metadata, cover art, and embedded lyrics to files';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadataSubtitleOff =>
|
||||
'Disabled (advanced): skip all metadata embedding';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCoverSubtitleDisabled =>
|
||||
'Disabled when metadata embedding is off';
|
||||
|
||||
@override
|
||||
String downloadFilenameHintExample(Object artist, Object title) {
|
||||
return '$artist - $title';
|
||||
}
|
||||
|
||||
@override
|
||||
String get trackCoverNoEmbeddedArt => 'No embedded album art found';
|
||||
|
||||
@override
|
||||
String get trackCoverReplace => 'Replace Cover';
|
||||
|
||||
@override
|
||||
String get trackCoverPick => 'Pick Cover';
|
||||
|
||||
@override
|
||||
String get trackCoverClearSelected => 'Clear selected cover';
|
||||
|
||||
@override
|
||||
String get trackCoverCurrent => 'Current cover';
|
||||
|
||||
@override
|
||||
String get trackCoverSelected => 'Selected cover';
|
||||
|
||||
@override
|
||||
String get trackCoverReplaceNotice =>
|
||||
'The selected cover will replace the current embedded cover when you tap Save.';
|
||||
|
||||
@override
|
||||
String get actionStop => 'Stop';
|
||||
|
||||
@override
|
||||
String get queueFinalizingDownload => 'Finalizing download';
|
||||
|
||||
@override
|
||||
String get queueDownloadedFileMissing => 'Downloaded file missing';
|
||||
|
||||
@override
|
||||
String get queueDownloadCompleted => 'Download completed';
|
||||
|
||||
@override
|
||||
String appearanceSelectAccentColor(String hex) {
|
||||
return 'Select accent color $hex';
|
||||
}
|
||||
|
||||
@override
|
||||
String get logAutoScrollOn => 'Auto-scroll ON';
|
||||
|
||||
@override
|
||||
String get logAutoScrollOff => 'Auto-scroll OFF';
|
||||
|
||||
@override
|
||||
String get logCopyLogs => 'Copy logs';
|
||||
|
||||
@override
|
||||
String get logClearSearch => 'Clear search';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingLabel => 'ISP BLOCKING DETECTED';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingDescription =>
|
||||
'Your ISP may be blocking access to download services';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingSuggestion =>
|
||||
'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedLabel => 'RATE LIMITED';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedDescription =>
|
||||
'Too many requests to the service';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedSuggestion =>
|
||||
'Wait a few minutes before trying again';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorLabel => 'NETWORK ERROR';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorDescription => 'Connection issues detected';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorSuggestion => 'Check your internet connection';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundLabel => 'TRACK NOT FOUND';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundDescription =>
|
||||
'Some tracks could not be found on download services';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundSuggestion =>
|
||||
'The track may not be available in lossless quality';
|
||||
|
||||
@override
|
||||
String get clickableLookingUpArtist => 'Looking up artist...';
|
||||
|
||||
@override
|
||||
String clickableInformationUnavailable(String type) {
|
||||
return '$type information not available';
|
||||
}
|
||||
|
||||
@override
|
||||
String get extensionDetailsTags => 'Tags';
|
||||
|
||||
@override
|
||||
String get extensionDetailsInformation => 'Information';
|
||||
|
||||
@override
|
||||
String get extensionUtilityFunctions => 'Utility Functions';
|
||||
|
||||
@override
|
||||
String get actionDismiss => 'Dismiss';
|
||||
|
||||
@override
|
||||
String get setupChangeFolderTooltip => 'Change folder';
|
||||
|
||||
@override
|
||||
String a11yOpenTrackByArtist(String trackName, String artistName) {
|
||||
return 'Open track $trackName by $artistName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenItem(String itemType, String name) {
|
||||
return 'Open $itemType $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenItemCount(String title, int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'items',
|
||||
one: 'item',
|
||||
);
|
||||
return 'Open $title, $count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenAlbumByArtistTrackCount(
|
||||
String albumName,
|
||||
String artistName,
|
||||
int trackCount,
|
||||
) {
|
||||
return 'Open album $albumName by $artistName, $trackCount tracks';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yTrackByArtist(String trackName, String artistName) {
|
||||
return '$trackName by $artistName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11ySelectAlbum(String albumName) {
|
||||
return 'Select album $albumName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenAlbum(String albumName) {
|
||||
return 'Open album $albumName';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,13 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get homeSubtitle => 'Spotify URL을 붙여 넣거나 검색';
|
||||
|
||||
@override
|
||||
String get homeEmptyTitle => 'Home is empty';
|
||||
|
||||
@override
|
||||
String get homeEmptySubtitle =>
|
||||
'Install your first extension to unlock search and browsing.';
|
||||
|
||||
@override
|
||||
String get homeSupports => '지원 항목: 트랙, 앨범, 플레이리스트, 아티스트 URLs';
|
||||
|
||||
@@ -3403,4 +3410,223 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get notifUpdateFailedBody =>
|
||||
'Could not download update. Try again later.';
|
||||
|
||||
@override
|
||||
String get searchTracks => 'Tracks';
|
||||
|
||||
@override
|
||||
String get homeSearchHintDefault => 'Paste supported URL or search...';
|
||||
|
||||
@override
|
||||
String homeSearchHintProvider(String providerName) {
|
||||
return 'Search with $providerName...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get homeImportCsvTooltip => 'Import CSV';
|
||||
|
||||
@override
|
||||
String get homeChangeSearchProviderTooltip => 'Change search provider';
|
||||
|
||||
@override
|
||||
String get actionPaste => 'Paste';
|
||||
|
||||
@override
|
||||
String get searchTracksHint => 'Search tracks...';
|
||||
|
||||
@override
|
||||
String get searchTracksEmptyPrompt => 'Search for tracks';
|
||||
|
||||
@override
|
||||
String get tutorialSearchHint => 'Paste or search...';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadCompletedSemantics => 'Download completed';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadInProgressSemantics => 'Download in progress';
|
||||
|
||||
@override
|
||||
String get tutorialStartDownloadSemantics => 'Start download';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadata => 'Embed Metadata';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadataSubtitleOn =>
|
||||
'Write metadata, cover art, and embedded lyrics to files';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadataSubtitleOff =>
|
||||
'Disabled (advanced): skip all metadata embedding';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCoverSubtitleDisabled =>
|
||||
'Disabled when metadata embedding is off';
|
||||
|
||||
@override
|
||||
String downloadFilenameHintExample(Object artist, Object title) {
|
||||
return '$artist - $title';
|
||||
}
|
||||
|
||||
@override
|
||||
String get trackCoverNoEmbeddedArt => 'No embedded album art found';
|
||||
|
||||
@override
|
||||
String get trackCoverReplace => 'Replace Cover';
|
||||
|
||||
@override
|
||||
String get trackCoverPick => 'Pick Cover';
|
||||
|
||||
@override
|
||||
String get trackCoverClearSelected => 'Clear selected cover';
|
||||
|
||||
@override
|
||||
String get trackCoverCurrent => 'Current cover';
|
||||
|
||||
@override
|
||||
String get trackCoverSelected => 'Selected cover';
|
||||
|
||||
@override
|
||||
String get trackCoverReplaceNotice =>
|
||||
'The selected cover will replace the current embedded cover when you tap Save.';
|
||||
|
||||
@override
|
||||
String get actionStop => 'Stop';
|
||||
|
||||
@override
|
||||
String get queueFinalizingDownload => 'Finalizing download';
|
||||
|
||||
@override
|
||||
String get queueDownloadedFileMissing => 'Downloaded file missing';
|
||||
|
||||
@override
|
||||
String get queueDownloadCompleted => 'Download completed';
|
||||
|
||||
@override
|
||||
String appearanceSelectAccentColor(String hex) {
|
||||
return 'Select accent color $hex';
|
||||
}
|
||||
|
||||
@override
|
||||
String get logAutoScrollOn => 'Auto-scroll ON';
|
||||
|
||||
@override
|
||||
String get logAutoScrollOff => 'Auto-scroll OFF';
|
||||
|
||||
@override
|
||||
String get logCopyLogs => 'Copy logs';
|
||||
|
||||
@override
|
||||
String get logClearSearch => 'Clear search';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingLabel => 'ISP BLOCKING DETECTED';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingDescription =>
|
||||
'Your ISP may be blocking access to download services';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingSuggestion =>
|
||||
'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedLabel => 'RATE LIMITED';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedDescription =>
|
||||
'Too many requests to the service';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedSuggestion =>
|
||||
'Wait a few minutes before trying again';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorLabel => 'NETWORK ERROR';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorDescription => 'Connection issues detected';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorSuggestion => 'Check your internet connection';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundLabel => 'TRACK NOT FOUND';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundDescription =>
|
||||
'Some tracks could not be found on download services';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundSuggestion =>
|
||||
'The track may not be available in lossless quality';
|
||||
|
||||
@override
|
||||
String get clickableLookingUpArtist => 'Looking up artist...';
|
||||
|
||||
@override
|
||||
String clickableInformationUnavailable(String type) {
|
||||
return '$type information not available';
|
||||
}
|
||||
|
||||
@override
|
||||
String get extensionDetailsTags => 'Tags';
|
||||
|
||||
@override
|
||||
String get extensionDetailsInformation => 'Information';
|
||||
|
||||
@override
|
||||
String get extensionUtilityFunctions => 'Utility Functions';
|
||||
|
||||
@override
|
||||
String get actionDismiss => 'Dismiss';
|
||||
|
||||
@override
|
||||
String get setupChangeFolderTooltip => 'Change folder';
|
||||
|
||||
@override
|
||||
String a11yOpenTrackByArtist(String trackName, String artistName) {
|
||||
return 'Open track $trackName by $artistName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenItem(String itemType, String name) {
|
||||
return 'Open $itemType $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenItemCount(String title, int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'items',
|
||||
one: 'item',
|
||||
);
|
||||
return 'Open $title, $count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenAlbumByArtistTrackCount(
|
||||
String albumName,
|
||||
String artistName,
|
||||
int trackCount,
|
||||
) {
|
||||
return 'Open album $albumName by $artistName, $trackCount tracks';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yTrackByArtist(String trackName, String artistName) {
|
||||
return '$trackName by $artistName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11ySelectAlbum(String albumName) {
|
||||
return 'Select album $albumName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenAlbum(String albumName) {
|
||||
return 'Open album $albumName';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,13 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get homeSubtitle => 'Paste a Spotify link or search by name';
|
||||
|
||||
@override
|
||||
String get homeEmptyTitle => 'Home is empty';
|
||||
|
||||
@override
|
||||
String get homeEmptySubtitle =>
|
||||
'Install your first extension to unlock search and browsing.';
|
||||
|
||||
@override
|
||||
String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs';
|
||||
|
||||
@@ -3423,4 +3430,223 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get notifUpdateFailedBody =>
|
||||
'Could not download update. Try again later.';
|
||||
|
||||
@override
|
||||
String get searchTracks => 'Tracks';
|
||||
|
||||
@override
|
||||
String get homeSearchHintDefault => 'Paste supported URL or search...';
|
||||
|
||||
@override
|
||||
String homeSearchHintProvider(String providerName) {
|
||||
return 'Search with $providerName...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get homeImportCsvTooltip => 'Import CSV';
|
||||
|
||||
@override
|
||||
String get homeChangeSearchProviderTooltip => 'Change search provider';
|
||||
|
||||
@override
|
||||
String get actionPaste => 'Paste';
|
||||
|
||||
@override
|
||||
String get searchTracksHint => 'Search tracks...';
|
||||
|
||||
@override
|
||||
String get searchTracksEmptyPrompt => 'Search for tracks';
|
||||
|
||||
@override
|
||||
String get tutorialSearchHint => 'Paste or search...';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadCompletedSemantics => 'Download completed';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadInProgressSemantics => 'Download in progress';
|
||||
|
||||
@override
|
||||
String get tutorialStartDownloadSemantics => 'Start download';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadata => 'Embed Metadata';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadataSubtitleOn =>
|
||||
'Write metadata, cover art, and embedded lyrics to files';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadataSubtitleOff =>
|
||||
'Disabled (advanced): skip all metadata embedding';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCoverSubtitleDisabled =>
|
||||
'Disabled when metadata embedding is off';
|
||||
|
||||
@override
|
||||
String downloadFilenameHintExample(Object artist, Object title) {
|
||||
return '$artist - $title';
|
||||
}
|
||||
|
||||
@override
|
||||
String get trackCoverNoEmbeddedArt => 'No embedded album art found';
|
||||
|
||||
@override
|
||||
String get trackCoverReplace => 'Replace Cover';
|
||||
|
||||
@override
|
||||
String get trackCoverPick => 'Pick Cover';
|
||||
|
||||
@override
|
||||
String get trackCoverClearSelected => 'Clear selected cover';
|
||||
|
||||
@override
|
||||
String get trackCoverCurrent => 'Current cover';
|
||||
|
||||
@override
|
||||
String get trackCoverSelected => 'Selected cover';
|
||||
|
||||
@override
|
||||
String get trackCoverReplaceNotice =>
|
||||
'The selected cover will replace the current embedded cover when you tap Save.';
|
||||
|
||||
@override
|
||||
String get actionStop => 'Stop';
|
||||
|
||||
@override
|
||||
String get queueFinalizingDownload => 'Finalizing download';
|
||||
|
||||
@override
|
||||
String get queueDownloadedFileMissing => 'Downloaded file missing';
|
||||
|
||||
@override
|
||||
String get queueDownloadCompleted => 'Download completed';
|
||||
|
||||
@override
|
||||
String appearanceSelectAccentColor(String hex) {
|
||||
return 'Select accent color $hex';
|
||||
}
|
||||
|
||||
@override
|
||||
String get logAutoScrollOn => 'Auto-scroll ON';
|
||||
|
||||
@override
|
||||
String get logAutoScrollOff => 'Auto-scroll OFF';
|
||||
|
||||
@override
|
||||
String get logCopyLogs => 'Copy logs';
|
||||
|
||||
@override
|
||||
String get logClearSearch => 'Clear search';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingLabel => 'ISP BLOCKING DETECTED';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingDescription =>
|
||||
'Your ISP may be blocking access to download services';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingSuggestion =>
|
||||
'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedLabel => 'RATE LIMITED';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedDescription =>
|
||||
'Too many requests to the service';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedSuggestion =>
|
||||
'Wait a few minutes before trying again';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorLabel => 'NETWORK ERROR';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorDescription => 'Connection issues detected';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorSuggestion => 'Check your internet connection';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundLabel => 'TRACK NOT FOUND';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundDescription =>
|
||||
'Some tracks could not be found on download services';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundSuggestion =>
|
||||
'The track may not be available in lossless quality';
|
||||
|
||||
@override
|
||||
String get clickableLookingUpArtist => 'Looking up artist...';
|
||||
|
||||
@override
|
||||
String clickableInformationUnavailable(String type) {
|
||||
return '$type information not available';
|
||||
}
|
||||
|
||||
@override
|
||||
String get extensionDetailsTags => 'Tags';
|
||||
|
||||
@override
|
||||
String get extensionDetailsInformation => 'Information';
|
||||
|
||||
@override
|
||||
String get extensionUtilityFunctions => 'Utility Functions';
|
||||
|
||||
@override
|
||||
String get actionDismiss => 'Dismiss';
|
||||
|
||||
@override
|
||||
String get setupChangeFolderTooltip => 'Change folder';
|
||||
|
||||
@override
|
||||
String a11yOpenTrackByArtist(String trackName, String artistName) {
|
||||
return 'Open track $trackName by $artistName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenItem(String itemType, String name) {
|
||||
return 'Open $itemType $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenItemCount(String title, int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'items',
|
||||
one: 'item',
|
||||
);
|
||||
return 'Open $title, $count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenAlbumByArtistTrackCount(
|
||||
String albumName,
|
||||
String artistName,
|
||||
int trackCount,
|
||||
) {
|
||||
return 'Open album $albumName by $artistName, $trackCount tracks';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yTrackByArtist(String trackName, String artistName) {
|
||||
return '$trackName by $artistName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11ySelectAlbum(String albumName) {
|
||||
return 'Select album $albumName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenAlbum(String albumName) {
|
||||
return 'Open album $albumName';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,13 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get homeSubtitle => 'Paste a Spotify link or search by name';
|
||||
|
||||
@override
|
||||
String get homeEmptyTitle => 'Home is empty';
|
||||
|
||||
@override
|
||||
String get homeEmptySubtitle =>
|
||||
'Install your first extension to unlock search and browsing.';
|
||||
|
||||
@override
|
||||
String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs';
|
||||
|
||||
@@ -3424,6 +3431,225 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get notifUpdateFailedBody =>
|
||||
'Could not download update. Try again later.';
|
||||
|
||||
@override
|
||||
String get searchTracks => 'Tracks';
|
||||
|
||||
@override
|
||||
String get homeSearchHintDefault => 'Paste supported URL or search...';
|
||||
|
||||
@override
|
||||
String homeSearchHintProvider(String providerName) {
|
||||
return 'Search with $providerName...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get homeImportCsvTooltip => 'Import CSV';
|
||||
|
||||
@override
|
||||
String get homeChangeSearchProviderTooltip => 'Change search provider';
|
||||
|
||||
@override
|
||||
String get actionPaste => 'Paste';
|
||||
|
||||
@override
|
||||
String get searchTracksHint => 'Search tracks...';
|
||||
|
||||
@override
|
||||
String get searchTracksEmptyPrompt => 'Search for tracks';
|
||||
|
||||
@override
|
||||
String get tutorialSearchHint => 'Paste or search...';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadCompletedSemantics => 'Download completed';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadInProgressSemantics => 'Download in progress';
|
||||
|
||||
@override
|
||||
String get tutorialStartDownloadSemantics => 'Start download';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadata => 'Embed Metadata';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadataSubtitleOn =>
|
||||
'Write metadata, cover art, and embedded lyrics to files';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadataSubtitleOff =>
|
||||
'Disabled (advanced): skip all metadata embedding';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCoverSubtitleDisabled =>
|
||||
'Disabled when metadata embedding is off';
|
||||
|
||||
@override
|
||||
String downloadFilenameHintExample(Object artist, Object title) {
|
||||
return '$artist - $title';
|
||||
}
|
||||
|
||||
@override
|
||||
String get trackCoverNoEmbeddedArt => 'No embedded album art found';
|
||||
|
||||
@override
|
||||
String get trackCoverReplace => 'Replace Cover';
|
||||
|
||||
@override
|
||||
String get trackCoverPick => 'Pick Cover';
|
||||
|
||||
@override
|
||||
String get trackCoverClearSelected => 'Clear selected cover';
|
||||
|
||||
@override
|
||||
String get trackCoverCurrent => 'Current cover';
|
||||
|
||||
@override
|
||||
String get trackCoverSelected => 'Selected cover';
|
||||
|
||||
@override
|
||||
String get trackCoverReplaceNotice =>
|
||||
'The selected cover will replace the current embedded cover when you tap Save.';
|
||||
|
||||
@override
|
||||
String get actionStop => 'Stop';
|
||||
|
||||
@override
|
||||
String get queueFinalizingDownload => 'Finalizing download';
|
||||
|
||||
@override
|
||||
String get queueDownloadedFileMissing => 'Downloaded file missing';
|
||||
|
||||
@override
|
||||
String get queueDownloadCompleted => 'Download completed';
|
||||
|
||||
@override
|
||||
String appearanceSelectAccentColor(String hex) {
|
||||
return 'Select accent color $hex';
|
||||
}
|
||||
|
||||
@override
|
||||
String get logAutoScrollOn => 'Auto-scroll ON';
|
||||
|
||||
@override
|
||||
String get logAutoScrollOff => 'Auto-scroll OFF';
|
||||
|
||||
@override
|
||||
String get logCopyLogs => 'Copy logs';
|
||||
|
||||
@override
|
||||
String get logClearSearch => 'Clear search';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingLabel => 'ISP BLOCKING DETECTED';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingDescription =>
|
||||
'Your ISP may be blocking access to download services';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingSuggestion =>
|
||||
'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedLabel => 'RATE LIMITED';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedDescription =>
|
||||
'Too many requests to the service';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedSuggestion =>
|
||||
'Wait a few minutes before trying again';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorLabel => 'NETWORK ERROR';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorDescription => 'Connection issues detected';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorSuggestion => 'Check your internet connection';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundLabel => 'TRACK NOT FOUND';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundDescription =>
|
||||
'Some tracks could not be found on download services';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundSuggestion =>
|
||||
'The track may not be available in lossless quality';
|
||||
|
||||
@override
|
||||
String get clickableLookingUpArtist => 'Looking up artist...';
|
||||
|
||||
@override
|
||||
String clickableInformationUnavailable(String type) {
|
||||
return '$type information not available';
|
||||
}
|
||||
|
||||
@override
|
||||
String get extensionDetailsTags => 'Tags';
|
||||
|
||||
@override
|
||||
String get extensionDetailsInformation => 'Information';
|
||||
|
||||
@override
|
||||
String get extensionUtilityFunctions => 'Utility Functions';
|
||||
|
||||
@override
|
||||
String get actionDismiss => 'Dismiss';
|
||||
|
||||
@override
|
||||
String get setupChangeFolderTooltip => 'Change folder';
|
||||
|
||||
@override
|
||||
String a11yOpenTrackByArtist(String trackName, String artistName) {
|
||||
return 'Open track $trackName by $artistName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenItem(String itemType, String name) {
|
||||
return 'Open $itemType $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenItemCount(String title, int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'items',
|
||||
one: 'item',
|
||||
);
|
||||
return 'Open $title, $count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenAlbumByArtistTrackCount(
|
||||
String albumName,
|
||||
String artistName,
|
||||
int trackCount,
|
||||
) {
|
||||
return 'Open album $albumName by $artistName, $trackCount tracks';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yTrackByArtist(String trackName, String artistName) {
|
||||
return '$trackName by $artistName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11ySelectAlbum(String albumName) {
|
||||
return 'Select album $albumName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenAlbum(String albumName) {
|
||||
return 'Open album $albumName';
|
||||
}
|
||||
}
|
||||
|
||||
/// The translations for Portuguese, as used in Portugal (`pt_PT`).
|
||||
|
||||
@@ -29,6 +29,13 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get homeSubtitle => 'Вставьте ссылку Spotify или ищите по названию';
|
||||
|
||||
@override
|
||||
String get homeEmptyTitle => 'Home is empty';
|
||||
|
||||
@override
|
||||
String get homeEmptySubtitle =>
|
||||
'Install your first extension to unlock search and browsing.';
|
||||
|
||||
@override
|
||||
String get homeSupports =>
|
||||
'Поддерживается: Трек, Альбом, Плейлист, URL исполнителя';
|
||||
@@ -3483,4 +3490,223 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get notifUpdateFailedBody =>
|
||||
'Could not download update. Try again later.';
|
||||
|
||||
@override
|
||||
String get searchTracks => 'Tracks';
|
||||
|
||||
@override
|
||||
String get homeSearchHintDefault => 'Paste supported URL or search...';
|
||||
|
||||
@override
|
||||
String homeSearchHintProvider(String providerName) {
|
||||
return 'Search with $providerName...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get homeImportCsvTooltip => 'Import CSV';
|
||||
|
||||
@override
|
||||
String get homeChangeSearchProviderTooltip => 'Change search provider';
|
||||
|
||||
@override
|
||||
String get actionPaste => 'Paste';
|
||||
|
||||
@override
|
||||
String get searchTracksHint => 'Search tracks...';
|
||||
|
||||
@override
|
||||
String get searchTracksEmptyPrompt => 'Search for tracks';
|
||||
|
||||
@override
|
||||
String get tutorialSearchHint => 'Paste or search...';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadCompletedSemantics => 'Download completed';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadInProgressSemantics => 'Download in progress';
|
||||
|
||||
@override
|
||||
String get tutorialStartDownloadSemantics => 'Start download';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadata => 'Embed Metadata';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadataSubtitleOn =>
|
||||
'Write metadata, cover art, and embedded lyrics to files';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadataSubtitleOff =>
|
||||
'Disabled (advanced): skip all metadata embedding';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCoverSubtitleDisabled =>
|
||||
'Disabled when metadata embedding is off';
|
||||
|
||||
@override
|
||||
String downloadFilenameHintExample(Object artist, Object title) {
|
||||
return '$artist - $title';
|
||||
}
|
||||
|
||||
@override
|
||||
String get trackCoverNoEmbeddedArt => 'No embedded album art found';
|
||||
|
||||
@override
|
||||
String get trackCoverReplace => 'Replace Cover';
|
||||
|
||||
@override
|
||||
String get trackCoverPick => 'Pick Cover';
|
||||
|
||||
@override
|
||||
String get trackCoverClearSelected => 'Clear selected cover';
|
||||
|
||||
@override
|
||||
String get trackCoverCurrent => 'Current cover';
|
||||
|
||||
@override
|
||||
String get trackCoverSelected => 'Selected cover';
|
||||
|
||||
@override
|
||||
String get trackCoverReplaceNotice =>
|
||||
'The selected cover will replace the current embedded cover when you tap Save.';
|
||||
|
||||
@override
|
||||
String get actionStop => 'Stop';
|
||||
|
||||
@override
|
||||
String get queueFinalizingDownload => 'Finalizing download';
|
||||
|
||||
@override
|
||||
String get queueDownloadedFileMissing => 'Downloaded file missing';
|
||||
|
||||
@override
|
||||
String get queueDownloadCompleted => 'Download completed';
|
||||
|
||||
@override
|
||||
String appearanceSelectAccentColor(String hex) {
|
||||
return 'Select accent color $hex';
|
||||
}
|
||||
|
||||
@override
|
||||
String get logAutoScrollOn => 'Auto-scroll ON';
|
||||
|
||||
@override
|
||||
String get logAutoScrollOff => 'Auto-scroll OFF';
|
||||
|
||||
@override
|
||||
String get logCopyLogs => 'Copy logs';
|
||||
|
||||
@override
|
||||
String get logClearSearch => 'Clear search';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingLabel => 'ISP BLOCKING DETECTED';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingDescription =>
|
||||
'Your ISP may be blocking access to download services';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingSuggestion =>
|
||||
'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedLabel => 'RATE LIMITED';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedDescription =>
|
||||
'Too many requests to the service';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedSuggestion =>
|
||||
'Wait a few minutes before trying again';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorLabel => 'NETWORK ERROR';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorDescription => 'Connection issues detected';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorSuggestion => 'Check your internet connection';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundLabel => 'TRACK NOT FOUND';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundDescription =>
|
||||
'Some tracks could not be found on download services';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundSuggestion =>
|
||||
'The track may not be available in lossless quality';
|
||||
|
||||
@override
|
||||
String get clickableLookingUpArtist => 'Looking up artist...';
|
||||
|
||||
@override
|
||||
String clickableInformationUnavailable(String type) {
|
||||
return '$type information not available';
|
||||
}
|
||||
|
||||
@override
|
||||
String get extensionDetailsTags => 'Tags';
|
||||
|
||||
@override
|
||||
String get extensionDetailsInformation => 'Information';
|
||||
|
||||
@override
|
||||
String get extensionUtilityFunctions => 'Utility Functions';
|
||||
|
||||
@override
|
||||
String get actionDismiss => 'Dismiss';
|
||||
|
||||
@override
|
||||
String get setupChangeFolderTooltip => 'Change folder';
|
||||
|
||||
@override
|
||||
String a11yOpenTrackByArtist(String trackName, String artistName) {
|
||||
return 'Open track $trackName by $artistName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenItem(String itemType, String name) {
|
||||
return 'Open $itemType $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenItemCount(String title, int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'items',
|
||||
one: 'item',
|
||||
);
|
||||
return 'Open $title, $count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenAlbumByArtistTrackCount(
|
||||
String albumName,
|
||||
String artistName,
|
||||
int trackCount,
|
||||
) {
|
||||
return 'Open album $albumName by $artistName, $trackCount tracks';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yTrackByArtist(String trackName, String artistName) {
|
||||
return '$trackName by $artistName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11ySelectAlbum(String albumName) {
|
||||
return 'Select album $albumName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenAlbum(String albumName) {
|
||||
return 'Open album $albumName';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,13 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
String get homeSubtitle =>
|
||||
'Bir Spotify bağlantısı yapıştırın veya şarkı arayın';
|
||||
|
||||
@override
|
||||
String get homeEmptyTitle => 'Home is empty';
|
||||
|
||||
@override
|
||||
String get homeEmptySubtitle =>
|
||||
'Install your first extension to unlock search and browsing.';
|
||||
|
||||
@override
|
||||
String get homeSupports =>
|
||||
'Desteklenenler: Şarkı, Albüm, Çalma Listesi, Sanatçı bağlantıları';
|
||||
@@ -3481,4 +3488,223 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
@override
|
||||
String get notifUpdateFailedBody =>
|
||||
'Could not download update. Try again later.';
|
||||
|
||||
@override
|
||||
String get searchTracks => 'Tracks';
|
||||
|
||||
@override
|
||||
String get homeSearchHintDefault => 'Paste supported URL or search...';
|
||||
|
||||
@override
|
||||
String homeSearchHintProvider(String providerName) {
|
||||
return 'Search with $providerName...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get homeImportCsvTooltip => 'Import CSV';
|
||||
|
||||
@override
|
||||
String get homeChangeSearchProviderTooltip => 'Change search provider';
|
||||
|
||||
@override
|
||||
String get actionPaste => 'Paste';
|
||||
|
||||
@override
|
||||
String get searchTracksHint => 'Search tracks...';
|
||||
|
||||
@override
|
||||
String get searchTracksEmptyPrompt => 'Search for tracks';
|
||||
|
||||
@override
|
||||
String get tutorialSearchHint => 'Paste or search...';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadCompletedSemantics => 'Download completed';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadInProgressSemantics => 'Download in progress';
|
||||
|
||||
@override
|
||||
String get tutorialStartDownloadSemantics => 'Start download';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadata => 'Embed Metadata';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadataSubtitleOn =>
|
||||
'Write metadata, cover art, and embedded lyrics to files';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadataSubtitleOff =>
|
||||
'Disabled (advanced): skip all metadata embedding';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCoverSubtitleDisabled =>
|
||||
'Disabled when metadata embedding is off';
|
||||
|
||||
@override
|
||||
String downloadFilenameHintExample(Object artist, Object title) {
|
||||
return '$artist - $title';
|
||||
}
|
||||
|
||||
@override
|
||||
String get trackCoverNoEmbeddedArt => 'No embedded album art found';
|
||||
|
||||
@override
|
||||
String get trackCoverReplace => 'Replace Cover';
|
||||
|
||||
@override
|
||||
String get trackCoverPick => 'Pick Cover';
|
||||
|
||||
@override
|
||||
String get trackCoverClearSelected => 'Clear selected cover';
|
||||
|
||||
@override
|
||||
String get trackCoverCurrent => 'Current cover';
|
||||
|
||||
@override
|
||||
String get trackCoverSelected => 'Selected cover';
|
||||
|
||||
@override
|
||||
String get trackCoverReplaceNotice =>
|
||||
'The selected cover will replace the current embedded cover when you tap Save.';
|
||||
|
||||
@override
|
||||
String get actionStop => 'Stop';
|
||||
|
||||
@override
|
||||
String get queueFinalizingDownload => 'Finalizing download';
|
||||
|
||||
@override
|
||||
String get queueDownloadedFileMissing => 'Downloaded file missing';
|
||||
|
||||
@override
|
||||
String get queueDownloadCompleted => 'Download completed';
|
||||
|
||||
@override
|
||||
String appearanceSelectAccentColor(String hex) {
|
||||
return 'Select accent color $hex';
|
||||
}
|
||||
|
||||
@override
|
||||
String get logAutoScrollOn => 'Auto-scroll ON';
|
||||
|
||||
@override
|
||||
String get logAutoScrollOff => 'Auto-scroll OFF';
|
||||
|
||||
@override
|
||||
String get logCopyLogs => 'Copy logs';
|
||||
|
||||
@override
|
||||
String get logClearSearch => 'Clear search';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingLabel => 'ISP BLOCKING DETECTED';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingDescription =>
|
||||
'Your ISP may be blocking access to download services';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingSuggestion =>
|
||||
'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedLabel => 'RATE LIMITED';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedDescription =>
|
||||
'Too many requests to the service';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedSuggestion =>
|
||||
'Wait a few minutes before trying again';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorLabel => 'NETWORK ERROR';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorDescription => 'Connection issues detected';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorSuggestion => 'Check your internet connection';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundLabel => 'TRACK NOT FOUND';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundDescription =>
|
||||
'Some tracks could not be found on download services';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundSuggestion =>
|
||||
'The track may not be available in lossless quality';
|
||||
|
||||
@override
|
||||
String get clickableLookingUpArtist => 'Looking up artist...';
|
||||
|
||||
@override
|
||||
String clickableInformationUnavailable(String type) {
|
||||
return '$type information not available';
|
||||
}
|
||||
|
||||
@override
|
||||
String get extensionDetailsTags => 'Tags';
|
||||
|
||||
@override
|
||||
String get extensionDetailsInformation => 'Information';
|
||||
|
||||
@override
|
||||
String get extensionUtilityFunctions => 'Utility Functions';
|
||||
|
||||
@override
|
||||
String get actionDismiss => 'Dismiss';
|
||||
|
||||
@override
|
||||
String get setupChangeFolderTooltip => 'Change folder';
|
||||
|
||||
@override
|
||||
String a11yOpenTrackByArtist(String trackName, String artistName) {
|
||||
return 'Open track $trackName by $artistName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenItem(String itemType, String name) {
|
||||
return 'Open $itemType $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenItemCount(String title, int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'items',
|
||||
one: 'item',
|
||||
);
|
||||
return 'Open $title, $count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenAlbumByArtistTrackCount(
|
||||
String albumName,
|
||||
String artistName,
|
||||
int trackCount,
|
||||
) {
|
||||
return 'Open album $albumName by $artistName, $trackCount tracks';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yTrackByArtist(String trackName, String artistName) {
|
||||
return '$trackName by $artistName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11ySelectAlbum(String albumName) {
|
||||
return 'Select album $albumName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenAlbum(String albumName) {
|
||||
return 'Open album $albumName';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,13 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get homeSubtitle => 'Paste a Spotify link or search by name';
|
||||
|
||||
@override
|
||||
String get homeEmptyTitle => 'Home is empty';
|
||||
|
||||
@override
|
||||
String get homeEmptySubtitle =>
|
||||
'Install your first extension to unlock search and browsing.';
|
||||
|
||||
@override
|
||||
String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs';
|
||||
|
||||
@@ -3424,6 +3431,225 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get notifUpdateFailedBody =>
|
||||
'Could not download update. Try again later.';
|
||||
|
||||
@override
|
||||
String get searchTracks => 'Tracks';
|
||||
|
||||
@override
|
||||
String get homeSearchHintDefault => 'Paste supported URL or search...';
|
||||
|
||||
@override
|
||||
String homeSearchHintProvider(String providerName) {
|
||||
return 'Search with $providerName...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get homeImportCsvTooltip => 'Import CSV';
|
||||
|
||||
@override
|
||||
String get homeChangeSearchProviderTooltip => 'Change search provider';
|
||||
|
||||
@override
|
||||
String get actionPaste => 'Paste';
|
||||
|
||||
@override
|
||||
String get searchTracksHint => 'Search tracks...';
|
||||
|
||||
@override
|
||||
String get searchTracksEmptyPrompt => 'Search for tracks';
|
||||
|
||||
@override
|
||||
String get tutorialSearchHint => 'Paste or search...';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadCompletedSemantics => 'Download completed';
|
||||
|
||||
@override
|
||||
String get tutorialDownloadInProgressSemantics => 'Download in progress';
|
||||
|
||||
@override
|
||||
String get tutorialStartDownloadSemantics => 'Start download';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadata => 'Embed Metadata';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadataSubtitleOn =>
|
||||
'Write metadata, cover art, and embedded lyrics to files';
|
||||
|
||||
@override
|
||||
String get optionsEmbedMetadataSubtitleOff =>
|
||||
'Disabled (advanced): skip all metadata embedding';
|
||||
|
||||
@override
|
||||
String get optionsMaxQualityCoverSubtitleDisabled =>
|
||||
'Disabled when metadata embedding is off';
|
||||
|
||||
@override
|
||||
String downloadFilenameHintExample(Object artist, Object title) {
|
||||
return '$artist - $title';
|
||||
}
|
||||
|
||||
@override
|
||||
String get trackCoverNoEmbeddedArt => 'No embedded album art found';
|
||||
|
||||
@override
|
||||
String get trackCoverReplace => 'Replace Cover';
|
||||
|
||||
@override
|
||||
String get trackCoverPick => 'Pick Cover';
|
||||
|
||||
@override
|
||||
String get trackCoverClearSelected => 'Clear selected cover';
|
||||
|
||||
@override
|
||||
String get trackCoverCurrent => 'Current cover';
|
||||
|
||||
@override
|
||||
String get trackCoverSelected => 'Selected cover';
|
||||
|
||||
@override
|
||||
String get trackCoverReplaceNotice =>
|
||||
'The selected cover will replace the current embedded cover when you tap Save.';
|
||||
|
||||
@override
|
||||
String get actionStop => 'Stop';
|
||||
|
||||
@override
|
||||
String get queueFinalizingDownload => 'Finalizing download';
|
||||
|
||||
@override
|
||||
String get queueDownloadedFileMissing => 'Downloaded file missing';
|
||||
|
||||
@override
|
||||
String get queueDownloadCompleted => 'Download completed';
|
||||
|
||||
@override
|
||||
String appearanceSelectAccentColor(String hex) {
|
||||
return 'Select accent color $hex';
|
||||
}
|
||||
|
||||
@override
|
||||
String get logAutoScrollOn => 'Auto-scroll ON';
|
||||
|
||||
@override
|
||||
String get logAutoScrollOff => 'Auto-scroll OFF';
|
||||
|
||||
@override
|
||||
String get logCopyLogs => 'Copy logs';
|
||||
|
||||
@override
|
||||
String get logClearSearch => 'Clear search';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingLabel => 'ISP BLOCKING DETECTED';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingDescription =>
|
||||
'Your ISP may be blocking access to download services';
|
||||
|
||||
@override
|
||||
String get logIssueIspBlockingSuggestion =>
|
||||
'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedLabel => 'RATE LIMITED';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedDescription =>
|
||||
'Too many requests to the service';
|
||||
|
||||
@override
|
||||
String get logIssueRateLimitedSuggestion =>
|
||||
'Wait a few minutes before trying again';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorLabel => 'NETWORK ERROR';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorDescription => 'Connection issues detected';
|
||||
|
||||
@override
|
||||
String get logIssueNetworkErrorSuggestion => 'Check your internet connection';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundLabel => 'TRACK NOT FOUND';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundDescription =>
|
||||
'Some tracks could not be found on download services';
|
||||
|
||||
@override
|
||||
String get logIssueTrackNotFoundSuggestion =>
|
||||
'The track may not be available in lossless quality';
|
||||
|
||||
@override
|
||||
String get clickableLookingUpArtist => 'Looking up artist...';
|
||||
|
||||
@override
|
||||
String clickableInformationUnavailable(String type) {
|
||||
return '$type information not available';
|
||||
}
|
||||
|
||||
@override
|
||||
String get extensionDetailsTags => 'Tags';
|
||||
|
||||
@override
|
||||
String get extensionDetailsInformation => 'Information';
|
||||
|
||||
@override
|
||||
String get extensionUtilityFunctions => 'Utility Functions';
|
||||
|
||||
@override
|
||||
String get actionDismiss => 'Dismiss';
|
||||
|
||||
@override
|
||||
String get setupChangeFolderTooltip => 'Change folder';
|
||||
|
||||
@override
|
||||
String a11yOpenTrackByArtist(String trackName, String artistName) {
|
||||
return 'Open track $trackName by $artistName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenItem(String itemType, String name) {
|
||||
return 'Open $itemType $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenItemCount(String title, int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'items',
|
||||
one: 'item',
|
||||
);
|
||||
return 'Open $title, $count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenAlbumByArtistTrackCount(
|
||||
String albumName,
|
||||
String artistName,
|
||||
int trackCount,
|
||||
) {
|
||||
return 'Open album $albumName by $artistName, $trackCount tracks';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yTrackByArtist(String trackName, String artistName) {
|
||||
return '$trackName by $artistName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11ySelectAlbum(String albumName) {
|
||||
return 'Select album $albumName';
|
||||
}
|
||||
|
||||
@override
|
||||
String a11yOpenAlbum(String albumName) {
|
||||
return 'Open album $albumName';
|
||||
}
|
||||
}
|
||||
|
||||
/// The translations for Chinese, as used in China (`zh_CN`).
|
||||
|
||||
@@ -29,6 +29,14 @@
|
||||
"@homeSubtitle": {
|
||||
"description": "Subtitle shown below search box"
|
||||
},
|
||||
"homeEmptyTitle": "Home is empty",
|
||||
"@homeEmptyTitle": {
|
||||
"description": "Title shown on home when no providers are available yet"
|
||||
},
|
||||
"homeEmptySubtitle": "Install your first extension to unlock search and browsing.",
|
||||
"@homeEmptySubtitle": {
|
||||
"description": "Subtitle shown on home when no providers are available yet"
|
||||
},
|
||||
"homeSupports": "Supports: Track, Album, Playlist, Artist URLs",
|
||||
"@homeSupports": {
|
||||
"description": "Info text about supported URL types"
|
||||
@@ -4548,5 +4556,309 @@
|
||||
"notifUpdateFailedBody": "Could not download update. Try again later.",
|
||||
"@notifUpdateFailedBody": {
|
||||
"description": "Notification body when app update download fails"
|
||||
},
|
||||
"searchTracks": "Tracks",
|
||||
"@searchTracks": {
|
||||
"description": "Search filter label - tracks"
|
||||
},
|
||||
"homeSearchHintDefault": "Paste supported URL or search...",
|
||||
"@homeSearchHintDefault": {
|
||||
"description": "Default placeholder for the main search field on Home"
|
||||
},
|
||||
"homeSearchHintProvider": "Search with {providerName}...",
|
||||
"@homeSearchHintProvider": {
|
||||
"description": "Placeholder for the main search field when a provider is selected",
|
||||
"placeholders": {
|
||||
"providerName": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"homeImportCsvTooltip": "Import CSV",
|
||||
"@homeImportCsvTooltip": {
|
||||
"description": "Tooltip for importing a CSV file into Home search"
|
||||
},
|
||||
"homeChangeSearchProviderTooltip": "Change search provider",
|
||||
"@homeChangeSearchProviderTooltip": {
|
||||
"description": "Tooltip for the Home search provider picker"
|
||||
},
|
||||
"actionPaste": "Paste",
|
||||
"@actionPaste": {
|
||||
"description": "Generic action - paste from clipboard"
|
||||
},
|
||||
"searchTracksHint": "Search tracks...",
|
||||
"@searchTracksHint": {
|
||||
"description": "Placeholder for the search screen input"
|
||||
},
|
||||
"searchTracksEmptyPrompt": "Search for tracks",
|
||||
"@searchTracksEmptyPrompt": {
|
||||
"description": "Empty-state prompt on the search screen"
|
||||
},
|
||||
"tutorialSearchHint": "Paste or search...",
|
||||
"@tutorialSearchHint": {
|
||||
"description": "Placeholder shown in the tutorial search demo"
|
||||
},
|
||||
"tutorialDownloadCompletedSemantics": "Download completed",
|
||||
"@tutorialDownloadCompletedSemantics": {
|
||||
"description": "Accessibility label for completed download state in tutorial demo"
|
||||
},
|
||||
"tutorialDownloadInProgressSemantics": "Download in progress",
|
||||
"@tutorialDownloadInProgressSemantics": {
|
||||
"description": "Accessibility label for active download state in tutorial demo"
|
||||
},
|
||||
"tutorialStartDownloadSemantics": "Start download",
|
||||
"@tutorialStartDownloadSemantics": {
|
||||
"description": "Accessibility label for idle download button in tutorial demo"
|
||||
},
|
||||
"optionsEmbedMetadata": "Embed Metadata",
|
||||
"@optionsEmbedMetadata": {
|
||||
"description": "Settings toggle title for writing metadata into downloaded files"
|
||||
},
|
||||
"optionsEmbedMetadataSubtitleOn": "Write metadata, cover art, and embedded lyrics to files",
|
||||
"@optionsEmbedMetadataSubtitleOn": {
|
||||
"description": "Subtitle when metadata embedding is enabled"
|
||||
},
|
||||
"optionsEmbedMetadataSubtitleOff": "Disabled (advanced): skip all metadata embedding",
|
||||
"@optionsEmbedMetadataSubtitleOff": {
|
||||
"description": "Subtitle when metadata embedding is disabled"
|
||||
},
|
||||
"optionsMaxQualityCoverSubtitleDisabled": "Disabled when metadata embedding is off",
|
||||
"@optionsMaxQualityCoverSubtitleDisabled": {
|
||||
"description": "Subtitle for max quality cover when metadata embedding is disabled"
|
||||
},
|
||||
"downloadFilenameHintExample": "{artist} - {title}",
|
||||
"@downloadFilenameHintExample": {
|
||||
"description": "Example placeholder for the download filename format input"
|
||||
},
|
||||
"trackCoverNoEmbeddedArt": "No embedded album art found",
|
||||
"@trackCoverNoEmbeddedArt": {
|
||||
"description": "Message shown when a track file has no embedded cover art"
|
||||
},
|
||||
"trackCoverReplace": "Replace Cover",
|
||||
"@trackCoverReplace": {
|
||||
"description": "Button label for replacing selected cover art"
|
||||
},
|
||||
"trackCoverPick": "Pick Cover",
|
||||
"@trackCoverPick": {
|
||||
"description": "Button label for selecting cover art"
|
||||
},
|
||||
"trackCoverClearSelected": "Clear selected cover",
|
||||
"@trackCoverClearSelected": {
|
||||
"description": "Tooltip for clearing the newly selected cover art"
|
||||
},
|
||||
"trackCoverCurrent": "Current cover",
|
||||
"@trackCoverCurrent": {
|
||||
"description": "Label for the currently embedded cover preview"
|
||||
},
|
||||
"trackCoverSelected": "Selected cover",
|
||||
"@trackCoverSelected": {
|
||||
"description": "Label for the newly selected cover preview"
|
||||
},
|
||||
"trackCoverReplaceNotice": "The selected cover will replace the current embedded cover when you tap Save.",
|
||||
"@trackCoverReplaceNotice": {
|
||||
"description": "Notice shown when a new cover has been selected but not saved yet"
|
||||
},
|
||||
"actionStop": "Stop",
|
||||
"@actionStop": {
|
||||
"description": "Generic action - stop"
|
||||
},
|
||||
"queueFinalizingDownload": "Finalizing download",
|
||||
"@queueFinalizingDownload": {
|
||||
"description": "Accessibility label for a queue item that is finalizing"
|
||||
},
|
||||
"queueDownloadedFileMissing": "Downloaded file missing",
|
||||
"@queueDownloadedFileMissing": {
|
||||
"description": "Accessibility label when a downloaded file is missing from disk"
|
||||
},
|
||||
"queueDownloadCompleted": "Download completed",
|
||||
"@queueDownloadCompleted": {
|
||||
"description": "Accessibility label for completed download state in queue"
|
||||
},
|
||||
"appearanceSelectAccentColor": "Select accent color {hex}",
|
||||
"@appearanceSelectAccentColor": {
|
||||
"description": "Accessibility label for picking an accent color",
|
||||
"placeholders": {
|
||||
"hex": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"logAutoScrollOn": "Auto-scroll ON",
|
||||
"@logAutoScrollOn": {
|
||||
"description": "Tooltip when auto-scroll is enabled on the log screen"
|
||||
},
|
||||
"logAutoScrollOff": "Auto-scroll OFF",
|
||||
"@logAutoScrollOff": {
|
||||
"description": "Tooltip when auto-scroll is disabled on the log screen"
|
||||
},
|
||||
"logCopyLogs": "Copy logs",
|
||||
"@logCopyLogs": {
|
||||
"description": "Tooltip for copying logs"
|
||||
},
|
||||
"logClearSearch": "Clear search",
|
||||
"@logClearSearch": {
|
||||
"description": "Tooltip for clearing the log search field"
|
||||
},
|
||||
"logIssueIspBlockingLabel": "ISP BLOCKING DETECTED",
|
||||
"@logIssueIspBlockingLabel": {
|
||||
"description": "Diagnostic badge label when ISP blocking is detected"
|
||||
},
|
||||
"logIssueIspBlockingDescription": "Your ISP may be blocking access to download services",
|
||||
"@logIssueIspBlockingDescription": {
|
||||
"description": "Diagnostic badge description for ISP blocking"
|
||||
},
|
||||
"logIssueIspBlockingSuggestion": "Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8",
|
||||
"@logIssueIspBlockingSuggestion": {
|
||||
"description": "Diagnostic badge suggestion for ISP blocking"
|
||||
},
|
||||
"logIssueRateLimitedLabel": "RATE LIMITED",
|
||||
"@logIssueRateLimitedLabel": {
|
||||
"description": "Diagnostic badge label when the service rate limits requests"
|
||||
},
|
||||
"logIssueRateLimitedDescription": "Too many requests to the service",
|
||||
"@logIssueRateLimitedDescription": {
|
||||
"description": "Diagnostic badge description for rate limiting"
|
||||
},
|
||||
"logIssueRateLimitedSuggestion": "Wait a few minutes before trying again",
|
||||
"@logIssueRateLimitedSuggestion": {
|
||||
"description": "Diagnostic badge suggestion for rate limiting"
|
||||
},
|
||||
"logIssueNetworkErrorLabel": "NETWORK ERROR",
|
||||
"@logIssueNetworkErrorLabel": {
|
||||
"description": "Diagnostic badge label for generic network errors"
|
||||
},
|
||||
"logIssueNetworkErrorDescription": "Connection issues detected",
|
||||
"@logIssueNetworkErrorDescription": {
|
||||
"description": "Diagnostic badge description for generic network errors"
|
||||
},
|
||||
"logIssueNetworkErrorSuggestion": "Check your internet connection",
|
||||
"@logIssueNetworkErrorSuggestion": {
|
||||
"description": "Diagnostic badge suggestion for generic network errors"
|
||||
},
|
||||
"logIssueTrackNotFoundLabel": "TRACK NOT FOUND",
|
||||
"@logIssueTrackNotFoundLabel": {
|
||||
"description": "Diagnostic badge label when a track is unavailable"
|
||||
},
|
||||
"logIssueTrackNotFoundDescription": "Some tracks could not be found on download services",
|
||||
"@logIssueTrackNotFoundDescription": {
|
||||
"description": "Diagnostic badge description when a track is unavailable"
|
||||
},
|
||||
"logIssueTrackNotFoundSuggestion": "The track may not be available in lossless quality",
|
||||
"@logIssueTrackNotFoundSuggestion": {
|
||||
"description": "Diagnostic badge suggestion when a track is unavailable"
|
||||
},
|
||||
"clickableLookingUpArtist": "Looking up artist...",
|
||||
"@clickableLookingUpArtist": {
|
||||
"description": "Snackbar shown while clickable artist metadata is being resolved"
|
||||
},
|
||||
"clickableInformationUnavailable": "{type} information not available",
|
||||
"@clickableInformationUnavailable": {
|
||||
"description": "Snackbar shown when clickable metadata cannot open a destination",
|
||||
"placeholders": {
|
||||
"type": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extensionDetailsTags": "Tags",
|
||||
"@extensionDetailsTags": {
|
||||
"description": "Section title for extension tags"
|
||||
},
|
||||
"extensionDetailsInformation": "Information",
|
||||
"@extensionDetailsInformation": {
|
||||
"description": "Section title for extension metadata information"
|
||||
},
|
||||
"extensionUtilityFunctions": "Utility Functions",
|
||||
"@extensionUtilityFunctions": {
|
||||
"description": "Capability label for utility-only extensions"
|
||||
},
|
||||
"actionDismiss": "Dismiss",
|
||||
"@actionDismiss": {
|
||||
"description": "Generic action - dismiss"
|
||||
},
|
||||
"setupChangeFolderTooltip": "Change folder",
|
||||
"@setupChangeFolderTooltip": {
|
||||
"description": "Tooltip for editing the selected download folder"
|
||||
},
|
||||
"a11yOpenTrackByArtist": "Open track {trackName} by {artistName}",
|
||||
"@a11yOpenTrackByArtist": {
|
||||
"description": "Accessibility label for opening a track item",
|
||||
"placeholders": {
|
||||
"trackName": {
|
||||
"type": "String"
|
||||
},
|
||||
"artistName": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"a11yOpenItem": "Open {itemType} {name}",
|
||||
"@a11yOpenItem": {
|
||||
"description": "Accessibility label for opening a generic item",
|
||||
"placeholders": {
|
||||
"itemType": {
|
||||
"type": "String"
|
||||
},
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"a11yOpenItemCount": "Open {title}, {count} {count, plural, =1{item} other{items}}",
|
||||
"@a11yOpenItemCount": {
|
||||
"description": "Accessibility label for opening a grouped item with count",
|
||||
"placeholders": {
|
||||
"title": {
|
||||
"type": "String"
|
||||
},
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"a11yOpenAlbumByArtistTrackCount": "Open album {albumName} by {artistName}, {trackCount} tracks",
|
||||
"@a11yOpenAlbumByArtistTrackCount": {
|
||||
"description": "Accessibility label for opening an album item with track count",
|
||||
"placeholders": {
|
||||
"albumName": {
|
||||
"type": "String"
|
||||
},
|
||||
"artistName": {
|
||||
"type": "String"
|
||||
},
|
||||
"trackCount": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"a11yTrackByArtist": "{trackName} by {artistName}",
|
||||
"@a11yTrackByArtist": {
|
||||
"description": "Accessibility label for a queue or list track item",
|
||||
"placeholders": {
|
||||
"trackName": {
|
||||
"type": "String"
|
||||
},
|
||||
"artistName": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"a11ySelectAlbum": "Select album {albumName}",
|
||||
"@a11ySelectAlbum": {
|
||||
"description": "Accessibility label for selecting an album",
|
||||
"placeholders": {
|
||||
"albumName": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"a11yOpenAlbum": "Open album {albumName}",
|
||||
"@a11yOpenAlbum": {
|
||||
"description": "Accessibility label for opening an album",
|
||||
"placeholders": {
|
||||
"albumName": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ class AppSettings {
|
||||
lastSeenVersion; // Last app version the user has acknowledged (e.g. '3.7.0')
|
||||
|
||||
const AppSettings({
|
||||
this.defaultService = 'tidal',
|
||||
this.defaultService = '',
|
||||
this.audioQuality = 'LOSSLESS',
|
||||
this.filenameFormat = '{title} - {artist}',
|
||||
this.downloadDirectory = '',
|
||||
|
||||
@@ -7,7 +7,7 @@ part of 'settings.dart';
|
||||
// **************************************************************************
|
||||
|
||||
AppSettings _$AppSettingsFromJson(Map<String, dynamic> json) => AppSettings(
|
||||
defaultService: json['defaultService'] as String? ?? 'tidal',
|
||||
defaultService: json['defaultService'] as String? ?? '',
|
||||
audioQuality: json['audioQuality'] as String? ?? 'LOSSLESS',
|
||||
filenameFormat: json['filenameFormat'] as String? ?? '{title} - {artist}',
|
||||
downloadDirectory: json['downloadDirectory'] as String? ?? '',
|
||||
|
||||
@@ -1455,6 +1455,10 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
final decoded = jsonDecode(itemJson);
|
||||
if (decoded is! Map) continue;
|
||||
var item = DownloadItem.fromJson(Map<String, dynamic>.from(decoded));
|
||||
final normalizedService = _normalizeQueuedService(item.service);
|
||||
if (normalizedService != item.service) {
|
||||
item = item.copyWith(service: normalizedService);
|
||||
}
|
||||
if (item.status == DownloadStatus.downloading) {
|
||||
item = item.copyWith(status: DownloadStatus.queued, progress: 0);
|
||||
}
|
||||
@@ -2395,7 +2399,8 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
if (extensionPreferred != null) {
|
||||
return extensionPreferred;
|
||||
}
|
||||
if (service.toLowerCase() == 'tidal' && quality == 'HIGH') {
|
||||
if (_usesBuiltInCompatibleDownloadProvider(service, 'tidal') &&
|
||||
quality == 'HIGH') {
|
||||
return '.m4a';
|
||||
}
|
||||
final q = quality.toLowerCase();
|
||||
@@ -2405,6 +2410,49 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
return '.flac';
|
||||
}
|
||||
|
||||
bool _usesBuiltInCompatibleDownloadProvider(
|
||||
String service,
|
||||
String builtInProviderId,
|
||||
) {
|
||||
return ref
|
||||
.read(extensionProvider.notifier)
|
||||
.downloadProviderMatchesBuiltIn(service, builtInProviderId);
|
||||
}
|
||||
|
||||
String _normalizeQueuedService(String service) {
|
||||
final normalized = service.trim();
|
||||
if (normalized.isEmpty) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
final replacement = ref
|
||||
.read(extensionProvider.notifier)
|
||||
.replacedBuiltInDownloadProviderFor(normalized);
|
||||
if (replacement != null && replacement.isNotEmpty) {
|
||||
return replacement;
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
bool _hasActiveDownloadProvider(String service) {
|
||||
final normalized = service.trim();
|
||||
if (normalized.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
if (isBuiltInDownloadProvider(normalized)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final extensionState = ref.read(extensionProvider);
|
||||
return extensionState.extensions.any(
|
||||
(ext) =>
|
||||
ext.enabled &&
|
||||
ext.hasDownloadProvider &&
|
||||
ext.id.toLowerCase() == normalized.toLowerCase(),
|
||||
);
|
||||
}
|
||||
|
||||
String _mimeTypeForExt(String ext) {
|
||||
switch (ext.toLowerCase()) {
|
||||
case '.m4a':
|
||||
@@ -2837,7 +2885,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
final item = DownloadItem(
|
||||
id: id,
|
||||
track: track,
|
||||
service: service,
|
||||
service: _normalizeQueuedService(service),
|
||||
createdAt: DateTime.now(),
|
||||
qualityOverride: qualityOverride,
|
||||
playlistName: playlistName,
|
||||
@@ -2869,7 +2917,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
return DownloadItem(
|
||||
id: id,
|
||||
track: track,
|
||||
service: service,
|
||||
service: _normalizeQueuedService(service),
|
||||
createdAt: DateTime.now(),
|
||||
qualityOverride: qualityOverride,
|
||||
playlistName: playlistName,
|
||||
@@ -4388,6 +4436,31 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
}
|
||||
|
||||
Future<void> _downloadSingleItem(DownloadItem item) async {
|
||||
final normalizedService = _normalizeQueuedService(item.service);
|
||||
if (normalizedService != item.service) {
|
||||
item = item.copyWith(service: normalizedService);
|
||||
state = state.copyWith(
|
||||
items: [
|
||||
for (final existing in state.items)
|
||||
if (existing.id == item.id) item else existing,
|
||||
],
|
||||
currentDownload: state.currentDownload?.id == item.id
|
||||
? item
|
||||
: state.currentDownload,
|
||||
);
|
||||
_saveQueueToStorage();
|
||||
}
|
||||
|
||||
if (!_hasActiveDownloadProvider(item.service)) {
|
||||
updateItemStatus(
|
||||
item.id,
|
||||
DownloadStatus.failed,
|
||||
error: 'Download provider is no longer available',
|
||||
errorType: DownloadErrorType.notFound,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_log.d('Processing: ${item.track.name} by ${item.track.artistName}');
|
||||
_log.d('Cover URL: ${item.track.coverUrl}');
|
||||
var pausedDuringThisRun = false;
|
||||
@@ -4748,7 +4821,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
}
|
||||
if (trackToDownload.id.startsWith('tidal:')) {
|
||||
payloadTidalId = trackToDownload.id.substring(6);
|
||||
if (item.service == 'tidal') {
|
||||
if (_usesBuiltInCompatibleDownloadProvider(item.service, 'tidal')) {
|
||||
payloadSpotifyId = '';
|
||||
}
|
||||
}
|
||||
@@ -5051,7 +5124,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
!wasExisting &&
|
||||
isContentUriPath &&
|
||||
effectiveSafMode &&
|
||||
actualService == 'tidal' &&
|
||||
_usesBuiltInCompatibleDownloadProvider(actualService, 'tidal') &&
|
||||
filePath.endsWith('.flac') &&
|
||||
(mimeType == null || mimeType.contains('flac'));
|
||||
|
||||
|
||||
@@ -211,6 +211,20 @@ class Extension {
|
||||
bool get hasPostProcessing => postProcessing?.enabled ?? false;
|
||||
bool get hasHomeFeed => capabilities['homeFeed'] == true;
|
||||
bool get hasBrowseCategories => capabilities['browseCategories'] == true;
|
||||
List<String> get replacesBuiltInProviders {
|
||||
final value = capabilities['replacesBuiltInProviders'];
|
||||
if (value is! List) return const [];
|
||||
|
||||
final normalized = <String>[];
|
||||
for (final item in value) {
|
||||
if (item is! String) continue;
|
||||
final trimmed = item.trim().toLowerCase();
|
||||
if (trimmed.isEmpty || normalized.contains(trimmed)) continue;
|
||||
normalized.add(trimmed);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
String? get preferredDownloadOutputExtension {
|
||||
final value = capabilities['downloadOutputExtension'];
|
||||
if (value is! String) return null;
|
||||
@@ -743,6 +757,8 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
final extensions = list.map((e) => Extension.fromJson(e)).toList();
|
||||
state = state.copyWith(extensions: extensions);
|
||||
await _reconcileDownloadProviderPriority();
|
||||
await _reconcileDefaultDownloadService();
|
||||
_reconcileSearchProvider();
|
||||
_log.d('Loaded ${extensions.length} extensions');
|
||||
|
||||
for (final ext in extensions) {
|
||||
@@ -849,6 +865,8 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
|
||||
state = state.copyWith(extensions: extensions);
|
||||
await _reconcileDownloadProviderPriority();
|
||||
await _reconcileDefaultDownloadService();
|
||||
_reconcileSearchProvider();
|
||||
|
||||
if (!enabled && ext != null) {
|
||||
final settings = ref.read(settingsProvider);
|
||||
@@ -861,16 +879,16 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
}
|
||||
|
||||
if (ext.hasDownloadProvider && settings.defaultService == extensionId) {
|
||||
final availableProviders = getAllDownloadProviders();
|
||||
if (availableProviders.isNotEmpty) {
|
||||
final fallbackService = availableProviders.first;
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setDefaultService(fallbackService);
|
||||
_log.d(
|
||||
'Reset default service to $fallbackService because extension $extensionId was disabled',
|
||||
);
|
||||
}
|
||||
final fallbackService =
|
||||
_firstEnabledExtensionDownloadProviderId() ?? '';
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setDefaultService(fallbackService);
|
||||
_log.d(
|
||||
fallbackService.isEmpty
|
||||
? 'Cleared default service because extension $extensionId was disabled'
|
||||
: 'Reset default service to $fallbackService because extension $extensionId was disabled',
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -896,6 +914,142 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
_log.d('Reconciled provider priority after extension update: $sanitized');
|
||||
}
|
||||
|
||||
String? _firstEnabledExtensionDownloadProviderId() {
|
||||
return state.extensions
|
||||
.where((ext) => ext.enabled && ext.hasDownloadProvider)
|
||||
.map((ext) => ext.id)
|
||||
.firstOrNull;
|
||||
}
|
||||
|
||||
String? replacedBuiltInDownloadProviderFor(String providerId) {
|
||||
final normalized = providerId.trim().toLowerCase();
|
||||
if (normalized.isEmpty) return null;
|
||||
|
||||
return state.extensions
|
||||
.where(
|
||||
(ext) =>
|
||||
ext.enabled &&
|
||||
ext.hasDownloadProvider &&
|
||||
ext.replacesBuiltInProviders.contains(normalized),
|
||||
)
|
||||
.map((ext) => ext.id)
|
||||
.firstOrNull;
|
||||
}
|
||||
|
||||
String? replacedBuiltInSearchProviderFor(String providerId) {
|
||||
final normalized = providerId.trim().toLowerCase();
|
||||
if (normalized.isEmpty) return null;
|
||||
|
||||
return state.extensions
|
||||
.where(
|
||||
(ext) =>
|
||||
ext.enabled &&
|
||||
ext.hasCustomSearch &&
|
||||
ext.replacesBuiltInProviders.contains(normalized),
|
||||
)
|
||||
.map((ext) => ext.id)
|
||||
.firstOrNull;
|
||||
}
|
||||
|
||||
bool downloadProviderMatchesBuiltIn(
|
||||
String providerId,
|
||||
String builtInProviderId,
|
||||
) {
|
||||
final normalizedProvider = providerId.trim().toLowerCase();
|
||||
final normalizedBuiltIn = builtInProviderId.trim().toLowerCase();
|
||||
if (normalizedProvider.isEmpty || normalizedBuiltIn.isEmpty) return false;
|
||||
if (normalizedProvider == normalizedBuiltIn) return true;
|
||||
|
||||
final extension = state.extensions
|
||||
.where((ext) => ext.enabled && ext.hasDownloadProvider)
|
||||
.where((ext) => ext.id.toLowerCase() == normalizedProvider)
|
||||
.firstOrNull;
|
||||
return extension?.replacesBuiltInProviders.contains(normalizedBuiltIn) ??
|
||||
false;
|
||||
}
|
||||
|
||||
Future<void> _reconcileDefaultDownloadService() async {
|
||||
final settings = ref.read(settingsProvider);
|
||||
final preferredExtensionId = _firstEnabledExtensionDownloadProviderId();
|
||||
final currentService = settings.defaultService.trim();
|
||||
|
||||
if (currentService.isEmpty) {
|
||||
if (preferredExtensionId != null) {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setDefaultService(preferredExtensionId);
|
||||
_log.d(
|
||||
'Adopted first enabled download extension as default service: $preferredExtensionId',
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final replacementExtensionId = replacedBuiltInDownloadProviderFor(
|
||||
currentService,
|
||||
);
|
||||
if (replacementExtensionId != null) {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setDefaultService(replacementExtensionId);
|
||||
_log.d(
|
||||
'Migrated retired built-in service $currentService to $replacementExtensionId',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final currentExtension = state.extensions
|
||||
.where((ext) => ext.id == currentService)
|
||||
.firstOrNull;
|
||||
final isMissingOrInvalidExtension =
|
||||
currentExtension == null ||
|
||||
!currentExtension.enabled ||
|
||||
!currentExtension.hasDownloadProvider;
|
||||
if (!isBuiltInDownloadProvider(currentService) &&
|
||||
isMissingOrInvalidExtension) {
|
||||
final fallbackService = preferredExtensionId ?? '';
|
||||
ref.read(settingsProvider.notifier).setDefaultService(fallbackService);
|
||||
_log.d(
|
||||
fallbackService.isEmpty
|
||||
? 'Cleared default service because $currentService is no longer available'
|
||||
: 'Reset default service to $fallbackService because $currentService is no longer available',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _reconcileSearchProvider() {
|
||||
final settings = ref.read(settingsProvider);
|
||||
final currentSearchProvider = settings.searchProvider?.trim();
|
||||
if (currentSearchProvider == null || currentSearchProvider.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final replacementExtensionId = replacedBuiltInSearchProviderFor(
|
||||
currentSearchProvider,
|
||||
);
|
||||
if (replacementExtensionId != null) {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setSearchProvider(replacementExtensionId);
|
||||
_log.d(
|
||||
'Migrated retired built-in search provider $currentSearchProvider to $replacementExtensionId',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final hasMatchingExtension = state.extensions.any(
|
||||
(ext) =>
|
||||
ext.enabled && ext.hasCustomSearch && ext.id == currentSearchProvider,
|
||||
);
|
||||
if (!isBuiltInSearchProvider(currentSearchProvider) &&
|
||||
!hasMatchingExtension) {
|
||||
ref.read(settingsProvider.notifier).setSearchProvider(null);
|
||||
_log.d(
|
||||
'Cleared stale search provider because $currentSearchProvider is no longer available',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> ensureSpotifyWebExtensionReady({
|
||||
bool setAsSearchProvider = true,
|
||||
}) async {
|
||||
|
||||
@@ -12,13 +12,18 @@ import 'package:spotiflac_android/utils/logger.dart';
|
||||
|
||||
const _settingsKey = 'app_settings';
|
||||
const _migrationVersionKey = 'settings_migration_version';
|
||||
const _currentMigrationVersion = 10;
|
||||
const _currentMigrationVersion = 11;
|
||||
const _spotifyClientSecretKey = 'spotify_client_secret';
|
||||
final _log = AppLogger('SettingsProvider');
|
||||
|
||||
class SettingsNotifier extends Notifier<AppSettings> {
|
||||
static final RegExp _isoRegionPattern = RegExp(r'^[A-Z]{2}$');
|
||||
static const Set<String> _searchTabValues = {'all', 'track', 'artist', 'album'};
|
||||
static const Set<String> _searchTabValues = {
|
||||
'all',
|
||||
'track',
|
||||
'artist',
|
||||
'album',
|
||||
};
|
||||
|
||||
final Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
|
||||
final FlutterSecureStorage _secureStorage = const FlutterSecureStorage();
|
||||
@@ -137,10 +142,11 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
);
|
||||
}
|
||||
state = state.copyWith(lastSeenVersion: AppInfo.version);
|
||||
// Migration 7/10: retired built-in services reset back to Tidal
|
||||
// Migration 7/11: retired built-in services no longer fall back to a
|
||||
// preinstalled provider.
|
||||
if (state.defaultService == 'youtube' ||
|
||||
state.defaultService == 'deezer') {
|
||||
state = state.copyWith(defaultService: 'tidal');
|
||||
state = state.copyWith(defaultService: '');
|
||||
}
|
||||
await prefs.setInt(_migrationVersionKey, _currentMigrationVersion);
|
||||
await _saveSettings();
|
||||
|
||||
@@ -1689,8 +1689,8 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
button: true,
|
||||
selected: _isSelectionMode && isSelected,
|
||||
label: _isSelectionMode
|
||||
? 'Select album ${album.name}'
|
||||
: 'Open album ${album.name}',
|
||||
? context.l10n.a11ySelectAlbum(album.name)
|
||||
: context.l10n.a11yOpenAlbum(album.name),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (_isSelectionMode) {
|
||||
|
||||
@@ -866,7 +866,7 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
|
||||
trailing: _isSelectionMode
|
||||
? null
|
||||
: IconButton(
|
||||
tooltip: 'Play track',
|
||||
tooltip: context.l10n.tooltipPlay,
|
||||
onPressed: () => _openFile(track),
|
||||
icon: Icon(Icons.play_arrow, color: colorScheme.primary),
|
||||
style: IconButton.styleFrom(
|
||||
|
||||
+71
-28
@@ -431,6 +431,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
}
|
||||
|
||||
List<SearchFilter> _resolveSearchFilters(
|
||||
BuildContext context,
|
||||
String? currentSearchProvider,
|
||||
List<Extension> extensions,
|
||||
) {
|
||||
@@ -453,11 +454,27 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
}
|
||||
}
|
||||
|
||||
return const [
|
||||
SearchFilter(id: 'track', label: 'Tracks', icon: 'music'),
|
||||
SearchFilter(id: 'artist', label: 'Artists', icon: 'artist'),
|
||||
SearchFilter(id: 'album', label: 'Albums', icon: 'album'),
|
||||
SearchFilter(id: 'playlist', label: 'Playlists', icon: 'playlist'),
|
||||
return [
|
||||
SearchFilter(
|
||||
id: 'track',
|
||||
label: context.l10n.searchTracks,
|
||||
icon: 'music',
|
||||
),
|
||||
SearchFilter(
|
||||
id: 'artist',
|
||||
label: context.l10n.searchArtists,
|
||||
icon: 'artist',
|
||||
),
|
||||
SearchFilter(
|
||||
id: 'album',
|
||||
label: context.l10n.searchAlbums,
|
||||
icon: 'album',
|
||||
),
|
||||
SearchFilter(
|
||||
id: 'playlist',
|
||||
label: context.l10n.searchPlaylists,
|
||||
icon: 'playlist',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1249,9 +1266,13 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
final hasSearchedBefore = ref.watch(
|
||||
settingsProvider.select((s) => s.hasSearchedBefore),
|
||||
);
|
||||
final explicitSearchProvider = ref.watch(
|
||||
settingsProvider.select((s) => s.searchProvider),
|
||||
);
|
||||
final defaultSearchTab = ref.watch(
|
||||
settingsProvider.select((s) => s.defaultSearchTab),
|
||||
);
|
||||
final extensions = ref.watch(extensionProvider.select((s) => s.extensions));
|
||||
|
||||
final hasExploreContent = ref.watch(
|
||||
exploreProvider.select((s) => s.sections.isNotEmpty),
|
||||
@@ -1289,6 +1310,9 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
recentModeRequested &&
|
||||
(!hasSearchInput || hasShortSearchInput || !hasActualResults) &&
|
||||
!isLoading;
|
||||
final hasSearchProvider =
|
||||
(_resolveSearchProvider(explicitSearchProvider, extensions) ?? '')
|
||||
.isNotEmpty;
|
||||
final hasResults =
|
||||
hasSearchInput || hasActualResults || isLoading || showRecentAccess;
|
||||
final showExplore =
|
||||
@@ -1298,6 +1322,12 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
!homeFeedDisabled &&
|
||||
(hasHomeFeedExtension || hasExploreContent) &&
|
||||
hasExploreContent;
|
||||
final showEmptyHomeState =
|
||||
!hasSearchProvider &&
|
||||
!hasHomeFeedExtension &&
|
||||
!hasExploreContent &&
|
||||
!hasResults &&
|
||||
historyItems.isEmpty;
|
||||
|
||||
ref.listen<String>(settingsProvider.select((s) => s.defaultSearchTab), (
|
||||
previous,
|
||||
@@ -1413,13 +1443,17 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'SpotiFLAC',
|
||||
showEmptyHomeState
|
||||
? context.l10n.homeEmptyTitle
|
||||
: 'SpotiFLAC Mobile',
|
||||
style: Theme.of(context).textTheme.headlineSmall
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
context.l10n.homeSubtitle,
|
||||
showEmptyHomeState
|
||||
? context.l10n.homeEmptySubtitle
|
||||
: context.l10n.homeSubtitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium
|
||||
?.copyWith(
|
||||
@@ -1431,17 +1465,18 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
),
|
||||
),
|
||||
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
16,
|
||||
(hasResults || showExplore) ? 8 : 32,
|
||||
16,
|
||||
(hasResults || showExplore) ? 8 : 16,
|
||||
if (hasSearchProvider)
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
16,
|
||||
(hasResults || showExplore) ? 8 : 32,
|
||||
16,
|
||||
(hasResults || showExplore) ? 8 : 16,
|
||||
),
|
||||
child: _buildSearchBar(colorScheme),
|
||||
),
|
||||
child: _buildSearchBar(colorScheme),
|
||||
),
|
||||
),
|
||||
|
||||
if (hasActualResults && !showRecentAccess)
|
||||
Consumer(
|
||||
@@ -1456,6 +1491,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
trackProvider.select((s) => s.selectedSearchFilter),
|
||||
);
|
||||
final searchFilters = _resolveSearchFilters(
|
||||
context,
|
||||
currentSearchProvider,
|
||||
extensions,
|
||||
);
|
||||
@@ -1493,7 +1529,11 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeOut,
|
||||
child: (hasResults || showRecentAccess || showExplore)
|
||||
child:
|
||||
(hasResults ||
|
||||
showRecentAccess ||
|
||||
showExplore ||
|
||||
showEmptyHomeState)
|
||||
? const SizedBox.shrink()
|
||||
: Column(
|
||||
children: [
|
||||
@@ -1666,7 +1706,10 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
key: ValueKey(item.id),
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: 'Open track ${item.trackName} by ${item.artistName}',
|
||||
label: context.l10n.a11yOpenTrackByArtist(
|
||||
item.trackName,
|
||||
item.artistName,
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () => _navigateToMetadataScreen(
|
||||
item,
|
||||
@@ -1810,7 +1853,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: 'Open ${item.type} ${item.name}',
|
||||
label: context.l10n.a11yOpenItem(item.type, item.name),
|
||||
child: GestureDetector(
|
||||
onTap: () => _navigateToExploreItem(item),
|
||||
child: SizedBox(
|
||||
@@ -2294,7 +2337,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Dismiss',
|
||||
tooltip: context.l10n.actionDismiss,
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
size: 20,
|
||||
@@ -3341,13 +3384,13 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
);
|
||||
|
||||
if (!extState.isInitialized) {
|
||||
return 'Paste supported URL or search...';
|
||||
return context.l10n.homeSearchHintDefault;
|
||||
}
|
||||
|
||||
if (searchProvider != null && searchProvider.isNotEmpty) {
|
||||
final builtIn = builtInProviderSpecForId(searchProvider);
|
||||
if (builtIn != null && builtIn.supportsSearch) {
|
||||
return 'Search with ${builtIn.displayName}...';
|
||||
return context.l10n.homeSearchHintProvider(builtIn.displayName);
|
||||
}
|
||||
|
||||
final ext = extState.extensions
|
||||
@@ -3357,10 +3400,10 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
if (ext.searchBehavior?.placeholder != null) {
|
||||
return ext.searchBehavior!.placeholder!;
|
||||
}
|
||||
return 'Search with ${ext.displayName}...';
|
||||
return context.l10n.homeSearchHintProvider(ext.displayName);
|
||||
}
|
||||
}
|
||||
return 'Paste supported URL or search...';
|
||||
return context.l10n.homeSearchHintDefault;
|
||||
}
|
||||
|
||||
Widget _buildSearchFilterBar(
|
||||
@@ -3481,7 +3524,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: _clearAndRefresh,
|
||||
tooltip: 'Clear',
|
||||
tooltip: context.l10n.dialogClear,
|
||||
)
|
||||
else ...[
|
||||
IconButton(
|
||||
@@ -3489,12 +3532,12 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
onPressed: _isCsvImporting
|
||||
? null
|
||||
: () => _importCsv(context, ref),
|
||||
tooltip: 'Import CSV',
|
||||
tooltip: context.l10n.homeImportCsvTooltip,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.paste),
|
||||
onPressed: _pasteFromClipboard,
|
||||
tooltip: 'Paste',
|
||||
tooltip: context.l10n.actionPaste,
|
||||
),
|
||||
],
|
||||
],
|
||||
@@ -3640,7 +3683,7 @@ class _SearchProviderDropdown extends ConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
tooltip: 'Change search provider',
|
||||
tooltip: context.l10n.homeChangeSearchProviderTooltip,
|
||||
offset: const Offset(0, 40),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
onSelected: (String providerId) {
|
||||
|
||||
@@ -737,7 +737,7 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
|
||||
trailing: _isSelectionMode
|
||||
? null
|
||||
: IconButton(
|
||||
tooltip: 'Play track',
|
||||
tooltip: context.l10n.tooltipPlay,
|
||||
onPressed: () => _openFile(track),
|
||||
icon: Icon(Icons.play_arrow, color: colorScheme.primary),
|
||||
style: IconButton.styleFrom(
|
||||
|
||||
+19
-12
@@ -3537,7 +3537,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: _searchQuery.isNotEmpty
|
||||
? IconButton(
|
||||
tooltip: 'Clear',
|
||||
tooltip: context.l10n.dialogClear,
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
@@ -3954,7 +3954,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: 'Open $title, $count ${count == 1 ? 'item' : 'items'}',
|
||||
label: context.l10n.a11yOpenItemCount(title, count),
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
@@ -4909,7 +4909,11 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
}) {
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: 'Open album $albumName by $artistName, $trackCount tracks',
|
||||
label: context.l10n.a11yOpenAlbumByArtistTrackCount(
|
||||
albumName,
|
||||
artistName,
|
||||
trackCount,
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
@@ -6444,7 +6448,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
onPressed: () =>
|
||||
ref.read(downloadQueueProvider.notifier).cancelItem(item.id),
|
||||
icon: Icon(Icons.close, color: colorScheme.error),
|
||||
tooltip: 'Cancel',
|
||||
tooltip: context.l10n.dialogCancel,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: colorScheme.errorContainer.withValues(alpha: 0.3),
|
||||
),
|
||||
@@ -6454,14 +6458,14 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
onPressed: () =>
|
||||
ref.read(downloadQueueProvider.notifier).cancelItem(item.id),
|
||||
icon: Icon(Icons.stop, color: colorScheme.error),
|
||||
tooltip: 'Stop',
|
||||
tooltip: context.l10n.actionStop,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: colorScheme.errorContainer.withValues(alpha: 0.3),
|
||||
),
|
||||
);
|
||||
case DownloadStatus.finalizing:
|
||||
return Semantics(
|
||||
label: 'Finalizing download',
|
||||
label: context.l10n.queueFinalizingDownload,
|
||||
child: SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
@@ -6500,7 +6504,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
coverUrl: item.track.coverUrl ?? '',
|
||||
),
|
||||
icon: Icon(Icons.play_arrow, color: colorScheme.primary),
|
||||
tooltip: 'Play',
|
||||
tooltip: context.l10n.tooltipPlay,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: colorScheme.primaryContainer.withValues(
|
||||
alpha: 0.3,
|
||||
@@ -6509,7 +6513,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
)
|
||||
else
|
||||
Semantics(
|
||||
label: 'Downloaded file missing',
|
||||
label: context.l10n.queueDownloadedFileMissing,
|
||||
child: ExcludeSemantics(
|
||||
child: Icon(
|
||||
Icons.error_outline,
|
||||
@@ -6520,7 +6524,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Semantics(
|
||||
label: 'Download completed',
|
||||
label: context.l10n.queueDownloadCompleted,
|
||||
child: ExcludeSemantics(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
@@ -6549,7 +6553,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
onPressed: () =>
|
||||
ref.read(downloadQueueProvider.notifier).retryItem(item.id),
|
||||
icon: Icon(Icons.refresh, color: colorScheme.primary),
|
||||
tooltip: 'Retry',
|
||||
tooltip: context.l10n.dialogRetry,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: colorScheme.primaryContainer.withValues(
|
||||
alpha: 0.3,
|
||||
@@ -6566,7 +6570,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
? colorScheme.error
|
||||
: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
tooltip: 'Remove',
|
||||
tooltip: context.l10n.dialogRemove,
|
||||
style: item.status == DownloadStatus.failed
|
||||
? IconButton.styleFrom(
|
||||
backgroundColor: colorScheme.errorContainer.withValues(
|
||||
@@ -6743,7 +6747,10 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
: colorScheme.onSecondaryContainer;
|
||||
|
||||
return Semantics(
|
||||
label: '${item.trackName} by ${item.artistName}',
|
||||
label: context.l10n.a11yTrackByArtist(
|
||||
item.trackName,
|
||||
item.artistName,
|
||||
),
|
||||
selected: isSelected,
|
||||
child: Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
|
||||
@@ -142,7 +142,7 @@ class _RepoTabState extends ConsumerState<RepoTab> {
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: value.text.isNotEmpty
|
||||
? IconButton(
|
||||
tooltip: 'Clear',
|
||||
tooltip: context.l10n.dialogClear,
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
|
||||
@@ -70,7 +70,7 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
|
||||
controller: _searchController,
|
||||
style: TextStyle(color: colorScheme.onSurface),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search tracks...',
|
||||
hintText: context.l10n.searchTracksHint,
|
||||
hintStyle: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
@@ -124,7 +124,7 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
|
||||
Icon(Icons.search, size: 64, color: colorScheme.onSurfaceVariant),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Search for tracks',
|
||||
context.l10n.searchTracksEmptyPrompt,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
@@ -194,7 +194,7 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.download_rounded),
|
||||
tooltip: 'Download',
|
||||
tooltip: context.l10n.dialogDownload,
|
||||
onPressed: () => _downloadTrack(track),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -358,7 +358,7 @@ class _ColorPalettePicker extends StatelessWidget {
|
||||
child: Semantics(
|
||||
button: true,
|
||||
selected: isSelected,
|
||||
label: 'Select accent color $colorHex',
|
||||
label: context.l10n.appearanceSelectAccentColor(colorHex),
|
||||
child: GestureDetector(
|
||||
onTap: () => onColorSelected(color),
|
||||
child: _ColorPaletteItem(color: color, isSelected: isSelected),
|
||||
|
||||
@@ -297,11 +297,34 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final settings = ref.watch(settingsProvider);
|
||||
final extensionState = ref.watch(extensionProvider);
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final topPadding = normalizedHeaderTopPadding(context);
|
||||
final extensionDownloadProviders = extensionState.extensions
|
||||
.where(
|
||||
(extension) => extension.enabled && extension.hasDownloadProvider,
|
||||
)
|
||||
.toList(growable: false);
|
||||
final extensionNotifier = ref.read(extensionProvider.notifier);
|
||||
final hasDownloadProviders =
|
||||
builtInDownloadProviderSpecs.isNotEmpty ||
|
||||
extensionDownloadProviders.isNotEmpty;
|
||||
|
||||
final isBuiltInService = isBuiltInDownloadProvider(settings.defaultService);
|
||||
final isTidalService = settings.defaultService == 'tidal';
|
||||
final replacedBuiltInServiceId = builtInDownloadProviderSpecs
|
||||
.map((provider) => provider.id)
|
||||
.where(
|
||||
(providerId) => extensionNotifier.downloadProviderMatchesBuiltIn(
|
||||
settings.defaultService,
|
||||
providerId,
|
||||
),
|
||||
)
|
||||
.firstOrNull;
|
||||
final effectiveBuiltInServiceId = isBuiltInService
|
||||
? settings.defaultService
|
||||
: replacedBuiltInServiceId;
|
||||
final isBuiltInCompatibleService = effectiveBuiltInServiceId != null;
|
||||
final isTidalService = effectiveBuiltInServiceId == 'tidal';
|
||||
|
||||
return PopScope(
|
||||
canPop: true,
|
||||
@@ -375,17 +398,19 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
SettingsSwitchItem(
|
||||
icon: Icons.tune,
|
||||
title: context.l10n.downloadAskBeforeDownload,
|
||||
subtitle: isBuiltInService
|
||||
subtitle: !hasDownloadProviders
|
||||
? context.l10n.extensionsNoDownloadProvider
|
||||
: isBuiltInCompatibleService
|
||||
? context.l10n.downloadAskQualitySubtitle
|
||||
: context.l10n.downloadSelectServiceToEnable,
|
||||
value: settings.askQualityBeforeDownload,
|
||||
enabled: isBuiltInService,
|
||||
enabled: hasDownloadProviders && isBuiltInCompatibleService,
|
||||
onChanged: (value) => ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setAskQualityBeforeDownload(value),
|
||||
),
|
||||
if (!settings.askQualityBeforeDownload &&
|
||||
isBuiltInService) ...[
|
||||
isBuiltInCompatibleService) ...[
|
||||
_QualityOption(
|
||||
title: context.l10n.qualityFlacLossless,
|
||||
subtitle: context.l10n.qualityFlacLosslessSubtitle,
|
||||
@@ -441,7 +466,13 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
showDivider: false,
|
||||
),
|
||||
],
|
||||
if (!isBuiltInService) ...[
|
||||
if (!hasDownloadProviders) ...[
|
||||
_InlineInfoMessage(
|
||||
icon: Icons.extension_outlined,
|
||||
text: context.l10n.extensionsNoDownloadProvider,
|
||||
secondaryText: context.l10n.storeAddRepoDescription,
|
||||
),
|
||||
] else if (!isBuiltInCompatibleService) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: Row(
|
||||
@@ -1076,7 +1107,10 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
TextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
hintText: '{artist} - {title}',
|
||||
hintText: context.l10n.downloadFilenameHintExample(
|
||||
'{artist}',
|
||||
'{title}',
|
||||
),
|
||||
filled: true,
|
||||
fillColor: colorScheme.surfaceContainerHighest
|
||||
.withValues(alpha: 0.3),
|
||||
@@ -2070,34 +2104,42 @@ class _ServiceSelector extends ConsumerWidget {
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
children: [
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
for (final provider in builtInProviders)
|
||||
_ServiceChip(
|
||||
icon: resolveProviderIcon(provider.id),
|
||||
label: provider.displayName,
|
||||
isSelected: effectiveService == provider.id,
|
||||
onTap: () => onChanged(provider.id),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (extensionProviders.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
if (builtInProviders.isEmpty && extensionProviders.isEmpty)
|
||||
_InlineInfoMessage(
|
||||
icon: Icons.extension_outlined,
|
||||
text: context.l10n.extensionsNoDownloadProvider,
|
||||
secondaryText: context.l10n.storeAddRepoDescription,
|
||||
)
|
||||
else ...[
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
for (final extension in extensionProviders)
|
||||
for (final provider in builtInProviders)
|
||||
_ServiceChip(
|
||||
icon: Icons.extension,
|
||||
label: extension.displayName,
|
||||
isSelected: effectiveService == extension.id,
|
||||
onTap: () => onChanged(extension.id),
|
||||
icon: resolveProviderIcon(provider.id),
|
||||
label: provider.displayName,
|
||||
isSelected: effectiveService == provider.id,
|
||||
onTap: () => onChanged(provider.id),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (extensionProviders.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
for (final extension in extensionProviders)
|
||||
_ServiceChip(
|
||||
icon: Icons.extension,
|
||||
label: extension.displayName,
|
||||
isSelected: effectiveService == extension.id,
|
||||
onTap: () => onChanged(extension.id),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
@@ -2105,6 +2147,63 @@ class _ServiceSelector extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _InlineInfoMessage extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String text;
|
||||
final String? secondaryText;
|
||||
|
||||
const _InlineInfoMessage({
|
||||
required this.icon,
|
||||
required this.text,
|
||||
this.secondaryText,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.45),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: colorScheme.outlineVariant.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(icon, size: 18, color: colorScheme.onSurfaceVariant),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
text,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
if (secondaryText != null && secondaryText!.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
secondaryText!,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ServiceChip extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
|
||||
@@ -160,12 +160,14 @@ class _LogScreenState extends State<LogScreen> {
|
||||
? Icons.vertical_align_bottom
|
||||
: Icons.vertical_align_center,
|
||||
),
|
||||
tooltip: _autoScroll ? 'Auto-scroll ON' : 'Auto-scroll OFF',
|
||||
tooltip: _autoScroll
|
||||
? context.l10n.logAutoScrollOn
|
||||
: context.l10n.logAutoScrollOff,
|
||||
onPressed: () => setState(() => _autoScroll = !_autoScroll),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.copy),
|
||||
tooltip: 'Copy logs',
|
||||
tooltip: context.l10n.logCopyLogs,
|
||||
onPressed: _copyLogs,
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
@@ -327,7 +329,7 @@ class _LogScreenState extends State<LogScreen> {
|
||||
fillColor: colorScheme.surfaceContainerHighest,
|
||||
suffixIcon: _searchQuery.isNotEmpty
|
||||
? IconButton(
|
||||
tooltip: 'Clear search',
|
||||
tooltip: context.l10n.logClearSearch,
|
||||
icon: const Icon(Icons.clear, size: 20),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
@@ -609,11 +611,9 @@ class _LogSummaryCard extends StatelessWidget {
|
||||
if (analysis.hasISPBlocking) ...[
|
||||
_IssueBadge(
|
||||
icon: Icons.block,
|
||||
label: 'ISP BLOCKING DETECTED',
|
||||
description:
|
||||
'Your ISP may be blocking access to download services',
|
||||
suggestion:
|
||||
'Try using a VPN or change DNS to 1.1.1.1 or 8.8.8.8',
|
||||
label: context.l10n.logIssueIspBlockingLabel,
|
||||
description: context.l10n.logIssueIspBlockingDescription,
|
||||
suggestion: context.l10n.logIssueIspBlockingSuggestion,
|
||||
color: colorScheme.error,
|
||||
domains: analysis.blockedDomains,
|
||||
),
|
||||
@@ -623,9 +623,9 @@ class _LogSummaryCard extends StatelessWidget {
|
||||
if (analysis.hasRateLimit) ...[
|
||||
_IssueBadge(
|
||||
icon: Icons.speed,
|
||||
label: 'RATE LIMITED',
|
||||
description: 'Too many requests to the service',
|
||||
suggestion: 'Wait a few minutes before trying again',
|
||||
label: context.l10n.logIssueRateLimitedLabel,
|
||||
description: context.l10n.logIssueRateLimitedDescription,
|
||||
suggestion: context.l10n.logIssueRateLimitedSuggestion,
|
||||
color: Colors.orange,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -634,9 +634,9 @@ class _LogSummaryCard extends StatelessWidget {
|
||||
if (analysis.hasNetworkError && !analysis.hasISPBlocking) ...[
|
||||
_IssueBadge(
|
||||
icon: Icons.wifi_off,
|
||||
label: 'NETWORK ERROR',
|
||||
description: 'Connection issues detected',
|
||||
suggestion: 'Check your internet connection',
|
||||
label: context.l10n.logIssueNetworkErrorLabel,
|
||||
description: context.l10n.logIssueNetworkErrorDescription,
|
||||
suggestion: context.l10n.logIssueNetworkErrorSuggestion,
|
||||
color: colorScheme.tertiary,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -645,11 +645,9 @@ class _LogSummaryCard extends StatelessWidget {
|
||||
if (analysis.hasNotFound) ...[
|
||||
_IssueBadge(
|
||||
icon: Icons.search_off,
|
||||
label: 'TRACK NOT FOUND',
|
||||
description:
|
||||
'Some tracks could not be found on download services',
|
||||
suggestion:
|
||||
'The track may not be available in lossless quality',
|
||||
label: context.l10n.logIssueTrackNotFoundLabel,
|
||||
description: context.l10n.logIssueTrackNotFoundDescription,
|
||||
suggestion: context.l10n.logIssueTrackNotFoundSuggestion,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -107,10 +107,10 @@ class OptionsSettingsPage extends ConsumerWidget {
|
||||
),
|
||||
SettingsSwitchItem(
|
||||
icon: Icons.sell_outlined,
|
||||
title: 'Embed Metadata',
|
||||
title: context.l10n.optionsEmbedMetadata,
|
||||
subtitle: settings.embedMetadata
|
||||
? 'Write metadata, cover art, and embedded lyrics to files'
|
||||
: 'Disabled (advanced): skip all metadata embedding',
|
||||
? context.l10n.optionsEmbedMetadataSubtitleOn
|
||||
: context.l10n.optionsEmbedMetadataSubtitleOff,
|
||||
value: settings.embedMetadata,
|
||||
onChanged: (v) =>
|
||||
ref.read(settingsProvider.notifier).setEmbedMetadata(v),
|
||||
@@ -135,7 +135,7 @@ class OptionsSettingsPage extends ConsumerWidget {
|
||||
title: context.l10n.optionsMaxQualityCover,
|
||||
subtitle: settings.embedMetadata
|
||||
? context.l10n.optionsMaxQualityCoverSubtitle
|
||||
: 'Disabled when metadata embedding is off',
|
||||
: context.l10n.optionsMaxQualityCoverSubtitleDisabled,
|
||||
value: settings.maxQualityCover,
|
||||
enabled: settings.embedMetadata,
|
||||
onChanged: (v) => ref
|
||||
|
||||
@@ -749,7 +749,7 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: IconButton(
|
||||
tooltip: 'Change folder',
|
||||
tooltip: context.l10n.setupChangeFolderTooltip,
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: _selectDirectory,
|
||||
),
|
||||
|
||||
@@ -44,13 +44,18 @@ class _ExtensionDetailsScreenState
|
||||
_buildDescription(context, liveExtension, colorScheme),
|
||||
|
||||
if (liveExtension.tags.isNotEmpty) ...[
|
||||
_buildSectionHeader(context, 'Tags', Icons.tag, colorScheme),
|
||||
_buildSectionHeader(
|
||||
context,
|
||||
context.l10n.extensionDetailsTags,
|
||||
Icons.tag,
|
||||
colorScheme,
|
||||
),
|
||||
_buildTags(context, liveExtension, colorScheme),
|
||||
],
|
||||
|
||||
_buildSectionHeader(
|
||||
context,
|
||||
'Information',
|
||||
context.l10n.extensionDetailsInformation,
|
||||
Icons.table_chart_outlined,
|
||||
colorScheme,
|
||||
),
|
||||
@@ -438,7 +443,7 @@ class _ExtensionDetailsScreenState
|
||||
),
|
||||
_CapabilityRow(
|
||||
icon: Icons.build,
|
||||
label: 'Utility Functions',
|
||||
label: context.l10n.extensionUtilityFunctions,
|
||||
enabled: isUtility,
|
||||
colorScheme: colorScheme,
|
||||
isLast: true,
|
||||
|
||||
@@ -6037,7 +6037,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
|
||||
const LinearProgressIndicator(minHeight: 2)
|
||||
else if (!hasCurrentCover)
|
||||
Text(
|
||||
'No embedded album art found',
|
||||
context.l10n.trackCoverNoEmbeddedArt,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(color: cs.onSurfaceVariant),
|
||||
@@ -6050,14 +6050,16 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
|
||||
onPressed: _saving ? null : _pickCoverImage,
|
||||
icon: const Icon(Icons.image_outlined),
|
||||
label: Text(
|
||||
hasSelectedCover ? 'Replace Cover' : 'Pick Cover',
|
||||
hasSelectedCover
|
||||
? context.l10n.trackCoverReplace
|
||||
: context.l10n.trackCoverPick,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (hasSelectedCover) ...[
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
tooltip: 'Clear selected cover',
|
||||
tooltip: context.l10n.trackCoverClearSelected,
|
||||
onPressed: _saving
|
||||
? null
|
||||
: () async {
|
||||
@@ -6079,7 +6081,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
|
||||
child: _buildCoverPreviewTile(
|
||||
cs: cs,
|
||||
path: _currentCoverPath!,
|
||||
label: 'Current cover',
|
||||
label: context.l10n.trackCoverCurrent,
|
||||
),
|
||||
),
|
||||
if (hasCurrentCover && hasSelectedCover)
|
||||
@@ -6089,7 +6091,9 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
|
||||
child: _buildCoverPreviewTile(
|
||||
cs: cs,
|
||||
path: _selectedCoverPath!,
|
||||
label: _selectedCoverName ?? 'Selected cover',
|
||||
label:
|
||||
_selectedCoverName ??
|
||||
context.l10n.trackCoverSelected,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -6097,7 +6101,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
|
||||
if (hasSelectedCover) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'The selected cover will replace the current embedded cover when you tap Save.',
|
||||
context.l10n.trackCoverReplaceNotice,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(color: cs.onSurfaceVariant),
|
||||
|
||||
@@ -408,7 +408,7 @@ class _InteractiveSearchExampleState extends State<_InteractiveSearchExample> {
|
||||
},
|
||||
style: TextStyle(color: colorScheme.onSurface, fontSize: 16),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Paste or search...',
|
||||
hintText: context.l10n.tutorialSearchHint,
|
||||
hintStyle: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||
prefixIcon: Icon(Icons.search, color: colorScheme.primary),
|
||||
filled: true,
|
||||
@@ -619,10 +619,10 @@ class _InteractiveDownloadExampleState
|
||||
Semantics(
|
||||
button: true,
|
||||
label: _isCompleted
|
||||
? 'Download completed'
|
||||
? context.l10n.tutorialDownloadCompletedSemantics
|
||||
: _isDownloading
|
||||
? 'Download in progress'
|
||||
: 'Start download',
|
||||
? context.l10n.tutorialDownloadInProgressSemantics
|
||||
: context.l10n.tutorialStartDownloadSemantics,
|
||||
child: GestureDetector(
|
||||
onTap: _startDownload,
|
||||
child: AnimatedContainer(
|
||||
|
||||
@@ -108,7 +108,7 @@ class LocalTrackRedownloadService {
|
||||
case 'deezer':
|
||||
return settings.defaultService.toLowerCase();
|
||||
default:
|
||||
return 'tidal';
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -547,22 +547,6 @@ class PlatformBridge {
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> getTidalMetadata(
|
||||
String resourceType,
|
||||
String resourceId,
|
||||
) async {
|
||||
final result = await _channel.invokeMethod('getTidalMetadata', {
|
||||
'resource_type': resourceType,
|
||||
'resource_id': resourceId,
|
||||
});
|
||||
if (result == null) {
|
||||
throw Exception(
|
||||
'getTidalMetadata returned null for $resourceType:$resourceId',
|
||||
);
|
||||
}
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> getProviderMetadata(
|
||||
String providerId,
|
||||
String resourceType,
|
||||
@@ -581,15 +565,6 @@ class PlatformBridge {
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> convertTidalToSpotifyDeezer(
|
||||
String tidalUrl,
|
||||
) async {
|
||||
final result = await _channel.invokeMethod('convertTidalToSpotifyDeezer', {
|
||||
'url': tidalUrl,
|
||||
});
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> searchDeezerByISRC(
|
||||
String isrc, {
|
||||
String? itemId,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||
import 'package:spotiflac_android/providers/extension_provider.dart';
|
||||
import 'package:spotiflac_android/screens/artist_screen.dart';
|
||||
@@ -51,7 +52,7 @@ Future<void> navigateToArtist(
|
||||
return;
|
||||
}
|
||||
|
||||
_showLoadingSnackBar(context, 'Looking up artist...');
|
||||
_showLoadingSnackBar(context, context.l10n.clickableLookingUpArtist);
|
||||
try {
|
||||
final artistList = await _searchDeezerExtension(
|
||||
artistName,
|
||||
@@ -62,7 +63,7 @@ Future<void> navigateToArtist(
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
|
||||
if (artistList.isEmpty) {
|
||||
_showUnavailable(context, 'Artist');
|
||||
_showUnavailable(context, context.l10n.trackArtist);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -82,7 +83,7 @@ Future<void> navigateToArtist(
|
||||
final resolvedImage = bestMatch['images'] as String?;
|
||||
|
||||
if (resolvedId.isEmpty) {
|
||||
_showUnavailable(context, 'Artist');
|
||||
_showUnavailable(context, context.l10n.trackArtist);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -98,7 +99,7 @@ Future<void> navigateToArtist(
|
||||
_log.e('Failed to look up artist "$artistName": $e', e);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
_showUnavailable(context, 'Artist');
|
||||
_showUnavailable(context, context.l10n.trackArtist);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,7 +291,9 @@ void _showLoadingSnackBar(BuildContext context, String message) {
|
||||
void _showUnavailable(BuildContext context, String type) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('$type information not available')));
|
||||
).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.clickableInformationUnavailable(type))),
|
||||
);
|
||||
}
|
||||
|
||||
class ClickableArtistName extends StatefulWidget {
|
||||
|
||||
@@ -21,8 +21,8 @@ class BuiltInService {
|
||||
});
|
||||
}
|
||||
|
||||
const _builtInServices = [
|
||||
BuiltInService(
|
||||
const _builtInServiceCatalog = {
|
||||
'tidal': BuiltInService(
|
||||
id: 'tidal',
|
||||
label: 'Tidal',
|
||||
qualityOptions: [
|
||||
@@ -43,7 +43,7 @@ const _builtInServices = [
|
||||
),
|
||||
],
|
||||
),
|
||||
BuiltInService(
|
||||
'qobuz': BuiltInService(
|
||||
id: 'qobuz',
|
||||
label: 'Qobuz',
|
||||
qualityOptions: [
|
||||
@@ -64,7 +64,7 @@ const _builtInServices = [
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
};
|
||||
|
||||
class DownloadServicePicker extends ConsumerStatefulWidget {
|
||||
final String? trackName;
|
||||
@@ -118,58 +118,93 @@ class DownloadServicePicker extends ConsumerStatefulWidget {
|
||||
class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
|
||||
late String _selectedService;
|
||||
|
||||
List<BuiltInService> _availableBuiltInServices() {
|
||||
final availableIds = builtInDownloadProviderIds.toSet();
|
||||
final services = <BuiltInService>[];
|
||||
for (final id in availableIds) {
|
||||
final service = _builtInServiceCatalog[id];
|
||||
if (service != null) {
|
||||
services.add(service);
|
||||
}
|
||||
}
|
||||
return services;
|
||||
}
|
||||
|
||||
List<Extension> _downloadExtensions() {
|
||||
final extensionState = ref.read(extensionProvider);
|
||||
return extensionState.extensions
|
||||
.where((ext) => ext.enabled && ext.hasDownloadProvider)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
bool _serviceExists(
|
||||
String serviceId,
|
||||
List<BuiltInService> builtInServices,
|
||||
List<Extension> downloadExtensions,
|
||||
) {
|
||||
if (serviceId.isEmpty) return false;
|
||||
if (builtInServices.any((service) => service.id == serviceId)) return true;
|
||||
return downloadExtensions.any((ext) => ext.id == serviceId);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final builtInServices = _availableBuiltInServices();
|
||||
final downloadExtensions = _downloadExtensions();
|
||||
final recommended = widget.recommendedService;
|
||||
if (recommended != null && recommended.isNotEmpty) {
|
||||
if (recommended != null &&
|
||||
_serviceExists(recommended, builtInServices, downloadExtensions)) {
|
||||
_selectedService = recommended;
|
||||
} else {
|
||||
_selectedService = ref.read(settingsProvider).defaultService;
|
||||
}
|
||||
if (!_builtInServices.any((service) => service.id == _selectedService)) {
|
||||
final extensionState = ref.read(extensionProvider);
|
||||
final hasMatchingExtension = extensionState.extensions.any(
|
||||
(ext) =>
|
||||
ext.enabled &&
|
||||
ext.hasDownloadProvider &&
|
||||
ext.id == _selectedService,
|
||||
);
|
||||
if (!hasMatchingExtension) {
|
||||
_selectedService = 'tidal';
|
||||
}
|
||||
if (!_serviceExists(
|
||||
_selectedService,
|
||||
builtInServices,
|
||||
downloadExtensions,
|
||||
)) {
|
||||
_selectedService = builtInServices.isNotEmpty
|
||||
? builtInServices.first.id
|
||||
: downloadExtensions.isNotEmpty
|
||||
? downloadExtensions.first.id
|
||||
: '';
|
||||
}
|
||||
}
|
||||
|
||||
List<QualityOption> _getQualityOptions() {
|
||||
final builtIn = _builtInServices
|
||||
.where((s) => s.id == _selectedService)
|
||||
List<QualityOption> _getQualityOptions(
|
||||
List<BuiltInService> builtInServices,
|
||||
List<Extension> downloadExtensions,
|
||||
) {
|
||||
final builtIn = builtInServices
|
||||
.where((service) => service.id == _selectedService)
|
||||
.firstOrNull;
|
||||
if (builtIn != null) {
|
||||
return builtIn.qualityOptions;
|
||||
}
|
||||
|
||||
final extensionState = ref.read(extensionProvider);
|
||||
final ext = extensionState.extensions
|
||||
final ext = downloadExtensions
|
||||
.where((e) => e.id == _selectedService)
|
||||
.firstOrNull;
|
||||
if (ext != null && ext.qualityOptions.isNotEmpty) {
|
||||
return ext.qualityOptions;
|
||||
}
|
||||
|
||||
return _builtInServices.firstWhere((s) => s.id == 'tidal').qualityOptions;
|
||||
return const [];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final extensionState = ref.watch(extensionProvider);
|
||||
|
||||
final downloadExtensions = extensionState.extensions
|
||||
.where((ext) => ext.enabled && ext.hasDownloadProvider)
|
||||
.toList();
|
||||
|
||||
final qualityOptions = _getQualityOptions();
|
||||
ref.watch(extensionProvider);
|
||||
final builtInServices = _availableBuiltInServices();
|
||||
final downloadExtensions = _downloadExtensions();
|
||||
final hasProviders =
|
||||
builtInServices.isNotEmpty || downloadExtensions.isNotEmpty;
|
||||
final qualityOptions = _getQualityOptions(
|
||||
builtInServices,
|
||||
downloadExtensions,
|
||||
);
|
||||
|
||||
return SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
@@ -213,68 +248,77 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
for (final service in _builtInServices)
|
||||
_ServiceChip(
|
||||
label: service.isDisabled
|
||||
? '${service.label} (${service.disabledReason})'
|
||||
: widget.recommendedService == service.id
|
||||
? '${service.label} (Recommended)'
|
||||
: service.label,
|
||||
isSelected: _selectedService == service.id,
|
||||
isDisabled: service.isDisabled,
|
||||
onTap: service.isDisabled
|
||||
? null
|
||||
: () => setState(() => _selectedService = service.id),
|
||||
child: hasProviders
|
||||
? Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
for (final service in builtInServices)
|
||||
_ServiceChip(
|
||||
label: service.isDisabled
|
||||
? '${service.label} (${service.disabledReason})'
|
||||
: widget.recommendedService == service.id
|
||||
? '${service.label} (Recommended)'
|
||||
: service.label,
|
||||
isSelected: _selectedService == service.id,
|
||||
isDisabled: service.isDisabled,
|
||||
onTap: service.isDisabled
|
||||
? null
|
||||
: () => setState(
|
||||
() => _selectedService = service.id,
|
||||
),
|
||||
),
|
||||
for (final ext in downloadExtensions)
|
||||
_ServiceChip(
|
||||
label: widget.recommendedService == ext.id
|
||||
? '${ext.displayName} (Recommended)'
|
||||
: ext.displayName,
|
||||
isSelected: _selectedService == ext.id,
|
||||
onTap: () =>
|
||||
setState(() => _selectedService = ext.id),
|
||||
iconPath: ext.iconPath,
|
||||
),
|
||||
],
|
||||
)
|
||||
: _NoDownloadProviderHint(
|
||||
primaryText: context.l10n.extensionsNoDownloadProvider,
|
||||
secondaryText: context.l10n.storeAddRepoDescription,
|
||||
),
|
||||
for (final ext in downloadExtensions)
|
||||
_ServiceChip(
|
||||
label: widget.recommendedService == ext.id
|
||||
? '${ext.displayName} (Recommended)'
|
||||
: ext.displayName,
|
||||
isSelected: _selectedService == ext.id,
|
||||
onTap: () => setState(() => _selectedService = ext.id),
|
||||
iconPath: ext.iconPath,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 16, 24, 8),
|
||||
child: Text(
|
||||
context.l10n.downloadSelectQuality,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
|
||||
if (_builtInServices.any((s) => s.id == _selectedService))
|
||||
if (hasProviders) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 12),
|
||||
padding: const EdgeInsets.fromLTRB(24, 16, 24, 8),
|
||||
child: Text(
|
||||
context.l10n.qualityNote,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
fontStyle: FontStyle.italic,
|
||||
context.l10n.downloadSelectQuality,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
for (final quality in qualityOptions)
|
||||
_QualityOption(
|
||||
title: quality.label,
|
||||
subtitle: quality.description ?? '',
|
||||
icon: _getQualityIcon(quality.id),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
widget.onSelect(quality.id, _selectedService);
|
||||
},
|
||||
),
|
||||
if (builtInServices.any(
|
||||
(service) => service.id == _selectedService,
|
||||
))
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 12),
|
||||
child: Text(
|
||||
context.l10n.qualityNote,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
),
|
||||
for (final quality in qualityOptions)
|
||||
_QualityOption(
|
||||
title: _localizedQualityLabel(context, quality),
|
||||
subtitle: _localizedQualityDescription(context, quality),
|
||||
icon: _getQualityIcon(quality.id),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
widget.onSelect(quality.id, _selectedService);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
@@ -303,6 +347,35 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
|
||||
return Icons.music_note;
|
||||
}
|
||||
}
|
||||
|
||||
String _localizedQualityLabel(BuildContext context, QualityOption quality) {
|
||||
switch (quality.id.toUpperCase()) {
|
||||
case 'LOSSLESS':
|
||||
return context.l10n.qualityFlacLossless;
|
||||
case 'HI_RES':
|
||||
return context.l10n.qualityHiResFlac;
|
||||
case 'HI_RES_LOSSLESS':
|
||||
return context.l10n.qualityHiResFlacMax;
|
||||
default:
|
||||
return quality.label;
|
||||
}
|
||||
}
|
||||
|
||||
String _localizedQualityDescription(
|
||||
BuildContext context,
|
||||
QualityOption quality,
|
||||
) {
|
||||
switch (quality.id.toUpperCase()) {
|
||||
case 'LOSSLESS':
|
||||
return context.l10n.qualityFlacLosslessSubtitle;
|
||||
case 'HI_RES':
|
||||
return context.l10n.qualityHiResFlacSubtitle;
|
||||
case 'HI_RES_LOSSLESS':
|
||||
return context.l10n.qualityHiResFlacMaxSubtitle;
|
||||
default:
|
||||
return quality.description ?? '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _QualityOption extends StatelessWidget {
|
||||
@@ -421,6 +494,63 @@ class _ServiceChip extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _NoDownloadProviderHint extends StatelessWidget {
|
||||
final String primaryText;
|
||||
final String secondaryText;
|
||||
|
||||
const _NoDownloadProviderHint({
|
||||
required this.primaryText,
|
||||
required this.secondaryText,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.45),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: colorScheme.outlineVariant.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.extension_outlined,
|
||||
size: 18,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
primaryText,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
secondaryText,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TrackInfoHeader extends StatefulWidget {
|
||||
final String trackName;
|
||||
final String? artistName;
|
||||
|
||||
Reference in New Issue
Block a user