part of 'queue_tab.dart'; extension _QueueTabItemWidgets on _QueueTabState { Widget _buildSelectionBottomBar( BuildContext context, ColorScheme colorScheme, List unifiedItems, double bottomPadding, ) { final selectedCount = _selectedIds.length; final allSelected = selectedCount == unifiedItems.length && unifiedItems.isNotEmpty; final localOnlySelection = _isLocalOnlySelection(unifiedItems); final flacEligibleCount = _selectedFlacEligibleLocalItems( unifiedItems, ).length; return Container( decoration: BoxDecoration( color: colorScheme.surfaceContainerHigh, borderRadius: const BorderRadius.vertical(top: Radius.circular(28)), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.15), blurRadius: 12, offset: const Offset(0, -4), ), ], ), child: SafeArea( top: false, child: Padding( padding: EdgeInsets.fromLTRB(16, 16, 16, bottomPadding > 0 ? 8 : 16), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 32, height: 4, margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: colorScheme.outlineVariant, borderRadius: BorderRadius.circular(2), ), ), Row( children: [ IconButton.filledTonal( onPressed: _exitSelectionMode, tooltip: MaterialLocalizations.of( context, ).closeButtonTooltip, icon: const Icon(Icons.close), style: IconButton.styleFrom( backgroundColor: colorScheme.surfaceContainerHighest, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( context.l10n.selectionSelected(selectedCount), style: Theme.of(context).textTheme.titleMedium ?.copyWith(fontWeight: FontWeight.bold), ), Text( allSelected ? context.l10n.selectionAllSelected : context.l10n.downloadedAlbumTapToSelect, style: Theme.of(context).textTheme.bodySmall ?.copyWith(color: colorScheme.onSurfaceVariant), ), ], ), ), TextButton.icon( onPressed: () { if (allSelected) { _exitSelectionMode(); } else { _selectAll(unifiedItems); } }, icon: Icon( allSelected ? Icons.deselect : Icons.select_all, size: 20, ), label: Text( allSelected ? context.l10n.actionDeselect : context.l10n.actionSelectAll, ), style: TextButton.styleFrom( foregroundColor: colorScheme.primary, ), ), ], ), const SizedBox(height: 12), LayoutBuilder( builder: (context, constraints) { const spacing = 8.0; final itemWidth = (constraints.maxWidth - spacing) / 2; final actions = []; if (localOnlySelection && flacEligibleCount > 0) { actions.add( _SelectionActionButton( icon: Icons.download_for_offline_outlined, label: '${context.l10n.queueFlacAction} ($flacEligibleCount)', onPressed: () => _queueSelectedLocalAsFlac(unifiedItems), colorScheme: colorScheme, ), ); } actions.add( _SelectionActionButton( icon: localOnlySelection ? Icons.auto_fix_high_outlined : Icons.share_outlined, label: localOnlySelection ? '${context.l10n.trackReEnrich} ($selectedCount)' : context.l10n.selectionShareCount(selectedCount), onPressed: selectedCount > 0 ? () => localOnlySelection ? _reEnrichSelectedLocalFromQueue(unifiedItems) : _shareSelected(unifiedItems) : null, colorScheme: colorScheme, ), ); actions.add( _SelectionActionButton( icon: Icons.swap_horiz, label: context.l10n.selectionConvertCount(selectedCount), onPressed: selectedCount > 0 ? () => _showBatchConvertSheet(context, unifiedItems) : null, colorScheme: colorScheme, ), ); actions.add( _SelectionActionButton( icon: Icons.graphic_eq, label: context.l10n.selectionReplayGainCount( selectedCount, ), onPressed: selectedCount > 0 ? () => _runBatchReplayGain(unifiedItems) : null, colorScheme: colorScheme, ), ); return Wrap( spacing: spacing, runSpacing: spacing, children: [ for (final action in actions) SizedBox(width: itemWidth, child: action), ], ); }, ), const SizedBox(height: 8), SizedBox( width: double.infinity, child: FilledButton.icon( onPressed: selectedCount > 0 ? () => _deleteSelected(unifiedItems) : null, icon: const Icon(Icons.delete_outline), label: Text( selectedCount > 0 ? context.l10n.selectionDeleteTracksCount(selectedCount) : context.l10n.selectionSelectToDelete, ), style: FilledButton.styleFrom( backgroundColor: selectedCount > 0 ? colorScheme.error : colorScheme.surfaceContainerHighest, foregroundColor: selectedCount > 0 ? colorScheme.onError : colorScheme.onSurfaceVariant, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), ), ), ), ], ), ), ), ); } Widget _buildQueueItem( BuildContext context, DownloadItem item, ColorScheme colorScheme, ) { final isCompleted = item.status == DownloadStatus.completed; final isActive = item.status == DownloadStatus.queued || item.status == DownloadStatus.downloading || item.status == DownloadStatus.finalizing; return Dismissible( key: ValueKey('dismiss_${item.id}'), direction: DismissDirection.endToStart, confirmDismiss: isActive ? (_) async { return await showDialog( context: context, builder: (ctx) => AlertDialog( title: Text(context.l10n.cancelDownloadTitle), content: Text( context.l10n.cancelDownloadContent(item.track.name), ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: Text(context.l10n.cancelDownloadKeep), ), TextButton( onPressed: () => Navigator.of(ctx).pop(true), child: Text(context.l10n.dialogCancel), ), ], ), ) ?? false; } : null, onDismissed: (_) { ref.read(downloadQueueProvider.notifier).dismissItem(item.id); }, background: Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), decoration: BoxDecoration( color: colorScheme.errorContainer, borderRadius: BorderRadius.circular(12), ), alignment: Alignment.centerRight, padding: const EdgeInsets.only(right: 20), child: Icon(Icons.delete_outline, color: colorScheme.onErrorContainer), ), child: DownloadSuccessOverlay( showSuccess: isCompleted, child: Card( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), clipBehavior: Clip.antiAlias, child: InkWell( onTap: isCompleted ? () => _navigateToMetadataScreen(item) : item.status == DownloadStatus.failed ? () => _showDownloadErrorDialog(context, item) : null, borderRadius: BorderRadius.circular(12), child: Stack( children: [ if (item.status == DownloadStatus.downloading) Positioned.fill( child: Align( alignment: Alignment.centerLeft, child: FractionallySizedBox( widthFactor: item.progress.clamp(0.0, 1.0), child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, colors: [ colorScheme.primary.withValues(alpha: 0.16), colorScheme.primary.withValues(alpha: 0.04), ], ), ), ), ), ), ), Padding( padding: const EdgeInsets.all(12), child: Row( children: [ isCompleted ? Hero( tag: 'cover_${item.id}', child: _buildCoverArt(item, colorScheme), ) : _buildCoverArt(item, colorScheme), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.track.name, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.titleSmall ?.copyWith(fontWeight: FontWeight.w600), ), const SizedBox(height: 2), ClickableArtistName( artistName: item.track.artistName, artistId: item.track.artistId, coverUrl: item.track.coverUrl, extensionId: item.track.source, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall ?.copyWith( color: colorScheme.onSurfaceVariant, ), ), if (item.status == DownloadStatus.downloading) ...[ const SizedBox(height: 5), Row( children: [ Icon( Icons.download_rounded, size: 12, color: colorScheme.primary, ), const SizedBox(width: 4), Expanded( child: Text( _formatDownloadStatusLine(context, item), maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context) .textTheme .labelSmall ?.copyWith( color: colorScheme.primary, fontWeight: FontWeight.w600, ), ), ), ], ), ], if (item.status == DownloadStatus.failed) ...[ const SizedBox(height: 4), _buildDownloadFailureMessage( context, item, colorScheme, ), ], ], ), ), const SizedBox(width: 8), _buildActionButtons(context, item, colorScheme), ], ), ), ], ), ), ), ), ); } /// Download error messages are stored as fixed English sentinels on the /// item (so code can match on them); translate the known ones for display. String _localizedDownloadError(BuildContext context, String raw) { if (raw == safPermissionLostErrorMessage) { return context.l10n.downloadErrorSafPermissionLost; } if (raw == downloadFolderAccessLostErrorMessage) { return context.l10n.downloadErrorFolderAccessLost; } return raw; } Widget _buildDownloadFailureMessage( BuildContext context, DownloadItem item, ColorScheme colorScheme, ) { if (item.errorType != DownloadErrorType.rateLimit) { return Text( _localizedDownloadError(context, item.errorMessage), maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of( context, ).textTheme.labelSmall?.copyWith(color: colorScheme.error), ); } return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(top: 1), child: Icon( Icons.hourglass_top_rounded, size: 14, color: colorScheme.tertiary, ), ), const SizedBox(width: 6), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( context.l10n.queueRateLimitTitle, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.labelSmall?.copyWith( color: colorScheme.tertiary, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 1), Text( context.l10n.queueRateLimitMessage, maxLines: 2, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.labelSmall?.copyWith( color: colorScheme.tertiary.withValues(alpha: 0.8), ), ), ], ), ), ], ); } Widget _buildCoverArt(DownloadItem item, ColorScheme colorScheme) { final coverSize = _queueCoverSize(); final radius = BorderRadius.circular(8); final cover = item.track.coverUrl != null ? CachedCoverImage( imageUrl: item.track.coverUrl!, width: coverSize, height: coverSize, borderRadius: radius, fadeInDuration: const Duration(milliseconds: 180), fadeOutDuration: const Duration(milliseconds: 90), ) : Container( width: coverSize, height: coverSize, decoration: BoxDecoration( color: colorScheme.surfaceContainerHighest, borderRadius: radius, ), child: Icon(Icons.music_note, color: colorScheme.onSurfaceVariant), ); final isDownloading = item.status == DownloadStatus.downloading || item.status == DownloadStatus.finalizing; if (!isDownloading) return cover; final progress = item.progress.clamp(0.0, 1.0); final indeterminate = item.status == DownloadStatus.finalizing || progress <= 0; return SizedBox( width: coverSize, height: coverSize, child: Stack( fit: StackFit.expand, children: [ cover, ClipRRect( borderRadius: radius, child: ColoredBox(color: Colors.black.withValues(alpha: 0.45)), ), Center( child: SizedBox( width: coverSize * 0.6, height: coverSize * 0.6, child: CircularProgressIndicator( value: indeterminate ? null : progress, strokeWidth: 3, color: Colors.white, backgroundColor: Colors.white.withValues(alpha: 0.25), ), ), ), if (!indeterminate) Center( child: Text( '${(progress * 100).round()}', style: const TextStyle( color: Colors.white, fontSize: 11, fontWeight: FontWeight.w700, ), ), ), ], ), ); } Widget _buildActionButtons( BuildContext context, DownloadItem item, ColorScheme colorScheme, ) { switch (item.status) { case DownloadStatus.queued: return IconButton( onPressed: () => ref.read(downloadQueueProvider.notifier).cancelItem(item.id), icon: Icon(Icons.close, color: colorScheme.error), tooltip: context.l10n.dialogCancel, style: IconButton.styleFrom( backgroundColor: colorScheme.errorContainer.withValues(alpha: 0.3), ), ); case DownloadStatus.downloading: return IconButton( onPressed: () => ref.read(downloadQueueProvider.notifier).cancelItem(item.id), icon: Icon(Icons.stop, color: colorScheme.error), tooltip: context.l10n.actionStop, style: IconButton.styleFrom( backgroundColor: colorScheme.errorContainer.withValues(alpha: 0.3), ), ); case DownloadStatus.finalizing: return Semantics( label: context.l10n.queueFinalizingDownload, child: SizedBox( width: 40, height: 40, child: Stack( alignment: Alignment.center, children: [ CircularProgressIndicator( strokeWidth: 3, color: colorScheme.tertiary, ), ExcludeSemantics( child: Icon( Icons.edit_note, color: colorScheme.tertiary, size: 16, ), ), ], ), ), ); case DownloadStatus.completed: return ValueListenableBuilder( valueListenable: _fileExistsListenable(item.filePath), builder: (context, fileExists, child) { return Row( mainAxisSize: MainAxisSize.min, children: [ if (fileExists) IconButton( onPressed: () => _openFile( item.filePath!, title: item.track.name, artist: item.track.artistName, album: item.track.albumName, coverUrl: item.track.coverUrl ?? '', ), icon: Icon(Icons.play_arrow, color: colorScheme.primary), tooltip: context.l10n.tooltipPlay, style: IconButton.styleFrom( backgroundColor: colorScheme.primaryContainer.withValues( alpha: 0.3, ), ), ) else Semantics( label: context.l10n.queueDownloadedFileMissing, child: ExcludeSemantics( child: Icon( Icons.error_outline, color: colorScheme.error, size: 20, ), ), ), const SizedBox(width: 4), Semantics( label: context.l10n.queueDownloadCompleted, child: ExcludeSemantics( child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: colorScheme.primaryContainer, shape: BoxShape.circle, ), child: Icon( Icons.check, color: colorScheme.onPrimaryContainer, size: 20, ), ), ), ), ], ); }, ); case DownloadStatus.failed: case DownloadStatus.skipped: return Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( onPressed: () => ref.read(downloadQueueProvider.notifier).retryItem(item.id), icon: Icon(Icons.refresh, color: colorScheme.primary), tooltip: context.l10n.dialogRetry, style: IconButton.styleFrom( backgroundColor: colorScheme.primaryContainer.withValues( alpha: 0.3, ), ), ), const SizedBox(width: 4), IconButton( onPressed: () => ref.read(downloadQueueProvider.notifier).removeItem(item.id), icon: Icon( Icons.close, color: item.status == DownloadStatus.failed ? colorScheme.error : colorScheme.onSurfaceVariant, ), tooltip: context.l10n.dialogRemove, style: item.status == DownloadStatus.failed ? IconButton.styleFrom( backgroundColor: colorScheme.errorContainer.withValues( alpha: 0.3, ), ) : null, ), ], ); } } Widget _buildFilterButton( BuildContext context, List unifiedItems, ) { return GestureDetector( onLongPress: _activeFilterCount > 0 ? _resetFilters : null, child: TextButton.icon( onPressed: () => _showFilterSheet(context, unifiedItems), icon: Badge( isLabelVisible: _activeFilterCount > 0, label: Text('$_activeFilterCount'), child: const Icon(Icons.filter_list, size: 18), ), label: Text(context.l10n.libraryFilterTitle), style: TextButton.styleFrom(visualDensity: VisualDensity.compact), ), ); } /// When [size] is provided, renders at fixed dimensions (list mode). /// When [size] is null, fills the parent container (grid mode). Widget _buildUnifiedCoverImage( UnifiedLibraryItem item, ColorScheme colorScheme, [ double? size, ]) { final isDownloaded = item.source == LibraryItemSource.downloaded; // For downloaded items, listen to embedded cover version so the cover // updates after async extraction completes. if (isDownloaded) { return ValueListenableBuilder( valueListenable: _embeddedCoverVersion, builder: (context, _, child) => _buildUnifiedCoverImageInner(item, colorScheme, isDownloaded, size), ); } return _buildUnifiedCoverImageInner(item, colorScheme, isDownloaded, size); } Widget _buildUnifiedCoverImageInner( UnifiedLibraryItem item, ColorScheme colorScheme, bool isDownloaded, [ double? size, ]) { final cacheSize = size != null ? (size * 2).toInt() : 200; final iconSize = size != null ? size * 0.4 : 32.0; Widget buildPlaceholder({bool isLocal = false}) { final bgColor = (isDownloaded && !isLocal) ? colorScheme.surfaceContainerHighest : colorScheme.secondaryContainer; final fgColor = (isDownloaded && !isLocal) ? colorScheme.onSurfaceVariant : colorScheme.onSecondaryContainer; return Container( width: size, height: size, decoration: size != null ? BoxDecoration( color: bgColor, borderRadius: BorderRadius.circular(8), ) : null, color: size != null ? null : bgColor, child: Center( child: Icon(Icons.music_note, color: fgColor, size: iconSize), ), ); } Widget fadeInFileImage(Widget child, int? frame, bool wasSync) { if (wasSync) return child; final Widget backdrop; if (isDownloaded && item.coverUrl != null) { backdrop = CachedCoverImage( imageUrl: item.coverUrl!, width: size, height: size, memCacheWidth: cacheSize, memCacheHeight: cacheSize, placeholder: (context, url) => buildPlaceholder(), errorWidget: (context, url, error) => buildPlaceholder(), ); } else { backdrop = buildPlaceholder(isLocal: !isDownloaded); } final animated = Stack( fit: StackFit.expand, children: [ backdrop, AnimatedOpacity( opacity: frame == null ? 0.0 : 1.0, duration: const Duration(milliseconds: 180), curve: Curves.easeOutCubic, child: child, ), ], ); if (size == null) return animated; return SizedBox(width: size, height: size, child: animated); } if (isDownloaded) { final embeddedCoverPath = _resolveDownloadedEmbeddedCoverPath( item.filePath, ); if (embeddedCoverPath != null) { return ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.file( File(embeddedCoverPath), width: size, height: size, fit: BoxFit.cover, cacheWidth: cacheSize, cacheHeight: cacheSize, gaplessPlayback: true, frameBuilder: (context, child, frame, wasSynchronouslyLoaded) => fadeInFileImage(child, frame, wasSynchronouslyLoaded), errorBuilder: (context, error, stackTrace) => buildPlaceholder(), ), ); } } if (item.coverUrl != null) { return CachedCoverImage( imageUrl: item.coverUrl!, width: size, height: size, memCacheWidth: cacheSize, memCacheHeight: cacheSize, borderRadius: BorderRadius.circular(8), placeholder: (context, url) => buildPlaceholder(), errorWidget: (context, url, error) => buildPlaceholder(), fadeInDuration: const Duration(milliseconds: 180), fadeOutDuration: const Duration(milliseconds: 90), ); } if (item.localCoverPath != null && item.localCoverPath!.isNotEmpty) { return ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.file( File(item.localCoverPath!), width: size, height: size, fit: BoxFit.cover, cacheWidth: cacheSize, cacheHeight: cacheSize, gaplessPlayback: true, frameBuilder: (context, child, frame, wasSynchronouslyLoaded) => fadeInFileImage(child, frame, wasSynchronouslyLoaded), errorBuilder: (context, error, stackTrace) => buildPlaceholder(isLocal: true), ), ); } if (size != null) { return buildPlaceholder(); } return ClipRRect( borderRadius: BorderRadius.circular(8), child: buildPlaceholder(), ); } Widget _buildUnifiedLibraryItem( BuildContext context, UnifiedLibraryItem item, ColorScheme colorScheme, { required List downloadedNavigationItems, required int? downloadedNavigationIndex, required List localNavigationItems, required int? localNavigationIndex, required List libraryItems, }) { final fileExistsListenable = _fileExistsListenable(item.filePath); final isSelected = _selectedIds.contains(item.id); final date = item.addedAt; final dateStr = '${_QueueTabState._months[date.month - 1]} ${date.day}, ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}'; final isDownloaded = item.source == LibraryItemSource.downloaded; final sourceLabel = isDownloaded ? context.l10n.librarySourceDownloaded : context.l10n.librarySourceLocal; final sourceColor = isDownloaded ? colorScheme.primaryContainer : colorScheme.secondaryContainer; final sourceTextColor = isDownloaded ? colorScheme.onPrimaryContainer : colorScheme.onSecondaryContainer; return Semantics( label: context.l10n.a11yTrackByArtist(item.trackName, item.artistName), selected: isSelected, child: Card( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), color: isSelected ? colorScheme.primaryContainer.withValues(alpha: 0.3) : null, child: InkWell( onTap: _isSelectionMode ? () => _toggleSelection(item.id) : isDownloaded ? () => _navigateToHistoryMetadataScreen( item.historyItem!, navigationItems: downloadedNavigationItems, navigationIndex: downloadedNavigationIndex, ) : item.localItem != null ? () => _navigateToLocalMetadataScreen( item.localItem!, navigationItems: localNavigationItems, navigationIndex: localNavigationIndex, ) : () => _openFile( item.filePath, title: item.trackName, artist: item.artistName, album: item.albumName, coverUrl: item.coverUrl ?? item.localCoverPath ?? '', ), onLongPress: _isSelectionMode ? null : () => _enterSelectionMode(item.id), borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(12), child: Row( children: [ if (_isSelectionMode) ...[ Semantics( checked: isSelected, label: isSelected ? context.l10n.a11yDeselectTrack : context.l10n.a11ySelectTrack, child: AnimatedSelectionCheckbox( visible: true, selected: isSelected, colorScheme: colorScheme, size: 24, ), ), const SizedBox(width: 12), ], Hero( tag: 'cover_lib_${item.id}', child: _buildUnifiedCoverImage(item, colorScheme, 56), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.trackName, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 2), ClickableArtistName( artistName: item.artistName, coverUrl: item.coverUrl, extensionId: item.historyItem?.service, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 2), Row( children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: sourceColor, borderRadius: BorderRadius.circular(4), ), child: Text( sourceLabel, style: Theme.of(context).textTheme.labelSmall ?.copyWith( color: sourceTextColor, fontSize: 10, fontWeight: FontWeight.w500, ), ), ), const SizedBox(width: 8), Flexible( child: Text( dateStr, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.labelSmall ?.copyWith( color: colorScheme.onSurfaceVariant .withValues(alpha: 0.7), ), ), ), if (item.quality != null && item.quality!.isNotEmpty) ...[ const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: item.quality!.startsWith('24') ? colorScheme.primaryContainer : colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(4), ), child: Text( item.quality!, style: Theme.of(context).textTheme.labelSmall ?.copyWith( color: item.quality!.startsWith('24') ? colorScheme.onPrimaryContainer : colorScheme.onSurfaceVariant, fontSize: 10, fontWeight: FontWeight.w500, ), ), ), ], ], ), ], ), ), const SizedBox(width: 8), if (!_isSelectionMode) ValueListenableBuilder( valueListenable: fileExistsListenable, builder: (context, fileExists, child) { return Row( mainAxisSize: MainAxisSize.min, children: [ if (fileExists) IconButton( onPressed: () => _playLibraryItem(item, libraryItems), icon: Icon( Icons.play_arrow, color: colorScheme.primary, ), tooltip: context.l10n.tooltipPlay, style: IconButton.styleFrom( backgroundColor: colorScheme.primaryContainer .withValues(alpha: 0.3), ), ) else Icon( Icons.error_outline, color: colorScheme.error, size: 20, ), ], ); }, ), ], ), ), ), ), ); } Widget _buildUnifiedGridItem( BuildContext context, UnifiedLibraryItem item, ColorScheme colorScheme, { required List downloadedNavigationItems, required int? downloadedNavigationIndex, required List localNavigationItems, required int? localNavigationIndex, required List libraryItems, }) { final fileExistsListenable = _fileExistsListenable(item.filePath); final isSelected = _selectedIds.contains(item.id); final isDownloaded = item.source == LibraryItemSource.downloaded; return GestureDetector( onTap: _isSelectionMode ? () => _toggleSelection(item.id) : isDownloaded ? () => _navigateToHistoryMetadataScreen( item.historyItem!, navigationItems: downloadedNavigationItems, navigationIndex: downloadedNavigationIndex, ) : item.localItem != null ? () => _navigateToLocalMetadataScreen( item.localItem!, navigationItems: localNavigationItems, navigationIndex: localNavigationIndex, ) : () => _openFile( item.filePath, title: item.trackName, artist: item.artistName, album: item.albumName, coverUrl: item.coverUrl ?? item.localCoverPath ?? '', ), onLongPress: _isSelectionMode ? null : () => _enterSelectionMode(item.id), child: Stack( children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Stack( children: [ AspectRatio( aspectRatio: 1, child: Hero( tag: 'cover_lib_${item.id}', child: _buildUnifiedCoverImage(item, colorScheme), ), ), Positioned( right: 4, top: 4, child: Container( padding: const EdgeInsets.symmetric( horizontal: 4, vertical: 2, ), decoration: BoxDecoration( color: isDownloaded ? colorScheme.primaryContainer : colorScheme.secondaryContainer, borderRadius: BorderRadius.circular(4), ), child: Icon( isDownloaded ? Icons.download_done : Icons.folder, size: 12, color: isDownloaded ? colorScheme.onPrimaryContainer : colorScheme.onSecondaryContainer, ), ), ), if (item.quality != null && item.quality!.isNotEmpty) Positioned( left: 4, top: 4, child: Container( padding: const EdgeInsets.symmetric( horizontal: 4, vertical: 2, ), decoration: BoxDecoration( color: item.quality!.startsWith('24') ? colorScheme.primary : colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(4), ), child: Text( _getQualityBadgeText(item.quality!), style: Theme.of(context).textTheme.labelSmall ?.copyWith( color: item.quality!.startsWith('24') ? colorScheme.onPrimary : colorScheme.onSurfaceVariant, fontSize: 9, fontWeight: FontWeight.w600, ), ), ), ), if (!_isSelectionMode) Positioned( right: 4, bottom: 4, child: ValueListenableBuilder( valueListenable: fileExistsListenable, builder: (context, fileExists, child) { return fileExists ? Semantics( button: true, label: context.l10n.a11yPlayTrackByArtist( item.trackName, item.artistName, ), child: GestureDetector( onTap: () => _playLibraryItem(item, libraryItems), child: Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: colorScheme.primary, shape: BoxShape.circle, ), child: ExcludeSemantics( child: Icon( Icons.play_arrow, color: colorScheme.onPrimary, size: 16, ), ), ), ), ) : Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: colorScheme.errorContainer, shape: BoxShape.circle, ), child: Icon( Icons.error_outline, color: colorScheme.error, size: 14, ), ); }, ), ), if (_isSelectionMode) Positioned.fill( child: Container( decoration: BoxDecoration( color: isSelected ? colorScheme.primary.withValues(alpha: 0.3) : Colors.transparent, borderRadius: BorderRadius.circular(8), ), ), ), ], ), const SizedBox(height: 6), Text( item.trackName, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of( context, ).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w500), ), ClickableArtistName( artistName: item.artistName, coverUrl: item.coverUrl, extensionId: item.historyItem?.service, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.labelSmall?.copyWith( color: colorScheme.onSurfaceVariant, ), ), ], ), if (_isSelectionMode) Positioned( right: 4, top: 4, child: Container( decoration: BoxDecoration( color: isSelected ? colorScheme.primary : colorScheme.surface, shape: BoxShape.circle, border: Border.all( color: isSelected ? colorScheme.primary : colorScheme.outline, width: 2, ), ), child: isSelected ? Icon(Icons.check, color: colorScheme.onPrimary, size: 16) : const SizedBox(width: 16, height: 16), ), ), ], ), ); } }