From 47f17b27a9f85df02d92703a91280f797797c410 Mon Sep 17 00:00:00 2001 From: biso Date: Sun, 3 May 2026 00:52:30 +0300 Subject: [PATCH 1/8] Fix: fixed the share modal --- lib/core/data/models/track_dto.dart | 5 ++++ lib/core/domain/entities/track.dart | 4 +++ .../bottom_sheets/track_options_modal.dart | 28 +++++++++---------- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/lib/core/data/models/track_dto.dart b/lib/core/data/models/track_dto.dart index c01a7ac3..e33b83d5 100644 --- a/lib/core/data/models/track_dto.dart +++ b/lib/core/data/models/track_dto.dart @@ -21,6 +21,10 @@ class TrackDto { data['artists'] as String? ?? data['user']?['display_name'] as String? ?? '', + artistUsername: + data['username'] as String? ?? + data['artist_username'] as String? ?? + data['user']?['username'] as String?, artistPfp: data['profile_picture'] as String? ?? @@ -101,6 +105,7 @@ class TrackDto { 'user_id': track.userId, 'title': track.title, 'artist': track.artist, + 'artist_username': track.artistUsername, 'artist_pfp': track.artistPfp, 'artist_city': track.artistCity, 'artist_country': track.artistCountry, diff --git a/lib/core/domain/entities/track.dart b/lib/core/domain/entities/track.dart index 806c4324..38196087 100644 --- a/lib/core/domain/entities/track.dart +++ b/lib/core/domain/entities/track.dart @@ -3,6 +3,7 @@ class Track { final String userId; final String title; final String artist; + final String? artistUsername; final String? artistPfp; final String? artistCity; final String? artistCountry; @@ -46,6 +47,7 @@ class Track { required this.userId, required this.title, required this.artist, + this.artistUsername, this.artistPfp, this.artistCity, this.artistCountry, @@ -88,6 +90,7 @@ class Track { String? userId, String? title, String? artist, + String? artistUsername, String? artistPfp, String? artistCity, String? artistCountry, @@ -126,6 +129,7 @@ class Track { userId: userId ?? this.userId, title: title ?? this.title, artist: artist ?? this.artist, + artistUsername: artistUsername ?? this.artistUsername, artistPfp: artistPfp ?? this.artistPfp, artistCity: artistCity ?? this.artistCity, artistCountry: artistCountry ?? this.artistCountry, diff --git a/lib/features/track/presentation/widgets/bottom_sheets/track_options_modal.dart b/lib/features/track/presentation/widgets/bottom_sheets/track_options_modal.dart index 9943aa60..1311f6fc 100644 --- a/lib/features/track/presentation/widgets/bottom_sheets/track_options_modal.dart +++ b/lib/features/track/presentation/widgets/bottom_sheets/track_options_modal.dart @@ -3,7 +3,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:share_plus/share_plus.dart'; import '../../../../../core/domain/entities/track.dart'; import '../../../../../core/presentation/pages/report_page.dart'; @@ -44,27 +44,24 @@ class TrackOptionsModal extends ConsumerWidget { }); // --- Share Methods --- - Future _launchUrl(String urlString) async { - final url = Uri.parse(urlString); - if (await canLaunchUrl(url)) { - await launchUrl(url, mode: LaunchMode.externalApplication); - } + + String _getAzureShareUrl(Track track) { + final username = (track.artistUsername ?? track.artist) + .replaceAll(' ', '') + .toLowerCase(); + return 'https://gray-grass-0ab138600.7.azurestaticapps.net/$username/${track.id}'; } void _shareToWhatsApp(BuildContext context) { - final text = - 'Listen Now On Rythmify: https://rythmify.com/tracks/${track.id}'; - final urlString = 'whatsapp://send?text=${Uri.encodeComponent(text)}'; - _launchUrl(urlString); + final text = 'Listen Now On Rythmify: ${_getAzureShareUrl(track)}'; Navigator.pop(context); + Share.share(text); } void _shareToSMS(BuildContext context) { - final text = - 'Listen Now On Rythmify: https://rythmify.com/tracks/${track.id}'; - final urlString = 'sms:?body=${Uri.encodeComponent(text)}'; - _launchUrl(urlString); + final text = 'Listen Now On Rythmify: ${_getAzureShareUrl(track)}'; Navigator.pop(context); + Share.share(text); } void _copyLink(BuildContext context) { @@ -213,7 +210,8 @@ class TrackOptionsModal extends ConsumerWidget { label: 'Message', onTap: () { Navigator.pop(context); - context.push('/home/inbox'); + final text = 'Listen Now On Rythmify: ${_getAzureShareUrl(track)}'; + Share.share(text); }, ), _ShareIcon( From b20031d71f8ca975e770558614e7cc56081c66f4 Mon Sep 17 00:00:00 2001 From: biso Date: Sun, 3 May 2026 01:10:52 +0300 Subject: [PATCH 2/8] Fix: some profile ui bugs --- .../presentation/pages/following_page.dart | 15 ++------------- .../presentation/widgets/info_bottom_sheet.dart | 5 ----- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/lib/features/library/presentation/pages/following_page.dart b/lib/features/library/presentation/pages/following_page.dart index 0578e1fe..0d7a9540 100644 --- a/lib/features/library/presentation/pages/following_page.dart +++ b/lib/features/library/presentation/pages/following_page.dart @@ -131,7 +131,7 @@ class _FollowingPageState extends ConsumerState { ), ), ) - : const SizedBox.shrink(); + : const SizedBox(height: 500); // bottom spacing } return _UserTile(user: state.users[index]); }, @@ -214,7 +214,7 @@ class _UserTile extends ConsumerWidget { ], ), subtitle: Text( - '${_formatCount(user.followersCount)} followers', + '@${user.username}', key: Key('following_item_${user.id}_followers_text'), style: AppTheme.labelSmall, ), @@ -233,15 +233,4 @@ class _UserTile extends ConsumerWidget { ), ); } - - /// Formats a count integer with K/M suffix abbreviations. - /// - /// - Values >= 1,000,000 are shown as `X.XM` - /// - Values >= 1,000 are shown as `X.XK` - /// - Smaller values are shown as-is - String _formatCount(int count) { - if (count >= 1000000) return '${(count / 1000000).toStringAsFixed(1)}M'; - if (count >= 1000) return '${(count / 1000).toStringAsFixed(1)}K'; - return count.toString(); - } } diff --git a/lib/features/profile/presentation/widgets/info_bottom_sheet.dart b/lib/features/profile/presentation/widgets/info_bottom_sheet.dart index 28e90f99..2c4f8b97 100644 --- a/lib/features/profile/presentation/widgets/info_bottom_sheet.dart +++ b/lib/features/profile/presentation/widgets/info_bottom_sheet.dart @@ -114,11 +114,6 @@ class InfoBottomSheet extends StatelessWidget { key: const Key('profile_info_following_stat'), ), const SizedBox(width: 24), - _statItem( - '${profile.tracksCount}', - 'Tracks', - key: const Key('profile_info_tracks_stat'), - ), ], ), From 305011edbe52fe1a245aecfe77194724b5b31c00 Mon Sep 17 00:00:00 2001 From: biso Date: Sun, 3 May 2026 01:28:34 +0300 Subject: [PATCH 3/8] Fix: fixed station playback --- .../screens/related_tracks_screen.dart | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/lib/features/playlist/presentation/screens/related_tracks_screen.dart b/lib/features/playlist/presentation/screens/related_tracks_screen.dart index ebe4b7e6..dac67922 100644 --- a/lib/features/playlist/presentation/screens/related_tracks_screen.dart +++ b/lib/features/playlist/presentation/screens/related_tracks_screen.dart @@ -44,6 +44,7 @@ import '../../../../core/theme/app_theme.dart'; import '../../../feed/presentation/providers/home_providers.dart'; import '../../../player/presentation/providers/queue_provider.dart'; import '../../../player/domain/entities/queue_state.dart'; +import '../../../track/presentation/providers/track_dependency_providers.dart'; import '../../domain/entities/playlist_track.dart'; import '../providers/playlist_provider.dart'; import '../providers/saved_content_provider.dart'; @@ -59,16 +60,17 @@ final _stationTracksProvider = FutureProvider.autoDispose }); Track _toTrack(PlaylistTrack pt) => Track( - id: pt.id, - userId: '', - title: pt.title, - artist: pt.artistName, - audioUrl: pt.id, - coverImage: pt.coverUrl, - duration: pt.duration, - playCount: pt.playCount, - createdAt: DateTime.now(), -); + id: pt.id, + userId: '', + title: pt.title, + artist: pt.artistName, + audioUrl: pt.audioUrl ?? pt.streamUrl ?? pt.id, + streamUrl: pt.streamUrl, + coverImage: pt.coverUrl, + duration: pt.duration, + playCount: pt.playCount, + createdAt: DateTime.now(), + ); class RelatedTracksScreen extends ConsumerWidget { const RelatedTracksScreen({ @@ -186,10 +188,14 @@ class _BodyState extends ConsumerState<_Body> { Future _play(List list, int index) async { if (list.isEmpty || index >= list.length) return; try { - await ref - .read(queueStateProvider.notifier) - .playQueue( - tracks: list, + // Resolve full track entities before playing to ensure valid audioUrls + // match behavior in PlaylistDetailScreen + final fullTracks = await Future.wait( + list.map((t) => ref.read(getTrackDetailsUseCaseProvider).call(t.id)), + ); + + await ref.read(queueStateProvider.notifier).playQueue( + tracks: fullTracks, initialIndex: index, context: QueueContext( type: QueueSource.station, From 2c400d3d71e3e1b4eb899da0b10cafe50f8a83b6 Mon Sep 17 00:00:00 2001 From: biso Date: Sun, 3 May 2026 02:11:44 +0300 Subject: [PATCH 4/8] Fix: fixed the infinite play --- .../presentation/pages/queue_screen.dart | 41 +++--- .../providers/player_provider.dart | 35 +++-- .../providers/queue_provider.dart | 128 ++++++++++-------- 3 files changed, 122 insertions(+), 82 deletions(-) diff --git a/lib/features/player/presentation/pages/queue_screen.dart b/lib/features/player/presentation/pages/queue_screen.dart index 3ad3ae03..8c1cfe08 100644 --- a/lib/features/player/presentation/pages/queue_screen.dart +++ b/lib/features/player/presentation/pages/queue_screen.dart @@ -158,19 +158,17 @@ class _QueueScreenState extends ConsumerState { }, itemBuilder: (context, index) { final item = manualUpcoming[index]; - return ReorderableDelayedDragStartListener( + return Material( key: ValueKey(item.queueItemId), - index: index, - child: Material( - color: Colors.transparent, - child: _QueueTile( - item: item, - onTap: () { - ref - .read(queueStateProvider.notifier) - .playFromQueue(index); - }, - ), + color: Colors.transparent, + child: _QueueTile( + item: item, + index: index, + onTap: () { + ref + .read(queueStateProvider.notifier) + .playFromQueue(index); + }, ), ); }, @@ -234,6 +232,7 @@ class _QueueTile extends StatelessWidget { final bool isHistory; final bool showDragHandle; final VoidCallback onTap; + final int? index; const _QueueTile({ required this.item, @@ -241,6 +240,7 @@ class _QueueTile extends StatelessWidget { this.isHistory = false, this.showDragHandle = true, required this.onTap, + this.index, }); @override @@ -333,10 +333,19 @@ class _QueueTile extends StatelessWidget { // --- Drag Handle on the Right --- if (!isHistory && !isActive && showDragHandle) - const Padding( - padding: EdgeInsets.only(left: 8.0), - child: Icon(Icons.drag_handle, color: Colors.grey, size: 20), - ), + if (index != null) + ReorderableDragStartListener( + index: index!, + child: const Padding( + padding: EdgeInsets.all(12.0), + child: Icon(Icons.drag_handle, color: Colors.grey, size: 22), + ), + ) + else + const Padding( + padding: EdgeInsets.only(left: 8.0), + child: Icon(Icons.drag_handle, color: Colors.grey, size: 20), + ), ], ), ), diff --git a/lib/features/player/presentation/providers/player_provider.dart b/lib/features/player/presentation/providers/player_provider.dart index 07e24153..b05d0d70 100644 --- a/lib/features/player/presentation/providers/player_provider.dart +++ b/lib/features/player/presentation/providers/player_provider.dart @@ -234,39 +234,50 @@ class PlayerNotifier extends Notifier { /// Internal helper to fetch full track metadata and waveform data. Future _updateTrackInBackground(String trackId) async { try { + final currentTrack = state.currentTrack; + if (currentTrack == null || currentTrack.id != trackId) return; + // Run both calls concurrently. - // Note: If one fails, Future.wait will throw immediately. - // We wrap individual calls to allow partial success if possible, - // or just catch the whole thing. final results = await Future.wait([ ref.read(getTrackDetailsUseCaseProvider).call(trackId).catchError((e) { debugPrint( '[PlayerNotifier] Failed to fetch track details for $trackId: $e', ); - return state.currentTrack!; // Fallback to existing track info + return currentTrack; // Fallback to existing }), ref.read(getWaveformUseCaseProvider).call(trackId).catchError((e) { debugPrint( '[PlayerNotifier] Failed to fetch waveform for $trackId: $e', ); - return []; // Fallback to empty waveform + return []; // Fallback to empty }), ]); final fullTrack = results[0] as Track; final waveform = results[1] as List; - // Only update if we actually got new useful info - if (waveform.isNotEmpty || fullTrack != state.currentTrack) { - final updatedTrack = fullTrack.copyWith( - waveformData: waveform.isNotEmpty ? waveform : fullTrack.waveformData, - ); + // CRITICAL: Merge the new data with current data to ensure no fields are lost + // (e.g. if currentTrack has a streamUrl but fullTrack doesn't, keep current) + final mergedTrack = currentTrack.copyWith( + userId: fullTrack.userId.isNotEmpty ? fullTrack.userId : currentTrack.userId, + artistUsername: fullTrack.artistUsername ?? currentTrack.artistUsername, + artistPfp: fullTrack.artistPfp ?? currentTrack.artistPfp, + waveformData: waveform.isNotEmpty ? waveform : (fullTrack.waveformData ?? currentTrack.waveformData), + isLiked: fullTrack.isLiked, + isReposted: fullTrack.isReposted, + isArtistFollowed: fullTrack.isArtistFollowed, + playCount: fullTrack.playCount > 0 ? fullTrack.playCount : currentTrack.playCount, + ); + + // Only update if we actually got new useful info (waveform is the big one) + if (mergedTrack.waveformData != currentTrack.waveformData || + mergedTrack.userId != currentTrack.userId || + mergedTrack.artistUsername != currentTrack.artistUsername) { await ref .read(updateTrackInfoUseCaseProvider) - .call(trackId, updatedTrack); + .call(trackId, mergedTrack); } } catch (e) { - // Catch-all for any other unexpected errors in the background update flow debugPrint( '[PlayerNotifier] Critical error in _updateTrackInBackground: $e', ); diff --git a/lib/features/player/presentation/providers/queue_provider.dart b/lib/features/player/presentation/providers/queue_provider.dart index 05d13102..75e38aa8 100644 --- a/lib/features/player/presentation/providers/queue_provider.dart +++ b/lib/features/player/presentation/providers/queue_provider.dart @@ -12,29 +12,27 @@ final queueStateProvider = NotifierProvider(() { }); class QueueNotifier extends Notifier { - String? _lastFetchedRelatedId; + String? _lastFetchedSeedId; @override AppQueueState build() { ref.listen(playerStateProvider, (previous, next) { - // ── Unified Reactive Logic ───────────────────────────────────────────── - // We react differently depending on WHAT changed in the player. - final oldId = previous?.currentTrack?.id; final newId = next.currentTrack?.id; - final oldIndex = previous?.queueIndex; - final newIndex = next.queueIndex; + final oldWave = previous?.currentTrack?.waveformData; + final newWave = next.currentTrack?.waveformData; // 1. TRACK CHANGED (Auto-advance or Skip) - // We sync state and check if we need to fetch more recommendations. - if (oldId != newId && newId != null) { - _syncWithHardware(newIndex ?? 0); + if (newId != null && oldId != newId) { + _syncWithHardware(next.queueIndex ?? 0); _checkAndFetchRelated(); } - // 2. INDEX CHANGED (Same track, different position in hardware queue) - // This happens during reorders or manual queue syncs. We just sync state. - else if (oldIndex != newIndex && newIndex != null) { - _syncWithHardware(newIndex); + // 2. METADATA RESOLVED (Waveform or detail update for the same track) + else if (newId != null && + oldId == newId && + oldWave == null && + newWave != null) { + _syncWithHardware(next.queueIndex ?? 0); } }); @@ -68,68 +66,90 @@ class QueueNotifier extends Notifier { ); // 3. Centralized Backend Sync: Every time the index changes, notify the server. - ref - .read(syncPlayerStateUseCaseProvider) - .call( + ref.read(syncPlayerStateUseCaseProvider).call( trackId: newCurrent.track.id, queue: newUpcoming.map((e) => e.toJson()).toList(), ); } - /// Proactively fetches discovery tracks BEFORE the manual queue ends. + /// Super simple proactive fetch: uses the LAST track in queue as seed. Future _checkAndFetchRelated() async { final upcoming = state.upcomingTracks; final current = state.currentTrack; - if (current == null) return; + if (current == null || state.isLoadingRecommendations) return; + + // 1. Identify the seed: The absolute last track we know about. + final seedItem = upcoming.isNotEmpty ? upcoming.last : current; + final seedId = seedItem.track.id; - // Threshold: Fetch when total remaining tracks are 5 or fewer. - if (upcoming.length <= 5 && - !state.isLoadingRecommendations && - _lastFetchedRelatedId != current.track.id) { - _lastFetchedRelatedId = current.track.id; + // 2. Threshold: If we have 5 or fewer tracks left in the queue. + // 3. Guard: Don't fetch for the same seed twice in a row. + if (upcoming.length <= 5 && _lastFetchedSeedId != seedId) { + _lastFetchedSeedId = seedId; state = state.copyWith(isLoadingRecommendations: true); try { - final relatedTracks = await ref - .read(getRelatedTracksUseCaseProvider) - .call(current.track.id); - - final existingIds = { - ...state.history.map((e) => e.track.id), - ...state.upcomingTracks.map((e) => e.track.id), - current.track.id, - }; - - final newRelated = relatedTracks - .where((t) => !existingIds.contains(t.id)) - .map( - (t) => QueueItem( - track: t, - queueBucket: 'context', - sourceType: 'related', - sourceId: current.track.id, - isRecommended: true, - ), - ) - .toList(); - - if (newRelated.isNotEmpty) { - // Hardware sync + final relatedTracks = + await ref.read(getRelatedTracksUseCaseProvider).call(seedId); + + // Simple duplicate filter (don't add what's already upcoming or playing) + final upcomingIds = upcoming.map((e) => e.track.id).toSet(); + final forbiddenIds = {...upcomingIds, current.track.id}; + + final filteredTracks = + relatedTracks.where((t) => !forbiddenIds.contains(t.id)).toList(); + + if (filteredTracks.isNotEmpty) { + // RESOLVE FULL METADATA AND WAVEFORMS BEFORE ADDING + // This ensures the queue is never filled with skeletons + final fullTracks = await Future.wait( + filteredTracks.map((t) async { + try { + // Fetch details and waveform concurrently + final results = await Future.wait([ + ref.read(getTrackDetailsUseCaseProvider).call(t.id), + ref + .read(getWaveformUseCaseProvider) + .call(t.id) + .catchError((_) => []), + ]); + final track = results[0] as Track; + final wave = results[1] as List; + return track.copyWith( + waveformData: wave.isNotEmpty ? wave : track.waveformData, + ); + } catch (_) { + return t; // Fallback to skeleton if resolution fails + } + }), + ); + + final newItems = fullTracks + .map( + (t) => QueueItem( + track: t, + queueBucket: 'context', + sourceType: 'related', + sourceId: seedId, + isRecommended: true, + ), + ) + .toList(); + + // Append to hardware and UI await ref .read(appendTracksUseCaseProvider) - .call(newRelated.map((e) => e.track).toList()); - - // UI sync + .call(newItems.map((e) => e.track).toList()); state = state.copyWith( - upcomingTracks: [...state.upcomingTracks, ...newRelated], + upcomingTracks: [...state.upcomingTracks, ...newItems], isLoadingRecommendations: false, ); } else { state = state.copyWith(isLoadingRecommendations: false); } } catch (e) { - debugPrint('[QueueNotifier] Proactive fetch failed: $e'); + debugPrint('[QueueNotifier] Fetch failed: $e'); state = state.copyWith(isLoadingRecommendations: false); } } @@ -146,7 +166,7 @@ class QueueNotifier extends Notifier { } final tappedTrack = tracks[initialIndex]; - _lastFetchedRelatedId = null; + _lastFetchedSeedId = null; final localItems = tracks.asMap().entries.map((entry) { final t = entry.value; From 0eeff424675916b6d5ea6e96babe0ac000ba9e4f Mon Sep 17 00:00:00 2001 From: biso Date: Sun, 3 May 2026 02:36:05 +0300 Subject: [PATCH 5/8] UI: modified some of the insights content and made the comment refresh --- .../presentation/pages/comments_screen.dart | 7 ++--- .../presentation/pages/insights_page.dart | 27 ++++++++++++------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/lib/features/comments/presentation/pages/comments_screen.dart b/lib/features/comments/presentation/pages/comments_screen.dart index d4f832ac..321eec1f 100644 --- a/lib/features/comments/presentation/pages/comments_screen.dart +++ b/lib/features/comments/presentation/pages/comments_screen.dart @@ -52,9 +52,10 @@ class _CommentsScreenState extends ConsumerState { }); WidgetsBinding.instance.addPostFrameCallback((_) { - ref - .read(trackCommentsProvider(widget.track.id).notifier) - .setInitialTrack(widget.track); + final notifier = ref.read(trackCommentsProvider(widget.track.id).notifier); + notifier.setInitialTrack(widget.track); + // Force a fresh fetch from the server every time the screen opens + notifier.fetchComments(refresh: true); }); } diff --git a/lib/features/library/presentation/pages/insights_page.dart b/lib/features/library/presentation/pages/insights_page.dart index ca00074b..427893ee 100644 --- a/lib/features/library/presentation/pages/insights_page.dart +++ b/lib/features/library/presentation/pages/insights_page.dart @@ -228,8 +228,9 @@ class _SoundCloudDataView extends StatelessWidget { @override Widget build(BuildContext context) { final totalPlays = insights.fold(0, (s, i) => s + i.totalPlays); - final totalListeners = insights.fold(0, (s, i) => s + i.uniqueListeners); final totalLikes = insights.fold(0, (s, i) => s + i.likes); + final totalReposts = insights.fold(0, (s, i) => s + i.reposts); + final totalComments = insights.fold(0, (s, i) => s + i.comments); return ListView( key: const Key('insights_sc_data_list_view'), @@ -258,10 +259,10 @@ class _SoundCloudDataView extends StatelessWidget { ), Expanded( child: _StatItem( - icon: Icons.people_outline, - label: 'Listeners', - value: _fmt(totalListeners), - key: const Key('insights_total_listeners_stat'), + icon: Icons.repeat, + label: 'Reposts', + value: _fmt(totalReposts), + key: const Key('insights_total_reposts_stat'), ), ), Expanded( @@ -272,6 +273,14 @@ class _SoundCloudDataView extends StatelessWidget { key: const Key('insights_total_likes_stat'), ), ), + Expanded( + child: _StatItem( + icon: Icons.comment, + label: 'Comments', + value: _fmt(totalComments), + key: const Key('insights_total_comments_stat'), + ), + ), ], ), ), @@ -454,10 +463,10 @@ class _InsightTrackCard extends StatelessWidget { ), Expanded( child: _MiniStat( - icon: Icons.people_outline, - value: _fmt(insight.uniqueListeners), - label: 'listeners', - key: Key('insights_track_${insight.trackId}_listeners_stat'), + icon: Icons.comment, + value: _fmt(insight.comments), + label: 'comments', + key: Key('insights_track_${insight.trackId}_comments_stat'), ), ), Expanded( From 3de40cad24528f440c495bf50a4b6d1934047178 Mon Sep 17 00:00:00 2001 From: biso Date: Sun, 3 May 2026 02:47:49 +0300 Subject: [PATCH 6/8] Feat: implemented delete from downloads feature --- .../presentation/pages/downloads_page.dart | 46 +++++++++++++++++-- .../presentation/widgets/track_card.dart | 11 ++++- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/lib/features/library/presentation/pages/downloads_page.dart b/lib/features/library/presentation/pages/downloads_page.dart index 2fd608ba..703644aa 100644 --- a/lib/features/library/presentation/pages/downloads_page.dart +++ b/lib/features/library/presentation/pages/downloads_page.dart @@ -110,15 +110,41 @@ class _DownloadsPageState extends ConsumerState { void playAt(int index) { if (filteredTracks.isEmpty) return; - ref - .read(queueStateProvider.notifier) - .playQueue( + ref.read(queueStateProvider.notifier).playQueue( tracks: filteredTracks, initialIndex: index, context: const QueueContext(type: QueueSource.downloads), ); } + Future showDeleteConfirmation(Track track) async { + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: AppTheme.surface, + title: const Text('Delete Download'), + content: Text( + 'Do you really want to delete "${track.title}" from your downloads? It cannot be restored.', + style: const TextStyle(color: Colors.white70), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Cancel', style: TextStyle(color: Colors.white54)), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('Delete', style: TextStyle(color: Colors.redAccent)), + ), + ], + ), + ); + + if (confirmed == true) { + await ref.read(downloadsProvider.notifier).removeTrack(track.id); + } + } + return CustomScrollView( key: const Key('downloads_scroll_view'), controller: _scrollController, @@ -214,12 +240,22 @@ class _DownloadsPageState extends ConsumerState { sliver: SliverList.builder( itemCount: filtered.length, itemBuilder: (context, index) { + final track = filteredTracks[index]; return TrackCard( - key: Key('downloads_item_${filteredTracks[index].id}_$index'), - track: filteredTracks[index], + key: Key('downloads_item_${track.id}_$index'), + track: track, onTap: () => playAt(index), showOptions: false, showStats: false, + trailing: IconButton( + key: Key('downloads_delete_button_${track.id}'), + icon: const Icon( + Icons.delete_outline_rounded, + color: Colors.white54, + size: 22, + ), + onPressed: () => showDeleteConfirmation(track), + ), ); }, ), diff --git a/lib/features/track/presentation/widgets/track_card.dart b/lib/features/track/presentation/widgets/track_card.dart index 7cb939bf..fb15cf25 100644 --- a/lib/features/track/presentation/widgets/track_card.dart +++ b/lib/features/track/presentation/widgets/track_card.dart @@ -36,6 +36,10 @@ class TrackCard extends ConsumerWidget { /// Whether to show the bottom stats row (play count, duration, etc.). final bool showStats; + /// Optional widget to display on the right side (e.g. a delete bin). + /// If provided, this replaces the default more options icon. + final Widget? trailing; + /// Creates a [TrackCard]. const TrackCard({ super.key, @@ -45,6 +49,7 @@ class TrackCard extends ConsumerWidget { this.observePlayerState = true, this.showOptions = true, this.showStats = true, + this.trailing, }); // --- Formatting Helpers --- @@ -144,8 +149,10 @@ class TrackCard extends ConsumerWidget { ), ), - // 4. Trailing More Icon - if (showOptions) + // 4. Trailing Action + if (trailing != null) + trailing! + else if (showOptions) InkWell( key: Key('track_card_${track.id}_more_inkwell'), onTap: () { From 367344d15d7cdc11b00cb97d4e8d8f558fb3c000 Mon Sep 17 00:00:00 2001 From: biso Date: Sun, 3 May 2026 03:32:42 +0300 Subject: [PATCH 7/8] Fix: made the report semi work (works for tracks copyright only now) --- lib/core/data/models/report_request.dart | 27 ++-- lib/core/presentation/pages/report_page.dart | 123 ++++++++++++++---- .../widgets/comment_action_bottom_sheet.dart | 11 +- .../widgets/pop_up_menu_widget.dart | 6 +- .../bottom_sheets/track_options_modal.dart | 6 +- 5 files changed, 121 insertions(+), 52 deletions(-) diff --git a/lib/core/data/models/report_request.dart b/lib/core/data/models/report_request.dart index 496f4743..79f98413 100644 --- a/lib/core/data/models/report_request.dart +++ b/lib/core/data/models/report_request.dart @@ -1,31 +1,22 @@ class ReportRequest { - final String contentId; + final String resourceId; + final String resourceType; final String reason; - final String details; - final String name; - final String email; - final String url; - final List violations; + final String description; ReportRequest({ - required this.contentId, + required this.resourceId, + this.resourceType = 'track', required this.reason, - required this.details, - required this.name, - required this.email, - required this.url, - required this.violations, + required this.description, }); Map toJson() { return { - "contentId": contentId, + "resource_id": resourceId, + "resource_type": resourceType, "reason": reason, - "details": details, - "name": name, - "email": email, - "url": url, - "violations": violations, + "description": description, }; } } diff --git a/lib/core/presentation/pages/report_page.dart b/lib/core/presentation/pages/report_page.dart index 7d542217..dc560848 100644 --- a/lib/core/presentation/pages/report_page.dart +++ b/lib/core/presentation/pages/report_page.dart @@ -1,14 +1,19 @@ import 'package:flutter/material.dart'; -import 'package:dio/dio.dart'; import '../../theme/app_theme.dart'; import '../../data/models/report_request.dart'; import '../../data/models/report_repository.dart'; +import '../../network/api_client.dart'; class ReportPage extends StatefulWidget { final String reportedContentId; + final String resourceType; - const ReportPage({super.key, required this.reportedContentId}); + const ReportPage({ + super.key, + required this.reportedContentId, + this.resourceType = 'track', + }); @override State createState() => _ReportPageState(); @@ -27,6 +32,8 @@ class _ReportPageState extends State { text: 'basseialaa33@gmail.com', ); final _urlController = TextEditingController(); + final _resourceTypeController = TextEditingController(); + final _resourceIdController = TextEditingController(); late final ReportRepository _repository; @@ -44,6 +51,20 @@ class _ReportPageState extends State { "I just don't like it", ]; + final Map _reasonMapping = { + "It's hate speech": "hate_speech", + "It's harassing or abusive content": "harassment", + "It contains sexual content or nudity": "sexual_content", + "It contains graphic violence": "violence", + "It promotes self-harm": "self_harm", + "It's infringement of intellectual property": "copyright", + "It's spam or misleading content": "spam", + "It's private or confidential information": "privacy", + "It's selling illegal goods": "illegal_goods", + "Something else": "other", + "I just don't like it": "dislike", + }; + final Map _violations = { 'Audio': false, 'Title/Description': false, @@ -57,11 +78,17 @@ class _ReportPageState extends State { void initState() { super.initState(); - _repository = ReportRepository(Dio()); + _repository = ReportRepository(apiClient.dio); + + _resourceIdController.text = widget.reportedContentId; + _resourceTypeController.text = widget.resourceType; - // ✅ Auto-generate URL - _urlController.text = - 'https://rythmify.com/tracks/${widget.reportedContentId}'; + // Adjust URL generation based on type + if (widget.resourceType == 'track') { + _urlController.text = 'https://rythmify.com/tracks/${widget.reportedContentId}'; + } else if (widget.resourceType == 'user') { + _urlController.text = 'https://rythmify.com/profile/${widget.reportedContentId}'; + } } @override @@ -70,29 +97,35 @@ class _ReportPageState extends State { _nameController.dispose(); _emailController.dispose(); _urlController.dispose(); + _resourceIdController.dispose(); + _resourceTypeController.dispose(); super.dispose(); } Future _submitReport() async { - if (!_formKey.currentState!.validate() || - _selectedReason == null || - !_isConsentChecked) { + if (!_formKey.currentState!.validate() || !_isConsentChecked) { return; } - final selectedViolations = _violations.entries - .where((e) => e.value) - .map((e) => e.key) - .toList(); + // If it's a track and no reason is selected, default to 'copyright' + String reasonSlug; + if (_selectedReason != null) { + reasonSlug = _reasonMapping[_selectedReason!] ?? 'other'; + } else if (widget.resourceType == 'track') { + reasonSlug = 'copyright'; + } else { + // For other types (like users), we still require a reason + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Please select a reason for reporting')), + ); + return; + } final request = ReportRequest( - contentId: widget.reportedContentId, - reason: _selectedReason!, - details: _detailsController.text, - name: _nameController.text, - email: _emailController.text, - url: _urlController.text, - violations: selectedViolations, + resourceId: widget.reportedContentId, + resourceType: widget.resourceType, + reason: reasonSlug, + description: _detailsController.text, ); setState(() => _isLoading = true); @@ -121,8 +154,11 @@ class _ReportPageState extends State { @override Widget build(BuildContext context) { - final bool isFormValid = - _selectedReason != null && _isConsentChecked && !_isLoading; + // For tracks, reason is optional (defaults to copyright) + // For others, reason must be selected + final bool isReasonValid = + widget.resourceType == 'track' || _selectedReason != null; + final bool isFormValid = isReasonValid && _isConsentChecked && !_isLoading; return Scaffold( backgroundColor: AppTheme.background, @@ -144,15 +180,50 @@ class _ReportPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + /// RESOURCE PREVIEW + Text( + 'Report Details', + style: AppTheme.titleMedium.copyWith(color: AppTheme.babyBlue), + ), + const SizedBox(height: 12), + TextFormField( + controller: _resourceTypeController, + readOnly: true, + decoration: InputDecoration( + labelText: 'Resource Type', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + filled: true, + fillColor: Colors.white10, + ), + ), + const SizedBox(height: 16), + TextFormField( + controller: _resourceIdController, + readOnly: true, + decoration: InputDecoration( + labelText: 'Resource ID', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + filled: true, + fillColor: Colors.white10, + ), + ), + + const SizedBox(height: 32), + /// TITLE Text( - 'Reason for Reporting', + widget.resourceType == 'track' + ? 'Reason for Reporting (Default: Copyright)' + : 'Reason for Reporting', style: AppTheme.titleMedium.copyWith(color: AppTheme.babyBlue), ), const SizedBox(height: 12), - /// ✅ FIXED RADIO LIST (NO NEGATIVE SPACING) Column( children: _reasons.map((reason) { return RadioListTile( @@ -165,7 +236,7 @@ class _ReportPageState extends State { setState(() => _selectedReason = value); }, contentPadding: EdgeInsets.zero, - dense: true, // ✅ cleaner spacing + dense: true, activeColor: AppTheme.primaryBrand, ); }).toList(), @@ -331,4 +402,4 @@ class _ReportPageState extends State { ), ); } -} +} \ No newline at end of file diff --git a/lib/features/comments/presentation/widgets/comment_action_bottom_sheet.dart b/lib/features/comments/presentation/widgets/comment_action_bottom_sheet.dart index 806950fc..797e24e1 100644 --- a/lib/features/comments/presentation/widgets/comment_action_bottom_sheet.dart +++ b/lib/features/comments/presentation/widgets/comment_action_bottom_sheet.dart @@ -68,11 +68,14 @@ class CommentActionBottomSheet extends ConsumerWidget { } } - void _reportUser(BuildContext context) { + void _reportComment(BuildContext context) { Navigator.pop(context); Navigator.of(context, rootNavigator: true).push( MaterialPageRoute( - builder: (context) => ReportPage(reportedContentId: comment.id), + builder: (context) => ReportPage( + reportedContentId: comment.userId, + resourceType: 'user', + ), ), ); } @@ -159,8 +162,8 @@ class CommentActionBottomSheet extends ConsumerWidget { _buildActionRow( key: const Key('comment_action_report_inkwell'), icon: Icons.flag_outlined, - label: 'Report user', - onTap: () => _reportUser(context), + label: 'Report comment', + onTap: () => _reportComment(context), ), // Row 6 (Other): Block/Unblock diff --git a/lib/features/messaging/presentation/widgets/pop_up_menu_widget.dart b/lib/features/messaging/presentation/widgets/pop_up_menu_widget.dart index c7999fa8..4969dc03 100644 --- a/lib/features/messaging/presentation/widgets/pop_up_menu_widget.dart +++ b/lib/features/messaging/presentation/widgets/pop_up_menu_widget.dart @@ -94,8 +94,10 @@ class PopUpMenuWidget extends ConsumerWidget { Navigator.pop(context); Navigator.of(parentContext, rootNavigator: true).push( MaterialPageRoute( - builder: (_) => - ReportPage(reportedContentId: participantId), + builder: (_) => ReportPage( + reportedContentId: participantId, + resourceType: 'user', + ), ), ); }, diff --git a/lib/features/track/presentation/widgets/bottom_sheets/track_options_modal.dart b/lib/features/track/presentation/widgets/bottom_sheets/track_options_modal.dart index 1311f6fc..2244b76f 100644 --- a/lib/features/track/presentation/widgets/bottom_sheets/track_options_modal.dart +++ b/lib/features/track/presentation/widgets/bottom_sheets/track_options_modal.dart @@ -464,8 +464,10 @@ class TrackOptionsModal extends ConsumerWidget { Navigator.pop(context); Navigator.of(context, rootNavigator: true).push( MaterialPageRoute( - builder: (context) => - ReportPage(reportedContentId: syncedTrack.id), + builder: (context) => ReportPage( + reportedContentId: syncedTrack.id, + resourceType: 'track', + ), ), ); }, From 046f07d8378f366ae0d79c65ef09ca1c859fce04 Mon Sep 17 00:00:00 2001 From: biso Date: Sun, 3 May 2026 03:42:33 +0300 Subject: [PATCH 8/8] Semi Final commit for this night --- lib/core/presentation/pages/report_page.dart | 79 +++++++------------ .../presentation/pages/comments_screen.dart | 4 +- .../widgets/comment_action_bottom_sheet.dart | 6 +- .../presentation/pages/downloads_page.dart | 14 +++- .../presentation/pages/queue_screen.dart | 6 +- .../providers/player_provider.dart | 12 ++- .../providers/queue_provider.dart | 14 ++-- .../screens/related_tracks_screen.dart | 26 +++--- .../bottom_sheets/track_options_modal.dart | 3 +- 9 files changed, 83 insertions(+), 81 deletions(-) diff --git a/lib/core/presentation/pages/report_page.dart b/lib/core/presentation/pages/report_page.dart index dc560848..d85e4eac 100644 --- a/lib/core/presentation/pages/report_page.dart +++ b/lib/core/presentation/pages/report_page.dart @@ -28,9 +28,7 @@ class _ReportPageState extends State { final _detailsController = TextEditingController(); final _nameController = TextEditingController(); - final _emailController = TextEditingController( - text: 'basseialaa33@gmail.com', - ); + final _emailController = TextEditingController(text: ''); final _urlController = TextEditingController(); final _resourceTypeController = TextEditingController(); final _resourceIdController = TextEditingController(); @@ -38,31 +36,17 @@ class _ReportPageState extends State { late final ReportRepository _repository; final List _reasons = [ - "It's hate speech", - "It's harassing or abusive content", - "It contains sexual content or nudity", - "It contains graphic violence", - "It promotes self-harm", - "It's infringement of intellectual property", - "It's spam or misleading content", - "It's private or confidential information", - "It's selling illegal goods", - "Something else", - "I just don't like it", + "Copyright", + "Inappropriate content", + "Spam", + "Impersonation", ]; final Map _reasonMapping = { - "It's hate speech": "hate_speech", - "It's harassing or abusive content": "harassment", - "It contains sexual content or nudity": "sexual_content", - "It contains graphic violence": "violence", - "It promotes self-harm": "self_harm", - "It's infringement of intellectual property": "copyright", - "It's spam or misleading content": "spam", - "It's private or confidential information": "privacy", - "It's selling illegal goods": "illegal_goods", - "Something else": "other", - "I just don't like it": "dislike", + "Copyright": "copyright", + "Inappropriate content": "inappropriate", + "Spam": "spam", + "Impersonation": "impersonation", }; final Map _violations = { @@ -85,9 +69,11 @@ class _ReportPageState extends State { // Adjust URL generation based on type if (widget.resourceType == 'track') { - _urlController.text = 'https://rythmify.com/tracks/${widget.reportedContentId}'; + _urlController.text = + 'https://rythmify.com/tracks/${widget.reportedContentId}'; } else if (widget.resourceType == 'user') { - _urlController.text = 'https://rythmify.com/profile/${widget.reportedContentId}'; + _urlController.text = + 'https://rythmify.com/profile/${widget.reportedContentId}'; } } @@ -103,23 +89,13 @@ class _ReportPageState extends State { } Future _submitReport() async { - if (!_formKey.currentState!.validate() || !_isConsentChecked) { + if (!_formKey.currentState!.validate() || + _selectedReason == null || + !_isConsentChecked) { return; } - // If it's a track and no reason is selected, default to 'copyright' - String reasonSlug; - if (_selectedReason != null) { - reasonSlug = _reasonMapping[_selectedReason!] ?? 'other'; - } else if (widget.resourceType == 'track') { - reasonSlug = 'copyright'; - } else { - // For other types (like users), we still require a reason - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Please select a reason for reporting')), - ); - return; - } + final reasonSlug = _reasonMapping[_selectedReason!] ?? 'inappropriate'; final request = ReportRequest( resourceId: widget.reportedContentId, @@ -154,11 +130,14 @@ class _ReportPageState extends State { @override Widget build(BuildContext context) { - // For tracks, reason is optional (defaults to copyright) - // For others, reason must be selected - final bool isReasonValid = - widget.resourceType == 'track' || _selectedReason != null; - final bool isFormValid = isReasonValid && _isConsentChecked && !_isLoading; + // Determine which reasons to show based on resource type + final filteredReasons = _reasons.where((r) { + if (widget.resourceType == 'user' && r == 'Copyright') return false; + return true; + }).toList(); + + final bool isFormValid = + _selectedReason != null && _isConsentChecked && !_isLoading; return Scaffold( backgroundColor: AppTheme.background, @@ -216,16 +195,14 @@ class _ReportPageState extends State { /// TITLE Text( - widget.resourceType == 'track' - ? 'Reason for Reporting (Default: Copyright)' - : 'Reason for Reporting', + 'Reason for Reporting', style: AppTheme.titleMedium.copyWith(color: AppTheme.babyBlue), ), const SizedBox(height: 12), Column( - children: _reasons.map((reason) { + children: filteredReasons.map((reason) { return RadioListTile( title: Text(reason, style: AppTheme.bodyNormal), value: reason, @@ -402,4 +379,4 @@ class _ReportPageState extends State { ), ); } -} \ No newline at end of file +} diff --git a/lib/features/comments/presentation/pages/comments_screen.dart b/lib/features/comments/presentation/pages/comments_screen.dart index 321eec1f..f1c6c6a4 100644 --- a/lib/features/comments/presentation/pages/comments_screen.dart +++ b/lib/features/comments/presentation/pages/comments_screen.dart @@ -52,7 +52,9 @@ class _CommentsScreenState extends ConsumerState { }); WidgetsBinding.instance.addPostFrameCallback((_) { - final notifier = ref.read(trackCommentsProvider(widget.track.id).notifier); + final notifier = ref.read( + trackCommentsProvider(widget.track.id).notifier, + ); notifier.setInitialTrack(widget.track); // Force a fresh fetch from the server every time the screen opens notifier.fetchComments(refresh: true); diff --git a/lib/features/comments/presentation/widgets/comment_action_bottom_sheet.dart b/lib/features/comments/presentation/widgets/comment_action_bottom_sheet.dart index 797e24e1..0327f67a 100644 --- a/lib/features/comments/presentation/widgets/comment_action_bottom_sheet.dart +++ b/lib/features/comments/presentation/widgets/comment_action_bottom_sheet.dart @@ -72,10 +72,8 @@ class CommentActionBottomSheet extends ConsumerWidget { Navigator.pop(context); Navigator.of(context, rootNavigator: true).push( MaterialPageRoute( - builder: (context) => ReportPage( - reportedContentId: comment.userId, - resourceType: 'user', - ), + builder: (context) => + ReportPage(reportedContentId: comment.userId, resourceType: 'user'), ), ); } diff --git a/lib/features/library/presentation/pages/downloads_page.dart b/lib/features/library/presentation/pages/downloads_page.dart index 703644aa..41795fc3 100644 --- a/lib/features/library/presentation/pages/downloads_page.dart +++ b/lib/features/library/presentation/pages/downloads_page.dart @@ -110,7 +110,9 @@ class _DownloadsPageState extends ConsumerState { void playAt(int index) { if (filteredTracks.isEmpty) return; - ref.read(queueStateProvider.notifier).playQueue( + ref + .read(queueStateProvider.notifier) + .playQueue( tracks: filteredTracks, initialIndex: index, context: const QueueContext(type: QueueSource.downloads), @@ -130,11 +132,17 @@ class _DownloadsPageState extends ConsumerState { actions: [ TextButton( onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel', style: TextStyle(color: Colors.white54)), + child: const Text( + 'Cancel', + style: TextStyle(color: Colors.white54), + ), ), TextButton( onPressed: () => Navigator.pop(context, true), - child: const Text('Delete', style: TextStyle(color: Colors.redAccent)), + child: const Text( + 'Delete', + style: TextStyle(color: Colors.redAccent), + ), ), ], ), diff --git a/lib/features/player/presentation/pages/queue_screen.dart b/lib/features/player/presentation/pages/queue_screen.dart index 8c1cfe08..707ee78a 100644 --- a/lib/features/player/presentation/pages/queue_screen.dart +++ b/lib/features/player/presentation/pages/queue_screen.dart @@ -338,7 +338,11 @@ class _QueueTile extends StatelessWidget { index: index!, child: const Padding( padding: EdgeInsets.all(12.0), - child: Icon(Icons.drag_handle, color: Colors.grey, size: 22), + child: Icon( + Icons.drag_handle, + color: Colors.grey, + size: 22, + ), ), ) else diff --git a/lib/features/player/presentation/providers/player_provider.dart b/lib/features/player/presentation/providers/player_provider.dart index b05d0d70..444c75d4 100644 --- a/lib/features/player/presentation/providers/player_provider.dart +++ b/lib/features/player/presentation/providers/player_provider.dart @@ -259,14 +259,20 @@ class PlayerNotifier extends Notifier { // CRITICAL: Merge the new data with current data to ensure no fields are lost // (e.g. if currentTrack has a streamUrl but fullTrack doesn't, keep current) final mergedTrack = currentTrack.copyWith( - userId: fullTrack.userId.isNotEmpty ? fullTrack.userId : currentTrack.userId, + userId: fullTrack.userId.isNotEmpty + ? fullTrack.userId + : currentTrack.userId, artistUsername: fullTrack.artistUsername ?? currentTrack.artistUsername, artistPfp: fullTrack.artistPfp ?? currentTrack.artistPfp, - waveformData: waveform.isNotEmpty ? waveform : (fullTrack.waveformData ?? currentTrack.waveformData), + waveformData: waveform.isNotEmpty + ? waveform + : (fullTrack.waveformData ?? currentTrack.waveformData), isLiked: fullTrack.isLiked, isReposted: fullTrack.isReposted, isArtistFollowed: fullTrack.isArtistFollowed, - playCount: fullTrack.playCount > 0 ? fullTrack.playCount : currentTrack.playCount, + playCount: fullTrack.playCount > 0 + ? fullTrack.playCount + : currentTrack.playCount, ); // Only update if we actually got new useful info (waveform is the big one) diff --git a/lib/features/player/presentation/providers/queue_provider.dart b/lib/features/player/presentation/providers/queue_provider.dart index 75e38aa8..e54287b4 100644 --- a/lib/features/player/presentation/providers/queue_provider.dart +++ b/lib/features/player/presentation/providers/queue_provider.dart @@ -66,7 +66,9 @@ class QueueNotifier extends Notifier { ); // 3. Centralized Backend Sync: Every time the index changes, notify the server. - ref.read(syncPlayerStateUseCaseProvider).call( + ref + .read(syncPlayerStateUseCaseProvider) + .call( trackId: newCurrent.track.id, queue: newUpcoming.map((e) => e.toJson()).toList(), ); @@ -90,15 +92,17 @@ class QueueNotifier extends Notifier { state = state.copyWith(isLoadingRecommendations: true); try { - final relatedTracks = - await ref.read(getRelatedTracksUseCaseProvider).call(seedId); + final relatedTracks = await ref + .read(getRelatedTracksUseCaseProvider) + .call(seedId); // Simple duplicate filter (don't add what's already upcoming or playing) final upcomingIds = upcoming.map((e) => e.track.id).toSet(); final forbiddenIds = {...upcomingIds, current.track.id}; - final filteredTracks = - relatedTracks.where((t) => !forbiddenIds.contains(t.id)).toList(); + final filteredTracks = relatedTracks + .where((t) => !forbiddenIds.contains(t.id)) + .toList(); if (filteredTracks.isNotEmpty) { // RESOLVE FULL METADATA AND WAVEFORMS BEFORE ADDING diff --git a/lib/features/playlist/presentation/screens/related_tracks_screen.dart b/lib/features/playlist/presentation/screens/related_tracks_screen.dart index dac67922..c18a8ab0 100644 --- a/lib/features/playlist/presentation/screens/related_tracks_screen.dart +++ b/lib/features/playlist/presentation/screens/related_tracks_screen.dart @@ -60,17 +60,17 @@ final _stationTracksProvider = FutureProvider.autoDispose }); Track _toTrack(PlaylistTrack pt) => Track( - id: pt.id, - userId: '', - title: pt.title, - artist: pt.artistName, - audioUrl: pt.audioUrl ?? pt.streamUrl ?? pt.id, - streamUrl: pt.streamUrl, - coverImage: pt.coverUrl, - duration: pt.duration, - playCount: pt.playCount, - createdAt: DateTime.now(), - ); + id: pt.id, + userId: '', + title: pt.title, + artist: pt.artistName, + audioUrl: pt.audioUrl ?? pt.streamUrl ?? pt.id, + streamUrl: pt.streamUrl, + coverImage: pt.coverUrl, + duration: pt.duration, + playCount: pt.playCount, + createdAt: DateTime.now(), +); class RelatedTracksScreen extends ConsumerWidget { const RelatedTracksScreen({ @@ -194,7 +194,9 @@ class _BodyState extends ConsumerState<_Body> { list.map((t) => ref.read(getTrackDetailsUseCaseProvider).call(t.id)), ); - await ref.read(queueStateProvider.notifier).playQueue( + await ref + .read(queueStateProvider.notifier) + .playQueue( tracks: fullTracks, initialIndex: index, context: QueueContext( diff --git a/lib/features/track/presentation/widgets/bottom_sheets/track_options_modal.dart b/lib/features/track/presentation/widgets/bottom_sheets/track_options_modal.dart index 2244b76f..1912edfa 100644 --- a/lib/features/track/presentation/widgets/bottom_sheets/track_options_modal.dart +++ b/lib/features/track/presentation/widgets/bottom_sheets/track_options_modal.dart @@ -210,7 +210,8 @@ class TrackOptionsModal extends ConsumerWidget { label: 'Message', onTap: () { Navigator.pop(context); - final text = 'Listen Now On Rythmify: ${_getAzureShareUrl(track)}'; + final text = + 'Listen Now On Rythmify: ${_getAzureShareUrl(track)}'; Share.share(text); }, ),