mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-05-16 13:39:15 +02:00
feat: add SongLink region setting and fix track metadata lookup with name+artist fallback
- Add configurable SongLink region (userCountry) setting with picker UI - Pass songLinkRegion through download request payload to Go backend - Go backend: thread-safe global SongLink region with per-request override - Fix downloaded track not recognized in collection tap: add findByTrackAndArtist fallback in download history lookup chain (Spotify ID → ISRC → name+artist) - Apply same name+artist fallback to isDownloaded check in track options sheet - Add missing library_database.dart import for LocalLibraryItem
This commit is contained in:
@@ -177,6 +177,7 @@ type DownloadRequest struct {
|
||||
LyricsMode string `json:"lyrics_mode,omitempty"`
|
||||
UseExtensions bool `json:"use_extensions,omitempty"`
|
||||
UseFallback bool `json:"use_fallback,omitempty"`
|
||||
SongLinkRegion string `json:"songlink_region,omitempty"`
|
||||
}
|
||||
|
||||
type DownloadResponse struct {
|
||||
@@ -378,11 +379,19 @@ func enrichRequestExtendedMetadata(req *DownloadRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
func applySongLinkRegionFromRequest(req *DownloadRequest) {
|
||||
if req == nil {
|
||||
return
|
||||
}
|
||||
SetSongLinkRegion(req.SongLinkRegion)
|
||||
}
|
||||
|
||||
func DownloadTrack(requestJSON string) (string, error) {
|
||||
var req DownloadRequest
|
||||
if err := json.Unmarshal([]byte(requestJSON), &req); err != nil {
|
||||
return errorResponse("Invalid request: " + err.Error())
|
||||
}
|
||||
applySongLinkRegionFromRequest(&req)
|
||||
defer closeOwnedOutputFD(req.OutputFD)
|
||||
|
||||
req.TrackName = strings.TrimSpace(req.TrackName)
|
||||
@@ -566,6 +575,7 @@ func DownloadWithFallback(requestJSON string) (string, error) {
|
||||
if err := json.Unmarshal([]byte(requestJSON), &req); err != nil {
|
||||
return errorResponse("Invalid request: " + err.Error())
|
||||
}
|
||||
applySongLinkRegionFromRequest(&req)
|
||||
defer closeOwnedOutputFD(req.OutputFD)
|
||||
|
||||
req.TrackName = strings.TrimSpace(req.TrackName)
|
||||
@@ -1533,6 +1543,7 @@ func DownloadFromYouTube(requestJSON string) (string, error) {
|
||||
if err := json.Unmarshal([]byte(requestJSON), &req); err != nil {
|
||||
return errorResponse("Invalid request: " + err.Error())
|
||||
}
|
||||
applySongLinkRegionFromRequest(&req)
|
||||
defer closeOwnedOutputFD(req.OutputFD)
|
||||
|
||||
req.TrackName = strings.TrimSpace(req.TrackName)
|
||||
@@ -2251,6 +2262,7 @@ func DownloadWithExtensionsJSON(requestJSON string) (string, error) {
|
||||
if err := json.Unmarshal([]byte(requestJSON), &req); err != nil {
|
||||
return "", fmt.Errorf("invalid request: %w", err)
|
||||
}
|
||||
applySongLinkRegionFromRequest(&req)
|
||||
defer closeOwnedOutputFD(req.OutputFD)
|
||||
|
||||
req.TrackName = strings.TrimSpace(req.TrackName)
|
||||
|
||||
+37
-2
@@ -34,6 +34,8 @@ type TrackAvailability struct {
|
||||
var (
|
||||
globalSongLinkClient *SongLinkClient
|
||||
songLinkClientOnce sync.Once
|
||||
songLinkRegion = "US"
|
||||
songLinkRegionMu sync.RWMutex
|
||||
)
|
||||
|
||||
func NewSongLinkClient() *SongLinkClient {
|
||||
@@ -45,6 +47,33 @@ func NewSongLinkClient() *SongLinkClient {
|
||||
return globalSongLinkClient
|
||||
}
|
||||
|
||||
func normalizeSongLinkRegion(region string) string {
|
||||
normalized := strings.ToUpper(strings.TrimSpace(region))
|
||||
if len(normalized) != 2 {
|
||||
return "US"
|
||||
}
|
||||
for _, ch := range normalized {
|
||||
if ch < 'A' || ch > 'Z' {
|
||||
return "US"
|
||||
}
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
func SetSongLinkRegion(region string) {
|
||||
normalized := normalizeSongLinkRegion(region)
|
||||
songLinkRegionMu.Lock()
|
||||
songLinkRegion = normalized
|
||||
songLinkRegionMu.Unlock()
|
||||
}
|
||||
|
||||
func GetSongLinkRegion() string {
|
||||
songLinkRegionMu.RLock()
|
||||
region := songLinkRegion
|
||||
songLinkRegionMu.RUnlock()
|
||||
return region
|
||||
}
|
||||
|
||||
func songLinkBaseURL() string {
|
||||
opts := GetNetworkCompatibilityOptions()
|
||||
if opts.AllowHTTP {
|
||||
@@ -54,6 +83,9 @@ func songLinkBaseURL() string {
|
||||
}
|
||||
|
||||
func buildSongLinkURLFromTarget(targetURL string, userCountry string) string {
|
||||
if userCountry == "" {
|
||||
userCountry = GetSongLinkRegion()
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s?url=%s", songLinkBaseURL(), url.QueryEscape(targetURL))
|
||||
if userCountry != "" {
|
||||
apiURL = fmt.Sprintf("%s&userCountry=%s", apiURL, url.QueryEscape(userCountry))
|
||||
@@ -62,6 +94,9 @@ func buildSongLinkURLFromTarget(targetURL string, userCountry string) string {
|
||||
}
|
||||
|
||||
func buildSongLinkURLByPlatform(platform, entityType, entityID, userCountry string) string {
|
||||
if userCountry == "" {
|
||||
userCountry = GetSongLinkRegion()
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s?platform=%s&type=%s&id=%s",
|
||||
songLinkBaseURL(),
|
||||
url.QueryEscape(platform),
|
||||
@@ -448,7 +483,7 @@ func (s *SongLinkClient) checkAvailabilityFromDeezerSongLink(deezerTrackID strin
|
||||
songLinkRateLimiter.WaitForSlot()
|
||||
|
||||
deezerURL := fmt.Sprintf("https://www.deezer.com/track/%s", deezerTrackID)
|
||||
apiURL := buildSongLinkURLFromTarget(deezerURL, "US")
|
||||
apiURL := buildSongLinkURLFromTarget(deezerURL, "")
|
||||
|
||||
req, err := http.NewRequest("GET", apiURL, nil)
|
||||
if err != nil {
|
||||
@@ -552,7 +587,7 @@ func (s *SongLinkClient) CheckAvailabilityByPlatform(platform, entityType, entit
|
||||
|
||||
songLinkRateLimiter.WaitForSlot()
|
||||
|
||||
apiURL := buildSongLinkURLByPlatform(platform, entityType, entityID, "US")
|
||||
apiURL := buildSongLinkURLByPlatform(platform, entityType, entityID, "")
|
||||
|
||||
req, err := http.NewRequest("GET", apiURL, nil)
|
||||
if err != nil {
|
||||
|
||||
@@ -51,6 +51,8 @@ class AppSettings {
|
||||
downloadNetworkMode; // 'any' = WiFi + Mobile, 'wifi_only' = WiFi only
|
||||
final bool
|
||||
networkCompatibilityMode; // Try HTTP + allow invalid TLS cert for API requests
|
||||
final String
|
||||
songLinkRegion; // SongLink userCountry region code used for platform lookup
|
||||
|
||||
// Local Library Settings
|
||||
final bool localLibraryEnabled; // Enable local library scanning
|
||||
@@ -115,6 +117,7 @@ class AppSettings {
|
||||
this.autoExportFailedDownloads = false,
|
||||
this.downloadNetworkMode = 'any',
|
||||
this.networkCompatibilityMode = false,
|
||||
this.songLinkRegion = 'US',
|
||||
// Local Library defaults
|
||||
this.localLibraryEnabled = false,
|
||||
this.localLibraryPath = '',
|
||||
@@ -177,6 +180,7 @@ class AppSettings {
|
||||
bool? autoExportFailedDownloads,
|
||||
String? downloadNetworkMode,
|
||||
bool? networkCompatibilityMode,
|
||||
String? songLinkRegion,
|
||||
// Local Library
|
||||
bool? localLibraryEnabled,
|
||||
String? localLibraryPath,
|
||||
@@ -241,6 +245,7 @@ class AppSettings {
|
||||
downloadNetworkMode: downloadNetworkMode ?? this.downloadNetworkMode,
|
||||
networkCompatibilityMode:
|
||||
networkCompatibilityMode ?? this.networkCompatibilityMode,
|
||||
songLinkRegion: songLinkRegion ?? this.songLinkRegion,
|
||||
// Local Library
|
||||
localLibraryEnabled: localLibraryEnabled ?? this.localLibraryEnabled,
|
||||
localLibraryPath: localLibraryPath ?? this.localLibraryPath,
|
||||
|
||||
@@ -54,6 +54,7 @@ AppSettings _$AppSettingsFromJson(Map<String, dynamic> json) => AppSettings(
|
||||
json['networkCompatibilityMode'] as bool? ??
|
||||
json['songLinkCompatibilityMode'] as bool? ??
|
||||
false,
|
||||
songLinkRegion: json['songLinkRegion'] as String? ?? 'US',
|
||||
localLibraryEnabled: json['localLibraryEnabled'] as bool? ?? false,
|
||||
localLibraryPath: json['localLibraryPath'] as String? ?? '',
|
||||
localLibraryShowDuplicates:
|
||||
@@ -117,6 +118,7 @@ Map<String, dynamic> _$AppSettingsToJson(
|
||||
'autoExportFailedDownloads': instance.autoExportFailedDownloads,
|
||||
'downloadNetworkMode': instance.downloadNetworkMode,
|
||||
'networkCompatibilityMode': instance.networkCompatibilityMode,
|
||||
'songLinkRegion': instance.songLinkRegion,
|
||||
'localLibraryEnabled': instance.localLibraryEnabled,
|
||||
'localLibraryPath': instance.localLibraryPath,
|
||||
'localLibraryShowDuplicates': instance.localLibraryShowDuplicates,
|
||||
|
||||
@@ -232,6 +232,22 @@ class DownloadHistoryState {
|
||||
|
||||
DownloadHistoryItem? getByIsrc(String isrc) => _byIsrc[isrc];
|
||||
|
||||
DownloadHistoryItem? findByTrackAndArtist(
|
||||
String trackName,
|
||||
String artistName,
|
||||
) {
|
||||
final normalizedTrack = trackName.trim().toLowerCase();
|
||||
final normalizedArtist = artistName.trim().toLowerCase();
|
||||
if (normalizedTrack.isEmpty) return null;
|
||||
for (final item in items) {
|
||||
if (item.trackName.trim().toLowerCase() == normalizedTrack &&
|
||||
item.artistName.trim().toLowerCase() == normalizedArtist) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
DownloadHistoryState copyWith({List<DownloadHistoryItem>? items}) {
|
||||
return DownloadHistoryState(items: items ?? this.items);
|
||||
}
|
||||
@@ -3111,6 +3127,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
safRelativeDir: relativeDir,
|
||||
safFileName: fileName,
|
||||
safOutputExt: outputExt,
|
||||
songLinkRegion: settings.songLinkRegion,
|
||||
);
|
||||
|
||||
return PlatformBridge.downloadByStrategy(
|
||||
|
||||
@@ -15,6 +15,7 @@ final _log = AppLogger('SettingsProvider');
|
||||
class SettingsNotifier extends Notifier<AppSettings> {
|
||||
static const List<int> _youtubeOpusSupportedBitrates = [128, 256];
|
||||
static const List<int> _youtubeMp3SupportedBitrates = [128, 256, 320];
|
||||
static final RegExp _isoRegionPattern = RegExp(r'^[A-Z]{2}$');
|
||||
|
||||
final Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
|
||||
final FlutterSecureStorage _secureStorage = const FlutterSecureStorage();
|
||||
@@ -36,6 +37,7 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
|
||||
await _runMigrations(prefs);
|
||||
await _normalizeYouTubeBitratesIfNeeded();
|
||||
await _normalizeSongLinkRegionIfNeeded();
|
||||
}
|
||||
|
||||
await _loadSpotifyClientSecret(prefs);
|
||||
@@ -165,6 +167,19 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
await _saveSettings();
|
||||
}
|
||||
|
||||
String _normalizeSongLinkRegion(String region) {
|
||||
final normalized = region.trim().toUpperCase();
|
||||
if (_isoRegionPattern.hasMatch(normalized)) return normalized;
|
||||
return 'US';
|
||||
}
|
||||
|
||||
Future<void> _normalizeSongLinkRegionIfNeeded() async {
|
||||
final normalized = _normalizeSongLinkRegion(state.songLinkRegion);
|
||||
if (normalized == state.songLinkRegion) return;
|
||||
state = state.copyWith(songLinkRegion: normalized);
|
||||
await _saveSettings();
|
||||
}
|
||||
|
||||
Future<void> _loadSpotifyClientSecret(SharedPreferences prefs) async {
|
||||
final storedSecret = await _secureStorage.read(
|
||||
key: _spotifyClientSecretKey,
|
||||
@@ -483,6 +498,12 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
_syncNetworkCompatibilitySettingsToBackend();
|
||||
}
|
||||
|
||||
void setSongLinkRegion(String region) {
|
||||
final normalized = _normalizeSongLinkRegion(region);
|
||||
state = state.copyWith(songLinkRegion: normalized);
|
||||
_saveSettings();
|
||||
}
|
||||
|
||||
void setLocalLibraryEnabled(bool enabled) {
|
||||
state = state.copyWith(localLibraryEnabled: enabled);
|
||||
_saveSettings();
|
||||
|
||||
@@ -8,6 +8,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/providers/download_queue_provider.dart';
|
||||
import 'package:spotiflac_android/providers/library_collections_provider.dart';
|
||||
import 'package:spotiflac_android/providers/local_library_provider.dart';
|
||||
import 'package:spotiflac_android/services/library_database.dart';
|
||||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/services/cover_cache_manager.dart';
|
||||
import 'package:spotiflac_android/screens/track_metadata_screen.dart';
|
||||
@@ -992,9 +994,12 @@ class _CollectionTrackTile extends ConsumerWidget {
|
||||
void _showTrackOptionsSheet(BuildContext context, WidgetRef ref) {
|
||||
final track = entry.track;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final isDownloaded = ref.read(
|
||||
downloadHistoryProvider.select((state) => state.isDownloaded(track.id)),
|
||||
);
|
||||
final historyState = ref.read(downloadHistoryProvider);
|
||||
final isDownloaded = historyState.isDownloaded(track.id) ||
|
||||
(track.isrc != null &&
|
||||
track.isrc!.isNotEmpty &&
|
||||
historyState.getByIsrc(track.isrc!) != null) ||
|
||||
historyState.findByTrackAndArtist(track.name, track.artistName) != null;
|
||||
// Wishlist: only show "Add to Playlist" if track is already downloaded
|
||||
final showAddToPlaylist =
|
||||
mode != LibraryTracksFolderMode.wishlist || isDownloaded;
|
||||
@@ -1168,22 +1173,62 @@ class _CollectionTrackTile extends ConsumerWidget {
|
||||
|
||||
Future<void> _navigateToMetadata(BuildContext context, WidgetRef ref) async {
|
||||
final track = entry.track;
|
||||
final historyItem = ref
|
||||
.read(downloadHistoryProvider.notifier)
|
||||
.getBySpotifyId(track.id);
|
||||
final historyState = ref.read(downloadHistoryProvider);
|
||||
|
||||
if (historyItem == null) return;
|
||||
// 1. Download history by Spotify ID
|
||||
var historyItem = historyState.getBySpotifyId(track.id);
|
||||
|
||||
await Navigator.of(context).push(
|
||||
PageRouteBuilder(
|
||||
transitionDuration: const Duration(milliseconds: 300),
|
||||
reverseTransitionDuration: const Duration(milliseconds: 250),
|
||||
pageBuilder: (context, animation, secondaryAnimation) =>
|
||||
TrackMetadataScreen(item: historyItem),
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
|
||||
FadeTransition(opacity: animation, child: child),
|
||||
),
|
||||
);
|
||||
// 2. Download history by ISRC
|
||||
if (historyItem == null &&
|
||||
track.isrc != null &&
|
||||
track.isrc!.isNotEmpty) {
|
||||
historyItem = historyState.getByIsrc(track.isrc!);
|
||||
}
|
||||
|
||||
// 3. Download history by track name + artist (handles ID/ISRC mismatch)
|
||||
historyItem ??=
|
||||
historyState.findByTrackAndArtist(track.name, track.artistName);
|
||||
|
||||
if (historyItem != null) {
|
||||
await Navigator.of(context).push(
|
||||
PageRouteBuilder(
|
||||
transitionDuration: const Duration(milliseconds: 300),
|
||||
reverseTransitionDuration: const Duration(milliseconds: 250),
|
||||
pageBuilder: (context, animation, secondaryAnimation) =>
|
||||
TrackMetadataScreen(item: historyItem),
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
|
||||
FadeTransition(opacity: animation, child: child),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Local library by ISRC
|
||||
final localState = ref.read(localLibraryProvider);
|
||||
LocalLibraryItem? localItem;
|
||||
if (track.isrc != null && track.isrc!.isNotEmpty) {
|
||||
localItem = localState.getByIsrc(track.isrc!);
|
||||
}
|
||||
|
||||
// 5. Local library by track name + artist
|
||||
localItem ??= localState.findByTrackAndArtist(track.name, track.artistName);
|
||||
|
||||
if (localItem != null) {
|
||||
await Navigator.of(context).push(
|
||||
PageRouteBuilder(
|
||||
transitionDuration: const Duration(milliseconds: 300),
|
||||
reverseTransitionDuration: const Duration(milliseconds: 250),
|
||||
pageBuilder: (context, animation, secondaryAnimation) =>
|
||||
TrackMetadataScreen(localItem: localItem),
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
|
||||
FadeTransition(opacity: animation, child: child),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 6. Not found anywhere — offer to download
|
||||
_downloadTrack(context, ref);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,206 @@ class DownloadSettingsPage extends ConsumerStatefulWidget {
|
||||
|
||||
class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
static const _builtInServices = ['tidal', 'qobuz', 'amazon'];
|
||||
static const _songLinkRegions = [
|
||||
'AD',
|
||||
'AE',
|
||||
'AG',
|
||||
'AL',
|
||||
'AM',
|
||||
'AO',
|
||||
'AR',
|
||||
'AT',
|
||||
'AU',
|
||||
'AZ',
|
||||
'BA',
|
||||
'BB',
|
||||
'BD',
|
||||
'BE',
|
||||
'BF',
|
||||
'BG',
|
||||
'BH',
|
||||
'BI',
|
||||
'BJ',
|
||||
'BN',
|
||||
'BO',
|
||||
'BR',
|
||||
'BS',
|
||||
'BT',
|
||||
'BW',
|
||||
'BZ',
|
||||
'CA',
|
||||
'CD',
|
||||
'CG',
|
||||
'CH',
|
||||
'CI',
|
||||
'CL',
|
||||
'CM',
|
||||
'CO',
|
||||
'CR',
|
||||
'CV',
|
||||
'CW',
|
||||
'CY',
|
||||
'CZ',
|
||||
'DE',
|
||||
'DJ',
|
||||
'DK',
|
||||
'DM',
|
||||
'DO',
|
||||
'DZ',
|
||||
'EC',
|
||||
'EE',
|
||||
'EG',
|
||||
'ES',
|
||||
'ET',
|
||||
'FI',
|
||||
'FJ',
|
||||
'FM',
|
||||
'FR',
|
||||
'GA',
|
||||
'GB',
|
||||
'GD',
|
||||
'GE',
|
||||
'GH',
|
||||
'GM',
|
||||
'GN',
|
||||
'GQ',
|
||||
'GR',
|
||||
'GT',
|
||||
'GW',
|
||||
'GY',
|
||||
'HK',
|
||||
'HN',
|
||||
'HR',
|
||||
'HT',
|
||||
'HU',
|
||||
'ID',
|
||||
'IE',
|
||||
'IL',
|
||||
'IN',
|
||||
'IQ',
|
||||
'IS',
|
||||
'IT',
|
||||
'JM',
|
||||
'JO',
|
||||
'JP',
|
||||
'KE',
|
||||
'KG',
|
||||
'KH',
|
||||
'KI',
|
||||
'KM',
|
||||
'KN',
|
||||
'KR',
|
||||
'KW',
|
||||
'KZ',
|
||||
'LA',
|
||||
'LB',
|
||||
'LC',
|
||||
'LI',
|
||||
'LK',
|
||||
'LR',
|
||||
'LS',
|
||||
'LT',
|
||||
'LU',
|
||||
'LV',
|
||||
'LY',
|
||||
'MA',
|
||||
'MC',
|
||||
'MD',
|
||||
'ME',
|
||||
'MG',
|
||||
'MH',
|
||||
'MK',
|
||||
'ML',
|
||||
'MN',
|
||||
'MO',
|
||||
'MR',
|
||||
'MT',
|
||||
'MU',
|
||||
'MV',
|
||||
'MW',
|
||||
'MX',
|
||||
'MY',
|
||||
'MZ',
|
||||
'NA',
|
||||
'NE',
|
||||
'NG',
|
||||
'NI',
|
||||
'NL',
|
||||
'NO',
|
||||
'NP',
|
||||
'NR',
|
||||
'NZ',
|
||||
'OM',
|
||||
'PA',
|
||||
'PE',
|
||||
'PG',
|
||||
'PH',
|
||||
'PK',
|
||||
'PL',
|
||||
'PS',
|
||||
'PT',
|
||||
'PW',
|
||||
'PY',
|
||||
'QA',
|
||||
'RO',
|
||||
'RS',
|
||||
'RW',
|
||||
'SA',
|
||||
'SB',
|
||||
'SC',
|
||||
'SE',
|
||||
'SG',
|
||||
'SI',
|
||||
'SK',
|
||||
'SL',
|
||||
'SM',
|
||||
'SN',
|
||||
'SR',
|
||||
'ST',
|
||||
'SV',
|
||||
'SZ',
|
||||
'TD',
|
||||
'TG',
|
||||
'TH',
|
||||
'TJ',
|
||||
'TL',
|
||||
'TN',
|
||||
'TO',
|
||||
'TR',
|
||||
'TT',
|
||||
'TV',
|
||||
'TW',
|
||||
'TZ',
|
||||
'UA',
|
||||
'UG',
|
||||
'US',
|
||||
'UY',
|
||||
'UZ',
|
||||
'VC',
|
||||
'VE',
|
||||
'VN',
|
||||
'VU',
|
||||
'WS',
|
||||
'XK',
|
||||
'ZA',
|
||||
'ZM',
|
||||
'ZW',
|
||||
];
|
||||
static const _songLinkRegionNames = <String, String>{
|
||||
'US': 'United States',
|
||||
'GB': 'United Kingdom',
|
||||
'FR': 'France',
|
||||
'DE': 'Germany',
|
||||
'JP': 'Japan',
|
||||
'KR': 'South Korea',
|
||||
'IN': 'India',
|
||||
'ID': 'Indonesia',
|
||||
'BR': 'Brazil',
|
||||
'MX': 'Mexico',
|
||||
'AU': 'Australia',
|
||||
'CA': 'Canada',
|
||||
'XK': 'Kosovo',
|
||||
};
|
||||
int _androidSdkVersion = 0;
|
||||
bool _hasAllFilesAccess = false;
|
||||
bool _artistFolderFiltersExpanded = false;
|
||||
@@ -536,6 +736,16 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
settings.downloadNetworkMode,
|
||||
),
|
||||
),
|
||||
SettingsItem(
|
||||
icon: Icons.public,
|
||||
title: 'SongLink Region',
|
||||
subtitle: _getSongLinkRegionLabel(settings.songLinkRegion),
|
||||
onTap: () => _showSongLinkRegionPicker(
|
||||
context,
|
||||
ref,
|
||||
settings.songLinkRegion,
|
||||
),
|
||||
),
|
||||
SettingsSwitchItem(
|
||||
icon: Icons.security_outlined,
|
||||
title: 'Network compatibility mode',
|
||||
@@ -1225,6 +1435,14 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
}
|
||||
}
|
||||
|
||||
String _getSongLinkRegionLabel(String code) {
|
||||
final normalized = code.trim().toUpperCase();
|
||||
final effective = normalized.isEmpty ? 'US' : normalized;
|
||||
final name = _songLinkRegionNames[effective];
|
||||
if (name == null) return effective;
|
||||
return '$effective - $name';
|
||||
}
|
||||
|
||||
void _showLyricsModePicker(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
@@ -1630,6 +1848,74 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showSongLinkRegionPicker(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
String current,
|
||||
) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final normalizedCurrent = current.trim().toUpperCase();
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: colorScheme.surfaceContainerHigh,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
|
||||
),
|
||||
builder: (context) => SafeArea(
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.7,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
|
||||
child: Text(
|
||||
'SongLink Region',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 16),
|
||||
child: Text(
|
||||
'Used as userCountry for SongLink API lookup.',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: _songLinkRegions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final code = _songLinkRegions[index];
|
||||
final isSelected = code == normalizedCurrent;
|
||||
final displayName = _songLinkRegionNames[code];
|
||||
return ListTile(
|
||||
title: Text(code),
|
||||
subtitle: displayName != null ? Text(displayName) : null,
|
||||
trailing: isSelected
|
||||
? Icon(Icons.check, color: colorScheme.primary)
|
||||
: null,
|
||||
onTap: () {
|
||||
ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setSongLinkRegion(code);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showFolderOrganizationPicker(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
|
||||
@@ -33,6 +33,7 @@ class DownloadRequestPayload {
|
||||
final String safRelativeDir;
|
||||
final String safFileName;
|
||||
final String safOutputExt;
|
||||
final String songLinkRegion;
|
||||
|
||||
const DownloadRequestPayload({
|
||||
this.isrc = '',
|
||||
@@ -69,6 +70,7 @@ class DownloadRequestPayload {
|
||||
this.safRelativeDir = '',
|
||||
this.safFileName = '',
|
||||
this.safOutputExt = '',
|
||||
this.songLinkRegion = 'US',
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
@@ -107,6 +109,7 @@ class DownloadRequestPayload {
|
||||
'saf_relative_dir': safRelativeDir,
|
||||
'saf_file_name': safFileName,
|
||||
'saf_output_ext': safOutputExt,
|
||||
'songlink_region': songLinkRegion,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -149,6 +152,7 @@ class DownloadRequestPayload {
|
||||
safRelativeDir: safRelativeDir,
|
||||
safFileName: safFileName,
|
||||
safOutputExt: safOutputExt,
|
||||
songLinkRegion: songLinkRegion,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user