mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-09 22:18:45 +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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user