Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ class _MusicPlayerAppState extends State<MusicPlayerApp> {
getIt(),
getIt(),
getIt(),
getIt(),
),
),
BlocProvider(
Expand Down
1 change: 1 addition & 0 deletions lib/core/constants/preferences_keys.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ sealed class PreferencesKeys {
static const String pinnedPlaylists = 'pinned_playlists';
static const String playlistCoverSongsId = 'playlist_cover_songs_id';
static const String songSortType = 'song_sort_type';
static const String recentlyPlayedSongIds = 'recently_played_song_ids';
}
2 changes: 1 addition & 1 deletion lib/core/widgets/song_image_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class ArtImageWidget extends StatelessWidget {
const ArtImageWidget({
required this.id,
this.type = ArtworkType.AUDIO,
this.size = 50,
this.size = 54,
this.quality = 70,
this.qualitySize = 200,
this.defaultCoverBg,
Expand Down
2 changes: 1 addition & 1 deletion lib/core/widgets/song_item.dart
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ class SongItem extends StatelessWidget {
Widget _buildTitleAndArtist(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 4,
children: [
Text(
track.title,
Expand All @@ -119,7 +120,6 @@ class SongItem extends StatelessWidget {
color: isCurrentTrack ? context.theme.colorScheme.primary : null,
),
),
const SizedBox(height: 4),
ArtistWidget(
artist: track.artist,
isCurrentTrack: isCurrentTrack,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import 'package:music_player/core/constants/preferences_keys.dart';
import 'package:music_player/core/services/services.dart';
import 'package:music_player/features/music_plyer/data/mapppers/mappers.dart';
import 'package:music_player/features/music_plyer/domain/entities/entities.dart';
import 'package:on_audio_query_pluse/on_audio_query.dart';
import 'package:shared_preferences/shared_preferences.dart';

/// Datasource interface for audio playback operations.
///
Expand Down Expand Up @@ -43,6 +45,12 @@ abstract interface class AudioHandlerDatasource {

/// Sets the loop/repeat mode.
PlayerLoopMode setPlayerLoopMode(PlayerLoopMode loopMode);

/// Adds a song to the recently played list.
/// Parameters:
/// - [songId]: ID of the song to add
/// Returns a [bool] indicating success or failure.
Future<bool> addToRecentlyPlayed(int songId);
}

/// Implementation of [AudioHandlerDatasource] using MAudioHandler.
Expand All @@ -53,9 +61,12 @@ class AudioHandlerDatasourceImpl implements AudioHandlerDatasource {
/// Creates an [AudioHandlerDatasourceImpl].
AudioHandlerDatasourceImpl({
required MAudioHandler audioHandler,
}) : _audioHandler = audioHandler;
required SharedPreferences preferences,
}) : _audioHandler = audioHandler,
_preferences = preferences;

final MAudioHandler _audioHandler;
final SharedPreferences _preferences;

@override
Future<void> pause() {
Expand Down Expand Up @@ -122,4 +133,28 @@ class AudioHandlerDatasourceImpl implements AudioHandlerDatasource {
_audioHandler.setLoopMode(PlayerLoopModeMapper.mapToLoopMode(loopMode));
return loopMode;
}

@override
Future<bool> addToRecentlyPlayed(int songId) {
final recentList =
_preferences.getStringList(PreferencesKeys.recentlyPlayedSongIds) ?? [];

final songIdStr = songId.toString();

// Remove if already exists to avoid duplicates
recentList
..remove(songIdStr)
// Add to the start of the list
..insert(0, songIdStr);

// Keep only the latest 50 entries
if (recentList.length > 50) {
recentList.removeRange(50, recentList.length);
}

return _preferences.setStringList(
PreferencesKeys.recentlyPlayedSongIds,
recentList,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,14 @@ class MusicPlayerRepoImpl implements MusicPlayerRepository {
return Result.failure('failed to set loop mode: $e');
}
}

@override
Future<Result<bool>> addToRecentlyPlayed(int songId) async {
try {
final result = await _audioHandlerDatasource.addToRecentlyPlayed(songId);
return Result.success(result);
} on Exception catch (e) {
return Result.failure('failed to add to recently played: $e');
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,11 @@ abstract class MusicPlayerRepository {
///
/// Returns a [Result] containing the set loop mode or an error.
Result<PlayerLoopMode> setLoopMode(PlayerLoopMode loopMode);

/// Adds a song to the recently played list.
///
/// Parameters:
/// - [songId]: ID of the song to add
/// Returns a [Result] indicating success or failure.
Future<Result<bool>> addToRecentlyPlayed(int songId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'package:music_player/core/result.dart';
import 'package:music_player/features/music_plyer/domain/domain.dart';

class AddToRecentlyPlayed {
/// Creates an [AddToRecentlyPlayed] use case.
const AddToRecentlyPlayed(this._repository);

final MusicPlayerRepository _repository;

/// Adds a song to the recently played list.
////
/// Parameters:
/// - [songId]: ID of the song to add
/// Returns a [Result] indicating success or failure.
Future<Result<bool>> call(int songId) {
return _repository.addToRecentlyPlayed(songId);
}
}
1 change: 1 addition & 0 deletions lib/features/music_plyer/domain/usecases/usecases.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export 'add_to_recently_played.dart';
export 'has_next_song.dart';
export 'has_previous_song.dart';
export 'pause_song.dart';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class MusicPlayerBloc extends Bloc<MusicPlayerEvent, MusicPlayerState> {
this.watchSongDuration,
this.watchSongPosition,
this.setLoopMode,
this.addToRecentlyPlayed,
) : super(const MusicPlayerState()) {
// Listen to player index changes
_playerIndexSubscription = watchPlayerIndex().distinct().listen(
Expand Down Expand Up @@ -59,6 +60,7 @@ class MusicPlayerBloc extends Bloc<MusicPlayerEvent, MusicPlayerState> {
final WatchPlayerIndex watchPlayerIndex;
final WatchSongDuration watchSongDuration;
final WatchSongPosition watchSongPosition;
final AddToRecentlyPlayed addToRecentlyPlayed;

late final StreamSubscription<int?> _playerIndexSubscription;

Expand Down Expand Up @@ -230,6 +232,7 @@ class MusicPlayerBloc extends Bloc<MusicPlayerEvent, MusicPlayerState> {

// Start playback
final result = await playSong(event.playList, event.index);
await addToRecentlyPlayed(event.playList[event.index].id);

if (result.isFailure) {
emit(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,6 @@ class _MiniPlayerPageState extends State<MiniPlayerPage>
tag: 'song_cover_${state.currentSong?.id ?? 0}',
child: ArtImageWidget(
id: state.currentSong?.id ?? 0,
size: 54,
),
),
title: SongTitle(
Expand Down
31 changes: 31 additions & 0 deletions lib/features/playlist/data/datasources/playlist_datasource.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ abstract class PlaylistDatasource {
Future<bool> createPlaylist(String name);
Future<bool> deletePlaylist(int id);
Future<List<SongModel>> getPlaylistSongs(int playlistId);
Future<List<SongModel>> getRecentPlayedSongs();
Future<void> addSongsToPlaylist(int playlistId, List<int> songIds);
Future<void> removeSongsFromPlaylist(int playlistId, List<int> songIds);
Future<bool> renamePlaylist(int id, String newName);
Expand Down Expand Up @@ -187,4 +188,34 @@ class PlaylistDatasourceImpl implements PlaylistDatasource {
rawList,
);
}

@override
Future<List<SongModel>> getRecentPlayedSongs() async {
final recentSongIds =
preferences.getStringList(
PreferencesKeys.recentlyPlayedSongIds,
) ??
<String>[];
if (recentSongIds.isEmpty) {
return [];
} else {
final songIdInts = recentSongIds.map(int.parse).toList();
final result = await audioQuery.querySongs();
final filteredSongs =
result
.where(
(song) => songIdInts.contains(song.id),
)
.toList()
// Sort songs to match the order in recentSongIds
..sort(
(a, b) => songIdInts
.indexOf(a.id)
.compareTo(
songIdInts.indexOf(b.id),
),
);
return filteredSongs;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,15 @@ class PlaylistRepositoryImpl implements PlaylistRepository {
return Result.failure('Failed to save as pinned playlist: $e');
}
}

@override
Future<Result<List<Song>>> getRecentlyPlayedSongs() async {
try {
final songModels = await datasource.getRecentPlayedSongs();
final songs = songModels.map(SongModelMapper.toDomain).toList();
return Result.success(songs);
} on Exception catch (e) {
return Result.failure('Failed to get recently played songs: $e');
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ abstract class PlaylistRepository {
/// Returns a [Result] containing the list of songs or an error.
Future<Result<List<Song>>> getPlaylistSongs(int playlistId);

/// Retrieves the list of recently played songs.
////
/// Returns a [Result] containing the list of songs or an error.
Future<Result<List<Song>>> getRecentlyPlayedSongs();

/// Adds songs to a playlist.
///
/// Parameters:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import 'package:music_player/core/domain/entities/song.dart';
import 'package:music_player/core/result.dart';
import 'package:music_player/features/playlist/domain/repositories/playlist_repository.dart';

class GetRecentlyPlayedSongs {
/// Creates a [GetRecentlyPlayedSongs] use case.
const GetRecentlyPlayedSongs(this._repository);

final PlaylistRepository _repository;

/// Retrieves the list of recently played songs.
//// /// Returns a [Result] containing the list of songs or an error.
Future<Result<List<Song>>> call() {
return _repository.getRecentlyPlayedSongs();
}
}
1 change: 1 addition & 0 deletions lib/features/playlist/domain/usecases/usecases.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export 'get_all_playlists.dart';
export 'get_playlist_by_id.dart';
export 'get_playlist_cover_song_id.dart';
export 'get_playlist_songs.dart';
export 'get_recently_played_songs.dart';
export 'initialize_playlist_covers.dart';
export 'remove_songs_from_playlist.dart';
export 'rename_playlist.dart';
55 changes: 39 additions & 16 deletions lib/features/playlist/presentation/bloc/playlist_details_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,57 @@ class PlaylistDetailsBloc
PlaylistDetailsBloc({
required Playlist playlist,
required this.getPlaylistSongs,
required this.getRecentlyPlayedSongs,
}) : super(PlaylistDetailsState(playlist: playlist)) {
on<GetPlaylistSongsEvent>(_onGetPlaylistSongs);
}

final GetPlaylistSongs getPlaylistSongs;
final GetRecentlyPlayedSongs getRecentlyPlayedSongs;

Future<void> _onGetPlaylistSongs(
GetPlaylistSongsEvent event,
Emitter<PlaylistDetailsState> emit,
) async {
emit(state.copyWith(status: PlaylistDetailsStatus.loading));

final result = await getPlaylistSongs(state.playlist.id);

if (result.isFailure) {
emit(
state.copyWith(
status: PlaylistDetailsStatus.failure,
errorMessage: result.error,
),
);
if (state.playlist.id == -1) {
// return recently played songs
final result = await getRecentlyPlayedSongs();
if (result.isFailure) {
emit(
state.copyWith(
status: PlaylistDetailsStatus.failure,
errorMessage: result.error,
),
);
return;
} else {
emit(
state.copyWith(
status: PlaylistDetailsStatus.success,
songs: result.value,
),
);
return;
}
} else {
emit(
state.copyWith(
status: PlaylistDetailsStatus.success,
songs: result.value,
),
);
final result = await getPlaylistSongs(state.playlist.id);

if (result.isFailure) {
emit(
state.copyWith(
status: PlaylistDetailsStatus.failure,
errorMessage: result.error,
),
);
} else {
emit(
state.copyWith(
status: PlaylistDetailsStatus.success,
songs: result.value,
),
);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class PlaylistDetailsPage extends StatelessWidget {
create: (_) => PlaylistDetailsBloc(
playlist: playlistModel,
getPlaylistSongs: getIt.get(),
getRecentlyPlayedSongs: getIt.get(),
),
child: const _PlaylistDetailsView(),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ class _PlaylistContentViewState extends State<PlaylistContentView> {
children: [
const SizedBox.shrink(),
Text(
'Add Song to Playlist',
context.localization.addSongsToPlaylist,
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w600,
),
Expand Down
Loading
Loading