feat: add metadata screen support for local library items

- TrackMetadataScreen now accepts both DownloadHistoryItem and LocalLibraryItem
- Tapping local library tracks in Library tab opens metadata screen
- Shows extracted metadata from audio files (artist, album, track number, etc)
- Supports local cover art display from extracted covers
This commit is contained in:
zarzet 2026-02-04 12:40:33 +07:00
parent 34ffbca3e8
commit e4a6177cb5
3 changed files with 137 additions and 100 deletions

View file

@ -21,6 +21,7 @@
- Deezer: Full support for track, album, playlist, artist links
- Tidal: Track links converted via SongLink to Spotify/Deezer for metadata
- YouTube Music: Handled via ytmusic extension URL handler
- Local library tracks now open metadata screen on tap
### Changed
@ -178,31 +179,26 @@ Same as 3.3.1 but fixes crash issues caused by FFmpeg.
- Perfect for players like Samsung Music that prefer external .lrc files
- LRC files include metadata headers (title, artist, by:SpotiFLAC-Mobile)
- Works with all download services (Tidal, Qobuz, Amazon)
- **CSV Import Quality Selection**: Choose audio quality when importing CSV playlists
- Quality picker now appears before adding CSV tracks to download queue
- Select between FLAC qualities (Lossless, Hi-Res, Hi-Res Max) or MP3
- Respects "Ask quality before download" setting - uses default quality if disabled
- **Persistent Cover Image Cache**: Album/track cover images now cached to persistent storage instead of temporary directory
- Cover images no longer disappear when app is closed or device restarts
- Cache stored in `app_flutter/cover_cache/` directory (not cleared by system)
- Maximum 1000 images cached for up to 365 days
- Covers are cached when displayed in History, Home, Album, Artist, or any other screen
- New `CoverCacheManager` service with `clearCache()` and `getStats()` methods for future cache management
- **Extended Metadata from Deezer Enrichment**: Track downloads now include label, copyright, and genre metadata from Deezer
- New fields in `ExtTrackMetadata`: `label`, `copyright`, `genre`
- Metadata fetched during `enrichTrack()` via Deezer album API
- Embedded as FLAC Vorbis comments: `GENRE`, `ORGANIZATION` (label), `COPYRIGHT`
- Works for both extension downloads and built-in provider downloads (Tidal, Qobuz, Amazon)
- **Track Metadata Screen Extended Info**: Genre, label, and copyright now displayed in track metadata screen
- Added `genre`, `label`, `copyright` fields to `DownloadHistoryItem` model
- Metadata is stored in download history and persists across app restarts
- New localization strings: `trackGenre`, `trackLabel`, `trackCopyright`
- **`utils.randomUserAgent()` for Extensions**: New utility function for extensions to get random browser User-Agent strings
- `**utils.randomUserAgent()` for Extensions**: New utility function for extensions to get random browser User-Agent strings
- Returns modern Chrome User-Agent format: `Chrome/{120-145}.0.{6000-7499}.{100-299}` with `Windows NT 10.0`
- Useful for extensions that need to rotate User-Agents to avoid detection
@ -211,7 +207,6 @@ Same as 3.3.1 but fixes crash issues caused by FFmpeg.
- **Portuguese Language Bug**: Fixed locale parsing for languages with country codes (e.g., pt_PT, es_ES)
- App now correctly loads Portuguese and Spanish translations
- Updated Portuguese label to "Português (Brasil)"
- **VM Race Condition Panic**: Fixed `panic during execution: runtime error: index out of range [-2]` crash when switching search providers
- Root cause: Goja VM was being accessed concurrently by multiple goroutines without synchronization
- Added `VMMu sync.Mutex` to `LoadedExtension` struct
@ -220,16 +215,13 @@ Same as 3.3.1 but fixes crash issues caused by FFmpeg.
- `EnrichTrack`, `CheckAvailability`, `GetDownloadURL`, `Download`
- `CustomSearch`, `HandleURL`, `MatchTrack`, `PostProcess`
- Prevents race conditions when rapidly switching between extension search providers
- **Tidal Release Date Fallback**: Fixed missing release date in FLAC metadata when downloading from Tidal
- Now uses Tidal API's release date when `req.ReleaseDate` is empty
- Ensures release date is always embedded in downloaded files
- **Extended Metadata for M4A→FLAC Conversion**: Fixed genre, label, and copyright not being embedded when converting Amazon M4A to FLAC
- Flutter now extracts extended metadata from Go backend response
- Passes `genre`, `label`, `copyright` parameters to `_embedMetadataAndCover()`
- Tags correctly embedded during FFmpeg conversion
- **Extended Metadata for MP3 Conversion**: Genre, label, and copyright now embedded in MP3 files when converting from FLAC
- Added `genre`, `label`, `copyright` parameters to `_embedMetadataToMp3()`
- Tags embedded as ID3v2: `GENRE`, `ORGANIZATION` (label), `COPYRIGHT`
@ -290,7 +282,6 @@ Same as 3.3.1 but fixes crash issues caused by FFmpeg.
- `go_backend/httputil.go`: Updated `getRandomUserAgent()` to use modern Chrome versions
- `go_backend/tidal.go`: Added release date fallback logic
- `go_backend/exports.go`: Added `Genre`, `Label`, `Copyright` fields to `DownloadResponse`
- **Flutter Changes**:
- `lib/services/cover_cache_manager.dart`: New persistent cache manager for cover images (365 days, 1000 images max)
- `lib/widgets/cached_cover_image.dart`: Wrapper widget for CachedNetworkImage with persistent cache
@ -315,7 +306,6 @@ Same as 3.3.1 but fixes crash issues caused by FFmpeg.
- Spanish: Credits 125 ([@credits125](https://crowdin.com/profile/credits125))
- Portuguese: Pedro Marcondes ([@justapedro](https://crowdin.com/profile/justapedro))
- Russian: Владислав ([@odinokiy_kot](https://crowdin.com/profile/odinokiy_kot))
- **Quick Search Provider Switcher** ([#76](https://github.com/zarzet/SpotiFLAC-Mobile/issues/76)): Dropdown menu in search bar for instant provider switching
- Tap the search icon to reveal a dropdown menu with all available search providers
- Shows default provider (Deezer based on metadata source setting) at the top
@ -325,56 +315,46 @@ Same as 3.3.1 but fixes crash issues caused by FFmpeg.
- Search hint text updates immediately when switching providers
- Re-triggers search automatically if there's existing text in the search bar
- Eliminates need to navigate to Settings > Extensions > Search Provider
- **Extension Button Setting Type** ([#74](https://github.com/zarzet/SpotiFLAC-Mobile/issues/74)): New setting type for extension actions
- Extensions can define `button` type in manifest settings
- Triggers JavaScript function when tapped (e.g., start OAuth flow)
- Useful for authentication, manual sync, or any custom action
- **Genre & Label Metadata** ([#75](https://github.com/zarzet/SpotiFLAC-Mobile/issues/75)): Downloaded tracks now include genre and record label information
- Fetches genre and label from Deezer album API for each track
- Embeds GENRE, ORGANIZATION (label), and COPYRIGHT tags into FLAC files
- Works automatically when Deezer track ID is available (via ISRC matching)
- Supports all download services (Tidal, Qobuz, Amazon) and extension downloads
- **MP3 Quality Option** ([#69](https://github.com/zarzet/SpotiFLAC-Mobile/issues/69)): Optional MP3 download format with FLAC-to-MP3 conversion
- New "Enable MP3 Option" toggle in Settings > Download > Audio Quality
- When enabled, MP3 (320kbps) appears as a quality option alongside FLAC options
- Available in both the quality picker dialog and default quality settings
- Works with all services (Tidal, Qobuz, Amazon) and extensions
- **MP3 Metadata Embedding**: Full metadata support for MP3 files
- Cover art embedded using ID3v2 tags
- Synced lyrics embedded (fetched from lrclib.net)
- All metadata preserved: title, artist, album, album artist, track/disc number, date, ISRC
- Automatic tag conversion from Vorbis comments (FLAC) to ID3v2 (MP3)
- **Dominant Color Header**: Album, Playlist, Downloaded Album, and Track Metadata screens now feature dynamic header backgrounds
- Extracts dominant color from cover art using `palette_generator`
- Creates a gradient from dominant color to theme surface color
- Smooth 500ms color transition animation
- **Larger Cover Art**: Cover images on detail screens are now 50% of screen width (previously 140px fixed)
- More prominent album artwork display
- Larger shadow and rounded corners (20px radius)
- Higher resolution cover caching
- **Sticky Title**: Title appears in AppBar when scrolling past the info card
- Smooth fade-in animation (200ms) when scrolling down
- Title hidden when header is expanded (shows in info card instead)
- AppBar uses theme color (surface) for clean, native look
- Works on Album, Playlist, Downloaded Album, Track Metadata, and Artist screens
- **Artist Name in Album Screen**: Album info card now displays artist name below album title
- Extracted from first track's artist metadata
- Styled with `onSurfaceVariant` color for visual hierarchy
- **Disc Separation for Multi-Disc Albums** ([#70](https://github.com/zarzet/SpotiFLAC-Mobile/issues/70)): Downloaded albums with multiple discs now display tracks grouped by disc
- Visual disc separator header showing "Disc 1", "Disc 2", etc.
- Tracks sorted by disc number first, then by track number
- Single-disc albums display normally without separators
- Fixes confusion when albums have duplicate track numbers across discs
- **Album Grouping in Recents** ([#70](https://github.com/zarzet/SpotiFLAC-Mobile/issues/70)): Downloads now show as albums instead of individual tracks in the Recent section
- Prevents flooding the recents list when downloading full albums
- Groups tracks by album name and artist
@ -387,7 +367,6 @@ Same as 3.3.1 but fixes crash issues caused by FFmpeg.
- MP3 files now saved in the same folder as FLAC (no separate MP3 subfolder)
- Original FLAC file automatically deleted after successful conversion
- New `embedMetadataToMp3()` method for MP3-specific tag embedding
- **Sticky Header Theme Integration**: AppBar background uses `colorScheme.surface` instead of dominant color when collapsed
- Dark theme: Black background with white text
- Light theme: White background with black text
@ -399,12 +378,10 @@ Same as 3.3.1 but fixes crash issues caused by FFmpeg.
- MP3 files now show "320kbps" instead of FLAC's bit depth/sample rate
- History no longer stores FLAC audio specs for converted MP3 files
- Both File Info badges and metadata grid show correct MP3 quality
- **Empty Catch Blocks**: Fixed analyzer warnings for empty catch blocks
- `download_queue_provider.dart`: Added comments explaining why polling errors are silently ignored
- `track_provider.dart`: Added comments explaining why availability check errors are silently ignored
- `ffmpeg_service.dart`: Added proper error logging for temp file cleanup failures
- **Russian Plural Forms**: Fixed ICU syntax warnings in Russian localization
- Removed redundant `=1` clauses that were overriding `one` plural category
- Affected 10 plural strings including track counts and delete confirmations
@ -424,24 +401,20 @@ Same as 3.3.1 but fixes crash issues caused by FFmpeg.
- Thread-safe cache with automatic expiration
- Cache key based on artist, track, and duration
- Log indicator shows "(cached)" when lyrics are served from cache
- **Lyrics Duration Matching**: Improved lyrics accuracy with duration-based matching
- Compares track duration with lrclib.net results
- 10-second tolerance to handle version differences (radio edit, remaster, etc.)
- Prioritizes synced lyrics over plain text when duration matches
- Falls back gracefully if no duration match found
- **Deezer Cover Art Upgrade**: Cover art from Deezer CDN now automatically upgraded to maximum quality
- Detects Deezer CDN URLs (`cdn-images.dzcdn.net`)
- Upgrades cover resolution to 1800x1800 (max available)
- Works alongside existing cover upgrade
- **Live Search for Extensions**: Search-as-you-type functionality for extension search
- 800ms debounce delay to prevent excessive API calls
- Minimum 3 characters required before searching
- Concurrency control to prevent race conditions in extension runtime
- Queues pending searches if a search is already in progress
- **Russian Language Support**: Added Russian (Русский) translation - 99% complete
- Translated via Crowdin community contributions
- Covers all UI elements, settings, and error messages
@ -452,12 +425,10 @@ Same as 3.3.1 but fixes crash issues caused by FFmpeg.
- Added per-directory build lock using `sync.Map` and `sync.Mutex`
- Double-check locking pattern ensures index is built only once
- Significantly improves performance during CSV import with many tracks
- **Queue Tab Scroll Exception**: Fixed Flutter rendering exception with NestedScrollView
- Disabled Material 3 stretch overscroll indicator that caused `_StretchController` assertion
- Wrapped NestedScrollView with ScrollConfiguration to prevent `setState during build` errors
- Issue was especially noticeable during rapid queue updates (CSV import)
- **CSV Import**: Fixed CSV export not being parsed correctly
- Added support for `Artist Name(s)` header (with parentheses)
- Added support for `Track URI` header for track IDs
@ -524,4 +495,4 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int
---
*For older versions, see [GitHub Releases](https://github.com/zarzet/SpotiFLAC-Mobile/releases)*
*For older versions, see [GitHub Releases*](https://github.com/zarzet/SpotiFLAC-Mobile/releases)

View file

@ -1091,6 +1091,21 @@ _precacheCover(historyItem.coverUrl);
).then((_) => _searchFocusNode.unfocus());
}
void _navigateToLocalMetadataScreen(LocalLibraryItem item) {
_searchFocusNode.unfocus();
Navigator.push(
context,
PageRouteBuilder(
transitionDuration: const Duration(milliseconds: 300),
reverseTransitionDuration: const Duration(milliseconds: 250),
pageBuilder: (context, animation, secondaryAnimation) =>
TrackMetadataScreen(localItem: item),
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
FadeTransition(opacity: animation, child: child),
),
).then((_) => _searchFocusNode.unfocus());
}
List<DownloadHistoryItem> _filterHistoryItems(
List<DownloadHistoryItem> items,
String filterMode,
@ -2907,7 +2922,9 @@ child: CachedNetworkImage(
? () => _toggleSelection(item.id)
: isDownloaded
? () => _navigateToHistoryMetadataScreen(item.historyItem!)
: () => _openFile(item.filePath),
: item.localItem != null
? () => _navigateToLocalMetadataScreen(item.localItem!)
: () => _openFile(item.filePath),
onLongPress: _isSelectionMode
? null
: () => _enterSelectionMode(item.id),
@ -3081,7 +3098,9 @@ child: CachedNetworkImage(
? () => _toggleSelection(item.id)
: isDownloaded
? () => _navigateToHistoryMetadataScreen(item.historyItem!)
: () => _openFile(item.filePath),
: item.localItem != null
? () => _navigateToLocalMetadataScreen(item.localItem!)
: () => _openFile(item.filePath),
onLongPress: _isSelectionMode
? null
: () => _enterSelectionMode(item.id),

View file

@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:open_filex/open_filex.dart';
import 'package:spotiflac_android/services/cover_cache_manager.dart';
import 'package:spotiflac_android/services/library_database.dart';
import 'package:spotiflac_android/services/palette_service.dart';
import 'package:spotiflac_android/utils/mime_utils.dart';
import 'package:url_launcher/url_launcher.dart';
@ -14,9 +15,11 @@ import 'package:spotiflac_android/services/platform_bridge.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
class TrackMetadataScreen extends ConsumerStatefulWidget {
final DownloadHistoryItem item;
final DownloadHistoryItem? item;
final LocalLibraryItem? localItem;
const TrackMetadataScreen({super.key, required this.item});
const TrackMetadataScreen({super.key, this.item, this.localItem})
: assert(item != null || localItem != null, 'Either item or localItem must be provided');
@override
ConsumerState<TrackMetadataScreen> createState() => _TrackMetadataScreenState();
@ -88,7 +91,17 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
}
Future<void> _extractDominantColor() async {
final coverUrl = widget.item.coverUrl;
// For local items with cover path, extract from file
if (_isLocalItem && _localCoverPath != null && _localCoverPath!.isNotEmpty) {
final color = await PaletteService.instance.extractDominantColorFromFile(_localCoverPath!);
if (mounted && color != null && color != _dominantColor) {
setState(() => _dominantColor = color);
}
return;
}
final coverUrl = _coverUrl;
if (coverUrl == null) return;
// Check cache first
final cachedColor = PaletteService.instance.getCached(coverUrl);
@ -107,7 +120,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
}
Future<void> _checkFile() async {
var filePath = widget.item.filePath;
var filePath = _filePath;
if (filePath.startsWith('EXISTS:')) {
filePath = filePath.substring(7);
}
@ -134,25 +147,38 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
}
}
DownloadHistoryItem get item => widget.item;
String get trackName => item.trackName;
String get artistName => item.artistName;
String get albumName => item.albumName;
String? get albumArtist => _normalizeOptionalString(item.albumArtist);
int? get trackNumber => item.trackNumber;
int? get discNumber => item.discNumber;
String? get releaseDate => item.releaseDate;
String? get isrc => item.isrc;
String? get genre => item.genre;
String? get label => item.label;
String? get copyright => item.copyright;
bool get _isLocalItem => widget.localItem != null;
DownloadHistoryItem? get _downloadItem => widget.item;
LocalLibraryItem? get _localLibraryItem => widget.localItem;
String get _itemId => _isLocalItem ? _localLibraryItem!.id : _downloadItem!.id;
String get trackName => _isLocalItem ? _localLibraryItem!.trackName : _downloadItem!.trackName;
String get artistName => _isLocalItem ? _localLibraryItem!.artistName : _downloadItem!.artistName;
String get albumName => _isLocalItem ? _localLibraryItem!.albumName : _downloadItem!.albumName;
String? get albumArtist => _normalizeOptionalString(_isLocalItem ? _localLibraryItem!.albumArtist : _downloadItem!.albumArtist);
int? get trackNumber => _isLocalItem ? _localLibraryItem!.trackNumber : _downloadItem!.trackNumber;
int? get discNumber => _isLocalItem ? _localLibraryItem!.discNumber : _downloadItem!.discNumber;
String? get releaseDate => _isLocalItem ? _localLibraryItem!.releaseDate : _downloadItem!.releaseDate;
String? get isrc => _isLocalItem ? _localLibraryItem!.isrc : _downloadItem!.isrc;
String? get genre => _isLocalItem ? _localLibraryItem!.genre : _downloadItem!.genre;
String? get label => _isLocalItem ? null : _downloadItem!.label;
String? get copyright => _isLocalItem ? null : _downloadItem!.copyright;
int? get duration => _isLocalItem ? _localLibraryItem!.duration : _downloadItem!.duration;
int? get bitDepth => _isLocalItem ? _localLibraryItem!.bitDepth : _downloadItem!.bitDepth;
int? get sampleRate => _isLocalItem ? _localLibraryItem!.sampleRate : _downloadItem!.sampleRate;
String get _filePath => _isLocalItem ? _localLibraryItem!.filePath : _downloadItem!.filePath;
String? get _coverUrl => _isLocalItem ? null : _downloadItem!.coverUrl;
String? get _localCoverPath => _isLocalItem ? _localLibraryItem!.coverPath : null;
String? get _spotifyId => _isLocalItem ? null : _downloadItem!.spotifyId;
String get _service => _isLocalItem ? 'local' : _downloadItem!.service;
DateTime get _addedAt => _isLocalItem ? _localLibraryItem!.scannedAt : _downloadItem!.downloadedAt;
String? get _quality => _isLocalItem ? null : _downloadItem!.quality;
String get cleanFilePath {
final path = item.filePath;
final path = _filePath;
return path.startsWith('EXISTS:') ? path.substring(7) : path;
}
int? get bitDepth => item.bitDepth;
int? get sampleRate => item.sampleRate;
@override
Widget build(BuildContext context) {
@ -287,7 +313,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
child: Padding(
padding: const EdgeInsets.only(top: 60),
child: Hero(
tag: 'cover_${item.id}',
tag: 'cover_$_itemId',
child: Container(
width: coverSize,
height: coverSize,
@ -303,9 +329,9 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: item.coverUrl != null
? CachedNetworkImage(
imageUrl: item.coverUrl!,
child: _coverUrl != null
? CachedNetworkImage(
imageUrl: _coverUrl!,
fit: BoxFit.cover,
memCacheWidth: (coverSize * 2).toInt(),
cacheManager: CoverCacheManager.instance,
@ -318,14 +344,19 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
),
),
)
: Container(
color: colorScheme.surfaceContainerHighest,
child: Icon(
Icons.music_note,
size: 64,
color: colorScheme.onSurfaceVariant,
),
),
: _localCoverPath != null && _localCoverPath!.isNotEmpty
? Image.file(
File(_localCoverPath!),
fit: BoxFit.cover,
)
: Container(
color: colorScheme.surfaceContainerHighest,
child: Icon(
Icons.music_note,
size: 64,
color: colorScheme.onSurfaceVariant,
),
),
),
),
),
@ -448,11 +479,11 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
_buildMetadataGrid(context, colorScheme),
if (item.spotifyId != null && item.spotifyId!.isNotEmpty) ...[
if (_spotifyId != null && _spotifyId!.isNotEmpty) ...[
const SizedBox(height: 8),
Builder(
builder: (context) {
final isDeezer = item.spotifyId!.contains('deezer');
final isDeezer = _spotifyId!.contains('deezer');
return OutlinedButton.icon(
onPressed: () => _openServiceUrl(context),
icon: const Icon(Icons.open_in_new, size: 18),
@ -474,10 +505,10 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
}
Future<void> _openServiceUrl(BuildContext context) async {
if (item.spotifyId == null) return;
if (_spotifyId == null) return;
final isDeezer = item.spotifyId!.contains('deezer');
final rawId = item.spotifyId!.replaceAll('deezer:', '');
final isDeezer = _spotifyId!.contains('deezer');
final rawId = _spotifyId!.replaceAll('deezer:', '');
final webUrl = isDeezer
? 'https://www.deezer.com/track/$rawId'
@ -519,12 +550,12 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
Widget _buildMetadataGrid(BuildContext context, ColorScheme colorScheme) {
// Determine audio quality string - prefer stored quality from download
String? audioQualityStr;
final fileName = item.filePath.split('/').last;
final fileName = _filePath.split('/').last;
final fileExt = fileName.contains('.') ? fileName.split('.').last.toUpperCase() : '';
// Use stored quality from download history if available
if (item.quality != null && item.quality!.isNotEmpty) {
audioQualityStr = item.quality;
if (_quality != null && _quality!.isNotEmpty) {
audioQualityStr = _quality;
} else if (bitDepth != null && sampleRate != null) {
// Fallback for FLAC files without stored quality
final sampleRateKHz = (sampleRate! / 1000).toStringAsFixed(1);
@ -550,8 +581,8 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
_MetadataItem(context.l10n.trackTrackNumber, trackNumber.toString()),
if (discNumber != null && discNumber! > 0)
_MetadataItem(context.l10n.trackDiscNumber, discNumber.toString()),
if (item.duration != null)
_MetadataItem(context.l10n.trackDuration, _formatDuration(item.duration!)),
if (duration != null)
_MetadataItem(context.l10n.trackDuration, _formatDuration(duration!)),
if (audioQualityStr != null)
_MetadataItem(context.l10n.trackAudioQuality, audioQualityStr),
if (releaseDate != null && releaseDate!.isNotEmpty)
@ -566,16 +597,17 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
_MetadataItem('ISRC', isrc!),
];
if (item.spotifyId != null && item.spotifyId!.isNotEmpty) {
final isDeezer = item.spotifyId!.contains('deezer');
final cleanId = item.spotifyId!.replaceAll('deezer:', '');
if (!_isLocalItem && _spotifyId != null && _spotifyId!.isNotEmpty) {
final isDeezer = _spotifyId!.contains('deezer');
final cleanId = _spotifyId!.replaceAll('deezer:', '');
items.add(_MetadataItem(isDeezer ? 'Deezer ID' : 'Spotify ID', cleanId));
}
items.addAll([
_MetadataItem(context.l10n.trackMetadataService, item.service.toUpperCase()),
_MetadataItem(context.l10n.trackDownloaded, _formatFullDate(item.downloadedAt)),
]);
items.add(_MetadataItem(context.l10n.trackMetadataService, _service.toUpperCase()));
items.add(_MetadataItem(
context.l10n.trackDownloaded,
_formatFullDate(_addedAt),
));
return Column(
children: items.map((metadata) {
@ -728,20 +760,20 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: _getServiceColor(item.service, colorScheme),
color: _getServiceColor(_service, colorScheme),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_getServiceIcon(item.service),
_getServiceIcon(_service),
size: 14,
color: Colors.white,
),
const SizedBox(width: 4),
Text(
item.service.toUpperCase(),
_service.toUpperCase(),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
@ -943,14 +975,14 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
try {
// Convert duration from seconds to milliseconds
final durationMs = (item.duration ?? 0) * 1000;
final durationMs = (duration ?? 0) * 1000;
// First, check if lyrics are embedded in the file
if (_fileExists) {
final embeddedResult = await PlatformBridge.getLyricsLRC(
'',
item.trackName,
item.artistName,
trackName,
artistName,
filePath: cleanFilePath,
durationMs: 0,
).timeout(const Duration(seconds: 5), onTimeout: () => '');
@ -971,9 +1003,9 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
// No embedded lyrics, fetch from online
final result = await PlatformBridge.getLyricsLRC(
item.spotifyId ?? '',
item.trackName,
item.artistName,
_spotifyId ?? '',
trackName,
artistName,
filePath: null, // Don't check file again
durationMs: durationMs,
).timeout(
@ -1177,17 +1209,32 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
),
TextButton(
onPressed: () async {
try {
final file = File(cleanFilePath);
if (await file.exists()) {
await file.delete();
if (_isLocalItem) {
// For local items, just delete the file
try {
final file = File(cleanFilePath);
if (await file.exists()) {
await file.delete();
}
} catch (e) {
debugPrint('Failed to delete file: $e');
}
} catch (e) {
debugPrint('Failed to delete file: $e');
// Also remove from local library database
// ref.read(localLibraryProvider.notifier).removeItem(_localLibraryItem!.id);
} else {
// Existing download history deletion logic
try {
final file = File(cleanFilePath);
if (await file.exists()) {
await file.delete();
}
} catch (e) {
debugPrint('Failed to delete file: $e');
}
ref.read(downloadHistoryProvider.notifier).removeFromHistory(_downloadItem!.id);
}
ref.read(downloadHistoryProvider.notifier).removeFromHistory(item.id);
if (context.mounted) {
Navigator.pop(context);
Navigator.pop(context);
@ -1242,7 +1289,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
await SharePlus.instance.share(
ShareParams(
files: [XFile(cleanFilePath)],
text: '${item.trackName} - ${item.artistName}',
text: '$trackName - $artistName',
),
);
}