From 063b9f178d62b9b58f24865f5c42c726fc8b7c8b Mon Sep 17 00:00:00 2001 From: TalebRafiepour Date: Tue, 9 Dec 2025 15:26:05 +0330 Subject: [PATCH 1/9] breaking: change songs sort types, move songs list to reusable view, implement new functinality of loading albums and artist (logic) --- lib/app.dart | 2 + lib/core/commands/delete_song_command.dart | 2 +- lib/core/domain/enums/enums.dart | 1 - .../data/datasources/songs_datasource.dart | 24 ++- .../data/mappers/album_model_mapper.dart | 26 ++++ .../data/mappers/artist_model_mapper.dart | 24 +++ .../songs/data/mappers/enum_mappers.dart | 47 ++++++ lib/features/songs/data/mappers/mappers.dart | 3 + .../data/repositories/songs_repo_impl.dart | 46 +++++- lib/features/songs/domain/entities/album.dart | 15 ++ .../songs/domain/entities/artist.dart | 13 ++ .../songs/domain/entities/entities.dart | 3 +- .../songs/domain/enums/albums_sort_type.dart | 5 + .../songs/domain/enums/artists_sort_type.dart | 5 + lib/features/songs/domain/enums/enums.dart | 3 + .../songs}/domain/enums/songs_sort_type.dart | 4 +- .../domain/repositories/songs_repository.dart | 14 +- .../songs/domain/usecases/query_albums.dart | 16 ++ .../songs/domain/usecases/query_artists.dart | 16 ++ .../songs/domain/usecases/query_songs.dart | 7 +- .../songs/domain/usecases/usecases.dart | 2 + .../songs/presentation/bloc/songs_bloc.dart | 69 ++++++--- .../songs/presentation/bloc/songs_event.dart | 13 +- .../songs/presentation/bloc/songs_state.dart | 12 +- .../songs/presentation/pages/songs_page.dart | 137 +++--------------- .../songs/presentation/views/songs_view.dart | 120 +++++++++++++++ .../presentation/widgets/songs_appbar.dart | 6 +- .../presentation/widgets/sort_type_ruler.dart | 10 +- lib/injection/service_locator.dart | 2 + lib/localization/app_en.arb | 3 + lib/localization/app_fa.arb | 3 + lib/localization/app_localizations.dart | 18 +++ lib/localization/app_localizations_en.dart | 9 ++ lib/localization/app_localizations_fa.dart | 9 ++ lib/main.dart | 4 +- 35 files changed, 532 insertions(+), 161 deletions(-) delete mode 100644 lib/core/domain/enums/enums.dart create mode 100644 lib/features/songs/data/mappers/album_model_mapper.dart create mode 100644 lib/features/songs/data/mappers/artist_model_mapper.dart create mode 100644 lib/features/songs/data/mappers/enum_mappers.dart create mode 100644 lib/features/songs/data/mappers/mappers.dart create mode 100644 lib/features/songs/domain/entities/album.dart create mode 100644 lib/features/songs/domain/entities/artist.dart create mode 100644 lib/features/songs/domain/enums/albums_sort_type.dart create mode 100644 lib/features/songs/domain/enums/artists_sort_type.dart create mode 100644 lib/features/songs/domain/enums/enums.dart rename lib/{core => features/songs}/domain/enums/songs_sort_type.dart (66%) create mode 100644 lib/features/songs/domain/usecases/query_albums.dart create mode 100644 lib/features/songs/domain/usecases/query_artists.dart create mode 100644 lib/features/songs/presentation/views/songs_view.dart diff --git a/lib/app.dart b/lib/app.dart index e992323..54a6148 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -65,6 +65,8 @@ class _MusicPlayerAppState extends State { getIt(), getIt(), getIt(), + getIt(), + getIt(), )..add(const LoadSongsEvent()), ), BlocProvider( diff --git a/lib/core/commands/delete_song_command.dart b/lib/core/commands/delete_song_command.dart index ea96a1a..a8befb1 100644 --- a/lib/core/commands/delete_song_command.dart +++ b/lib/core/commands/delete_song_command.dart @@ -36,7 +36,7 @@ class DeleteSongCommand implements BaseCommand { await originalFile.copy(_backupFile!.path); // Proceed with deletion - final result = await repository.deleteSong(song.data); + final result = await repository.deleteSong(songUri: song.data); if (result.isSuccess && (result.value ?? false)) { _wasDeleted = true; return Result.success(true); diff --git a/lib/core/domain/enums/enums.dart b/lib/core/domain/enums/enums.dart deleted file mode 100644 index db0787f..0000000 --- a/lib/core/domain/enums/enums.dart +++ /dev/null @@ -1 +0,0 @@ -export 'songs_sort_type.dart'; diff --git a/lib/features/songs/data/datasources/songs_datasource.dart b/lib/features/songs/data/datasources/songs_datasource.dart index e76c7c6..8df96d8 100644 --- a/lib/features/songs/data/datasources/songs_datasource.dart +++ b/lib/features/songs/data/datasources/songs_datasource.dart @@ -3,7 +3,15 @@ import 'package:on_audio_query_pluse/on_audio_query.dart'; import 'package:permission_handler/permission_handler.dart'; abstract interface class SongsDatasource { - Future> querySongs(); + Future> querySongs({ + SongSortType sortType = SongSortType.DATE_ADDED, + }); + Future> queryAlbums({ + AlbumSortType sortType = AlbumSortType.ALBUM, + }); + Future> queryArtists({ + ArtistSortType sortType = ArtistSortType.ARTIST, + }); Future deleteSong(String songUri); } @@ -37,5 +45,17 @@ class SongsDatasourceImpl implements SongsDatasource { } @override - Future> querySongs() => _onAudioQuery.querySongs(); + Future> querySongs({ + SongSortType sortType = SongSortType.DATE_ADDED, + }) => _onAudioQuery.querySongs(sortType: sortType); + + @override + Future> queryAlbums({ + AlbumSortType sortType = AlbumSortType.ALBUM, + }) => _onAudioQuery.queryAlbums(); + + @override + Future> queryArtists({ + ArtistSortType sortType = ArtistSortType.ARTIST, + }) => _onAudioQuery.queryArtists(sortType: sortType); } diff --git a/lib/features/songs/data/mappers/album_model_mapper.dart b/lib/features/songs/data/mappers/album_model_mapper.dart new file mode 100644 index 0000000..3fa73b4 --- /dev/null +++ b/lib/features/songs/data/mappers/album_model_mapper.dart @@ -0,0 +1,26 @@ +import 'package:music_player/features/songs/domain/entities/entities.dart'; +import 'package:on_audio_query_pluse/on_audio_query.dart'; + +sealed class AlbumModelMapper { + static AlbumModel fromDomain(Album album) { + final result = AlbumModel({ + '_id': album.id, + 'album': album.album, + 'artist': album.artist, + 'artist_id': album.artistId, + 'numsongs': album.numOfSongs, + }); + + return result; + } + + static Album toDomain(AlbumModel albumModel) { + return Album( + id: albumModel.id, + album: albumModel.album, + artist: albumModel.artist, + artistId: albumModel.artistId, + numOfSongs: albumModel.numOfSongs, + ); + } +} diff --git a/lib/features/songs/data/mappers/artist_model_mapper.dart b/lib/features/songs/data/mappers/artist_model_mapper.dart new file mode 100644 index 0000000..274e3db --- /dev/null +++ b/lib/features/songs/data/mappers/artist_model_mapper.dart @@ -0,0 +1,24 @@ +import 'package:music_player/features/songs/domain/entities/entities.dart'; +import 'package:on_audio_query_pluse/on_audio_query.dart'; + +sealed class ArtistModelMapper { + static ArtistModel fromDomain(Artist artist) { + final result = ArtistModel({ + '_id': artist.id, + 'artist': artist.artist, + 'number_of_albums': artist.numberOfAlbums, + 'number_of_tracks': artist.numberOfTracks, + }); + + return result; + } + + static Artist toDomain(ArtistModel artistModel) { + return Artist( + id: artistModel.id, + artist: artistModel.artist, + numberOfAlbums: artistModel.numberOfAlbums, + numberOfTracks: artistModel.numberOfTracks, + ); + } +} diff --git a/lib/features/songs/data/mappers/enum_mappers.dart b/lib/features/songs/data/mappers/enum_mappers.dart new file mode 100644 index 0000000..b5bf265 --- /dev/null +++ b/lib/features/songs/data/mappers/enum_mappers.dart @@ -0,0 +1,47 @@ +import 'package:music_player/features/songs/domain/enums/enums.dart'; +import 'package:on_audio_query_pluse/on_audio_query.dart'; + +extension SortSongMapper on SongsSortType { + SongSortType toSongSortType() { + switch (this) { + case SongsSortType.dateAdded: + return SongSortType.DATE_ADDED; + case SongsSortType.title: + return SongSortType.TITLE; + case SongsSortType.artist: + return SongSortType.ARTIST; + case SongsSortType.album: + return SongSortType.ALBUM; + case SongsSortType.duration: + return SongSortType.DURATION; + case SongsSortType.size: + return SongSortType.SIZE; + } + } +} + +extension SortAlbumMapper on AlbumsSortType { + AlbumSortType toAlbumSortType() { + switch (this) { + case AlbumsSortType.album: + return AlbumSortType.ALBUM; + case AlbumsSortType.artist: + return AlbumSortType.ARTIST; + case AlbumsSortType.numOfSongs: + return AlbumSortType.NUM_OF_SONGS; + } + } +} + +extension SortArtistMapper on ArtistsSortType { + ArtistSortType toArtistSortType() { + switch (this) { + case ArtistsSortType.artist: + return ArtistSortType.ARTIST; + case ArtistsSortType.numOfAlbums: + return ArtistSortType.NUM_OF_ALBUMS; + case ArtistsSortType.numOfTracks: + return ArtistSortType.NUM_OF_TRACKS; + } + } +} diff --git a/lib/features/songs/data/mappers/mappers.dart b/lib/features/songs/data/mappers/mappers.dart new file mode 100644 index 0000000..324960d --- /dev/null +++ b/lib/features/songs/data/mappers/mappers.dart @@ -0,0 +1,3 @@ +export 'album_model_mapper.dart'; +export 'artist_model_mapper.dart'; +export 'enum_mappers.dart'; diff --git a/lib/features/songs/data/repositories/songs_repo_impl.dart b/lib/features/songs/data/repositories/songs_repo_impl.dart index 2d840ed..b753d92 100644 --- a/lib/features/songs/data/repositories/songs_repo_impl.dart +++ b/lib/features/songs/data/repositories/songs_repo_impl.dart @@ -2,6 +2,10 @@ import 'package:music_player/core/data/mappers/song_model_mapper.dart'; import 'package:music_player/core/domain/entities/song.dart'; import 'package:music_player/core/result.dart'; import 'package:music_player/features/songs/data/datasources/datasources.dart'; +import 'package:music_player/features/songs/data/mappers/mappers.dart'; +import 'package:music_player/features/songs/domain/entities/album.dart'; +import 'package:music_player/features/songs/domain/entities/artist.dart'; +import 'package:music_player/features/songs/domain/enums/enums.dart'; import 'package:music_player/features/songs/domain/repositories/repositories.dart'; class SongsRepoImpl implements SongsRepository { @@ -11,7 +15,7 @@ class SongsRepoImpl implements SongsRepository { final SongsDatasource _songsDatasource; @override - Future> deleteSong(String songUri) async { + Future> deleteSong({required String songUri}) async { try { final deleteResult = await _songsDatasource.deleteSong(songUri); return Result.success(deleteResult); @@ -21,9 +25,13 @@ class SongsRepoImpl implements SongsRepository { } @override - Future>> querySongs() async { + Future>> querySongs({ + SongsSortType sortType = SongsSortType.dateAdded, + }) async { try { - final quriedSongs = await _songsDatasource.querySongs(); + final quriedSongs = await _songsDatasource.querySongs( + sortType: sortType.toSongSortType(), + ); return Result.success( quriedSongs.map(SongModelMapper.toDomain).toList(), ); @@ -31,4 +39,36 @@ class SongsRepoImpl implements SongsRepository { return Result.failure('Error in loading songs, $e'); } } + + @override + Future>> queryAlbums({ + AlbumsSortType sortType = AlbumsSortType.album, + }) async { + try { + final queriedAlbums = await _songsDatasource.queryAlbums( + sortType: sortType.toAlbumSortType(), + ); + return Result.success( + queriedAlbums.map(AlbumModelMapper.toDomain).toList(), + ); + } on Exception catch (e) { + return Result.failure('Error in loading albums, $e'); + } + } + + @override + Future>> queryArtists({ + ArtistsSortType sortType = ArtistsSortType.artist, + }) async { + try { + final queriedArtists = await _songsDatasource.queryArtists( + sortType: sortType.toArtistSortType(), + ); + return Result.success( + queriedArtists.map(ArtistModelMapper.toDomain).toList(), + ); + } on Exception catch (e) { + return Result.failure('Error in loading artists, $e'); + } + } } diff --git a/lib/features/songs/domain/entities/album.dart b/lib/features/songs/domain/entities/album.dart new file mode 100644 index 0000000..eed90ec --- /dev/null +++ b/lib/features/songs/domain/entities/album.dart @@ -0,0 +1,15 @@ +class Album { + const Album({ + required this.id, + required this.album, + required this.numOfSongs, + this.artist, + this.artistId, + }); + + final int id; + final String album; + final String? artist; + final int? artistId; + final int numOfSongs; +} diff --git a/lib/features/songs/domain/entities/artist.dart b/lib/features/songs/domain/entities/artist.dart new file mode 100644 index 0000000..99a0e1e --- /dev/null +++ b/lib/features/songs/domain/entities/artist.dart @@ -0,0 +1,13 @@ +class Artist { + const Artist({ + required this.id, + required this.artist, + this.numberOfAlbums, + this.numberOfTracks, + }); + + final int id; + final String artist; + final int? numberOfAlbums; + final int? numberOfTracks; +} diff --git a/lib/features/songs/domain/entities/entities.dart b/lib/features/songs/domain/entities/entities.dart index 8b13789..2412a3e 100644 --- a/lib/features/songs/domain/entities/entities.dart +++ b/lib/features/songs/domain/entities/entities.dart @@ -1 +1,2 @@ - +export 'album.dart'; +export 'artist.dart'; diff --git a/lib/features/songs/domain/enums/albums_sort_type.dart b/lib/features/songs/domain/enums/albums_sort_type.dart new file mode 100644 index 0000000..c7581a8 --- /dev/null +++ b/lib/features/songs/domain/enums/albums_sort_type.dart @@ -0,0 +1,5 @@ +enum AlbumsSortType { + album, + artist, + numOfSongs, +} diff --git a/lib/features/songs/domain/enums/artists_sort_type.dart b/lib/features/songs/domain/enums/artists_sort_type.dart new file mode 100644 index 0000000..2f29f44 --- /dev/null +++ b/lib/features/songs/domain/enums/artists_sort_type.dart @@ -0,0 +1,5 @@ +enum ArtistsSortType { + artist, + numOfTracks, + numOfAlbums, +} diff --git a/lib/features/songs/domain/enums/enums.dart b/lib/features/songs/domain/enums/enums.dart new file mode 100644 index 0000000..a4aeb15 --- /dev/null +++ b/lib/features/songs/domain/enums/enums.dart @@ -0,0 +1,3 @@ +export 'albums_sort_type.dart'; +export 'artists_sort_type.dart'; +export 'songs_sort_type.dart'; diff --git a/lib/core/domain/enums/songs_sort_type.dart b/lib/features/songs/domain/enums/songs_sort_type.dart similarity index 66% rename from lib/core/domain/enums/songs_sort_type.dart rename to lib/features/songs/domain/enums/songs_sort_type.dart index 8efe74c..1ef0998 100644 --- a/lib/core/domain/enums/songs_sort_type.dart +++ b/lib/features/songs/domain/enums/songs_sort_type.dart @@ -1,6 +1,8 @@ enum SongsSortType { - recentlyAdded, dateAdded, + title, + artist, + album, duration, size, } diff --git a/lib/features/songs/domain/repositories/songs_repository.dart b/lib/features/songs/domain/repositories/songs_repository.dart index a08ca8b..2b49f71 100644 --- a/lib/features/songs/domain/repositories/songs_repository.dart +++ b/lib/features/songs/domain/repositories/songs_repository.dart @@ -1,7 +1,17 @@ import 'package:music_player/core/domain/entities/entities.dart'; import 'package:music_player/core/result.dart'; +import 'package:music_player/features/songs/domain/entities/entities.dart'; +import 'package:music_player/features/songs/domain/enums/enums.dart'; abstract interface class SongsRepository { - Future>> querySongs(); - Future> deleteSong(String songUri); + Future>> querySongs({ + SongsSortType sortType = SongsSortType.dateAdded, + }); + Future>> queryAlbums({ + AlbumsSortType sortType = AlbumsSortType.album, + }); + Future>> queryArtists({ + ArtistsSortType sortType = ArtistsSortType.artist, + }); + Future> deleteSong({required String songUri}); } diff --git a/lib/features/songs/domain/usecases/query_albums.dart b/lib/features/songs/domain/usecases/query_albums.dart new file mode 100644 index 0000000..d733cc9 --- /dev/null +++ b/lib/features/songs/domain/usecases/query_albums.dart @@ -0,0 +1,16 @@ +import 'package:music_player/core/result.dart'; +import 'package:music_player/features/songs/domain/entities/entities.dart'; +import 'package:music_player/features/songs/domain/enums/enums.dart'; +import 'package:music_player/features/songs/domain/repositories/repositories.dart'; + +class QueryAlbums { + const QueryAlbums(this._repository); + + final SongsRepository _repository; + + Future>> call({ + AlbumsSortType sortType = AlbumsSortType.album, + }) { + return _repository.queryAlbums(sortType: sortType); + } +} diff --git a/lib/features/songs/domain/usecases/query_artists.dart b/lib/features/songs/domain/usecases/query_artists.dart new file mode 100644 index 0000000..e96b284 --- /dev/null +++ b/lib/features/songs/domain/usecases/query_artists.dart @@ -0,0 +1,16 @@ +import 'package:music_player/core/result.dart'; +import 'package:music_player/features/songs/domain/entities/entities.dart'; +import 'package:music_player/features/songs/domain/enums/enums.dart'; +import 'package:music_player/features/songs/domain/repositories/repositories.dart'; + +class QueryArtists { + const QueryArtists(this._repository); + + final SongsRepository _repository; + + Future>> call({ + ArtistsSortType sortType = ArtistsSortType.artist, + }) { + return _repository.queryArtists(sortType: sortType); + } +} diff --git a/lib/features/songs/domain/usecases/query_songs.dart b/lib/features/songs/domain/usecases/query_songs.dart index 98553ec..c553423 100644 --- a/lib/features/songs/domain/usecases/query_songs.dart +++ b/lib/features/songs/domain/usecases/query_songs.dart @@ -1,5 +1,6 @@ import 'package:music_player/core/domain/entities/entities.dart'; import 'package:music_player/core/result.dart'; +import 'package:music_player/features/songs/domain/enums/enums.dart'; import 'package:music_player/features/songs/domain/repositories/repositories.dart'; class QuerySongs { @@ -7,7 +8,9 @@ class QuerySongs { final SongsRepository _repository; - Future>> call() { - return _repository.querySongs(); + Future>> call({ + SongsSortType sortType = SongsSortType.dateAdded, + }) { + return _repository.querySongs(sortType: sortType); } } diff --git a/lib/features/songs/domain/usecases/usecases.dart b/lib/features/songs/domain/usecases/usecases.dart index bc98137..4ef4a2b 100644 --- a/lib/features/songs/domain/usecases/usecases.dart +++ b/lib/features/songs/domain/usecases/usecases.dart @@ -1,2 +1,4 @@ export 'delete_song_with_undo.dart'; +export 'query_albums.dart'; +export 'query_artists.dart'; export 'query_songs.dart'; diff --git a/lib/features/songs/presentation/bloc/songs_bloc.dart b/lib/features/songs/presentation/bloc/songs_bloc.dart index aaab630..737b24b 100644 --- a/lib/features/songs/presentation/bloc/songs_bloc.dart +++ b/lib/features/songs/presentation/bloc/songs_bloc.dart @@ -3,8 +3,9 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart' show immutable; import 'package:music_player/core/commands/commands.dart'; import 'package:music_player/core/domain/entities/song.dart'; -import 'package:music_player/core/domain/enums/enums.dart'; import 'package:music_player/core/domain/usecases/usecases.dart'; +import 'package:music_player/features/songs/domain/entities/entities.dart'; +import 'package:music_player/features/songs/domain/enums/enums.dart'; import 'package:music_player/features/songs/domain/usecases/usecases.dart'; part 'songs_event.dart'; @@ -15,10 +16,13 @@ class SongsBloc extends Bloc { this.ensureMediaPermission, this.deleteSong, this.querySongs, + this.queryAlbums, + this.queryArtists, this.commandManager, ) : super(const SongsState()) { on(onLoadSongs); - on(onSortSongs); + on(onLoadAlbums); + on(onLoadArtists); on(onDeleteSong); on(onUndoDeleteSong); on(onCanUndoChanged); @@ -29,6 +33,8 @@ class SongsBloc extends Bloc { final CommandManager commandManager; final DeleteSongWithUndo deleteSong; final QuerySongs querySongs; + final QueryAlbums queryAlbums; + final QueryArtists queryArtists; final EnsureMediaPermission ensureMediaPermission; void _onCanUndoChanged() { @@ -107,19 +113,35 @@ class SongsBloc extends Bloc { emit(state.copyWith(canUndo: event.canUndo)); } - void onSortSongs(SortSongsEvent event, Emitter emit) { - final sortedSongs = List.from(state.allSongs); - _sortSongs(sortedSongs, event.sortType); - emit(state.copyWith(allSongs: sortedSongs, sortType: event.sortType)); + Future onLoadSongs( + LoadSongsEvent event, + Emitter emit, + ) async { + emit(const SongsState(status: SongsStatus.loading)); + final queryResult = await querySongs(sortType: event.sortType); + if (queryResult.isSuccess) { + emit( + SongsState(allSongs: queryResult.value!, status: SongsStatus.loaded), + ); + } else { + emit( + state.copyWith( + errorMessage: queryResult.error, + status: SongsStatus.error, + ), + ); + } } - Future onLoadSongs(SongsEvent event, Emitter emit) async { + Future onLoadAlbums( + LoadAlbumsEvent event, + Emitter emit, + ) async { emit(const SongsState(status: SongsStatus.loading)); - final queryResult = await querySongs(); + final queryResult = await queryAlbums(sortType: event.sortType); if (queryResult.isSuccess) { - _sortSongs(queryResult.value!, state.sortType); emit( - SongsState(allSongs: queryResult.value!, status: SongsStatus.loaded), + SongsState(allAlbums: queryResult.value!, status: SongsStatus.loaded), ); } else { emit( @@ -131,16 +153,23 @@ class SongsBloc extends Bloc { } } - void _sortSongs(List songs, SongsSortType sortType) { - switch (sortType) { - case SongsSortType.recentlyAdded: - songs.sort((a, b) => b.dateAdded.compareTo(a.dateAdded)); - case SongsSortType.dateAdded: - songs.sort((a, b) => a.dateAdded.compareTo(b.dateAdded)); - case SongsSortType.duration: - songs.sort((a, b) => a.duration.compareTo(b.duration)); - case SongsSortType.size: - songs.sort((a, b) => a.size.compareTo(b.size)); + Future onLoadArtists( + LoadArtistsEvent event, + Emitter emit, + ) async { + emit(const SongsState(status: SongsStatus.loading)); + final queryResult = await queryArtists(sortType: event.sortType); + if (queryResult.isSuccess) { + emit( + SongsState(allArtists: queryResult.value!, status: SongsStatus.loaded), + ); + } else { + emit( + state.copyWith( + errorMessage: queryResult.error, + status: SongsStatus.error, + ), + ); } } diff --git a/lib/features/songs/presentation/bloc/songs_event.dart b/lib/features/songs/presentation/bloc/songs_event.dart index f36aede..2146109 100644 --- a/lib/features/songs/presentation/bloc/songs_event.dart +++ b/lib/features/songs/presentation/bloc/songs_event.dart @@ -6,13 +6,18 @@ sealed class SongsEvent { } final class LoadSongsEvent extends SongsEvent { - const LoadSongsEvent(); + const LoadSongsEvent({this.sortType = SongsSortType.dateAdded}); + final SongsSortType sortType; } -final class SortSongsEvent extends SongsEvent { - const SortSongsEvent(this.sortType); +final class LoadAlbumsEvent extends SongsEvent { + const LoadAlbumsEvent({this.sortType = AlbumsSortType.album}); + final AlbumsSortType sortType; +} - final SongsSortType sortType; +final class LoadArtistsEvent extends SongsEvent { + const LoadArtistsEvent({this.sortType = ArtistsSortType.artist}); + final ArtistsSortType sortType; } final class DeleteSongEvent extends SongsEvent { diff --git a/lib/features/songs/presentation/bloc/songs_state.dart b/lib/features/songs/presentation/bloc/songs_state.dart index 697b8be..e42f027 100644 --- a/lib/features/songs/presentation/bloc/songs_state.dart +++ b/lib/features/songs/presentation/bloc/songs_state.dart @@ -4,14 +4,18 @@ part of 'songs_bloc.dart'; final class SongsState extends Equatable { const SongsState({ this.allSongs = const [], + this.allAlbums = const [], + this.allArtists = const [], this.status = SongsStatus.initial, - this.sortType = SongsSortType.recentlyAdded, + this.sortType = SongsSortType.dateAdded, this.canUndo = false, this.lastDeletedSong, this.errorMessage, }); final List allSongs; + final List allAlbums; + final List allArtists; final SongsStatus status; final SongsSortType sortType; final bool canUndo; @@ -20,6 +24,8 @@ final class SongsState extends Equatable { SongsState copyWith({ List? allSongs, + List? allAlbums, + List? allArtists, SongsStatus? status, SongsSortType? sortType, bool? canUndo, @@ -28,6 +34,8 @@ final class SongsState extends Equatable { }) { return SongsState( allSongs: allSongs ?? this.allSongs, + allAlbums: allAlbums ?? this.allAlbums, + allArtists: allArtists ?? this.allArtists, status: status ?? this.status, sortType: sortType ?? this.sortType, canUndo: canUndo ?? this.canUndo, @@ -39,6 +47,8 @@ final class SongsState extends Equatable { @override List get props => [ allSongs, + allAlbums, + allArtists, status, sortType, canUndo, diff --git a/lib/features/songs/presentation/pages/songs_page.dart b/lib/features/songs/presentation/pages/songs_page.dart index a66ef4b..17fc0cb 100644 --- a/lib/features/songs/presentation/pages/songs_page.dart +++ b/lib/features/songs/presentation/pages/songs_page.dart @@ -1,18 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:music_player/core/domain/entities/song.dart'; -import 'package:music_player/core/domain/enums/enums.dart'; -import 'package:music_player/core/mixins/mixins.dart'; import 'package:music_player/core/widgets/widgets.dart'; import 'package:music_player/extensions/extensions.dart'; -import 'package:music_player/features/favorite/presentation/bloc/bloc.dart'; import 'package:music_player/features/music_plyer/presentation/bloc/bloc.dart'; -import 'package:music_player/features/music_plyer/presentation/pages/pages.dart'; -import 'package:music_player/features/playlist/presentation/pages/playlists_page.dart'; import 'package:music_player/features/search/presentation/pages/pages.dart'; +import 'package:music_player/features/songs/domain/enums/enums.dart'; import 'package:music_player/features/songs/presentation/bloc/bloc.dart'; import 'package:music_player/features/songs/presentation/constants/constants.dart'; -import 'package:music_player/features/songs/presentation/pages/songs_selection_page.dart'; +import 'package:music_player/features/songs/presentation/views/songs_view.dart'; import 'package:music_player/features/songs/presentation/widgets/songs_appbar.dart'; import 'package:music_player/features/songs/presentation/widgets/widgets.dart'; @@ -25,13 +20,7 @@ class SongsPage extends StatefulWidget { State createState() => _SongsPageState(); } -class _SongsPageState extends State - with - SongSharingMixin, - RingtoneMixin, - PlaylistManagementMixin, - SongDeletionMixin, - ToggleLikeMixin { +class _SongsPageState extends State { // Getters for BLoCs SongsBloc get _songsBloc => context.read(); MusicPlayerBloc get _musicPlayerBloc => context.read(); @@ -41,16 +30,6 @@ class _SongsPageState extends State super.initState(); } - // Event handlers for song actions - Future _handleSongTap(int songIndex, List songs) async { - _musicPlayerBloc.add(PlayMusicEvent(songIndex, songs)); - await Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => const MusicPlayerPage(), - ), - ); - } - @override Widget build(BuildContext context) { return BlocBuilder( @@ -58,7 +37,6 @@ class _SongsPageState extends State builder: (context, songsState) { return Scaffold( appBar: SongsAppbar( - numOfSongs: songsState.allSongs.length, onSearchButtonPressed: _onSearchButtonPressed, ), body: _buildSongsContent(songsState), @@ -75,18 +53,6 @@ class _SongsPageState extends State ); } - Future onLongPress(Song song, List songs) async { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => SongsSelectionPage( - title: context.localization.songs, - availableSongs: songs, - selectedSongIds: {song.id}, - ), - ), - ); - } - Widget _buildSongsContent(SongsState songsState) { // Handle loading state if (songsState.status == SongsStatus.loading) { @@ -111,87 +77,30 @@ class _SongsPageState extends State final songs = songsState.allSongs; - return BlocBuilder( - bloc: context.read(), - buildWhen: (previous, next) => - previous.currentSongIndex != next.currentSongIndex || - previous.playList != next.playList || - previous.status != next.status, - builder: (context, musicPlayerState) { - return Column( - children: [ - SortTypeRuler( - currentSortType: songsState.sortType, - onSortTypeChanged: _onSortSongs, - ), + return Column( + children: [ + SortTypeRuler( + currentSortType: songsState.sortType, + onSortTypeChanged: _onSortSongs, + ), - Expanded( - child: RefreshIndicator( - onRefresh: () async => _songsBloc.add(const LoadSongsEvent()), - child: - BlocSelector< - FavoriteSongsBloc, - FavoriteSongsState, - Set - >( - selector: (state) { - return state.favoriteSongIds; - }, - builder: (context, favoriteSongIds) { - return ListView.builder( - physics: const BouncingScrollPhysics(), - padding: const EdgeInsets.symmetric( - vertical: SongsPageConstants.listVerticalPadding, - horizontal: - SongsPageConstants.listHorizontalPadding, - ), - itemCount: songs.length, - itemBuilder: (context, index) { - final song = songs[index]; - final isCurrent = - musicPlayerState.currentSong?.id == song.id; - return SongItem( - track: song, - isCurrentTrack: isCurrent, - isPlayingNow: - musicPlayerState.status == - MusicPlayerStatus.playing && - isCurrent, - isFavorite: favoriteSongIds.contains(song.id), - onSetAsRingtone: () => setAsRingtone(song.data), - onDelete: () => showDeleteSongDialog(song), - onFavoriteToggle: () => onToggleLike(song.id), - onAddToPlaylist: () async { - await PlaylistsPage.showSheet( - context: context, - songIds: {song.id}, - ); - }, - onShare: () => shareSong(song), - onLongPress: () => onLongPress(song, songs), - onTap: () => _handleSongTap(index, songs), - onPlayPause: () { - context.read().add( - const TogglePlayPauseEvent(), - ); - }, - ); - }, - ); - }, - ), - ), - ), - // Bottom spacing for mini player - if (_musicPlayerBloc.state.playList.isNotEmpty) - const SizedBox(height: SongsPageConstants.minPlayerHeight), - ], - ); - }, + SongsView( + songs: songs, + onRefresh: () async { + _songsBloc.add(const LoadSongsEvent()); + await Future.delayed( + const Duration(milliseconds: 300), + ); + }, + ), + // Bottom spacing for mini player + if (_musicPlayerBloc.state.playList.isNotEmpty) + const SizedBox(height: SongsPageConstants.minPlayerHeight), + ], ); } void _onSortSongs(SongsSortType sortType) { - _songsBloc.add(SortSongsEvent(sortType)); + _songsBloc.add(LoadSongsEvent(sortType: sortType)); } } diff --git a/lib/features/songs/presentation/views/songs_view.dart b/lib/features/songs/presentation/views/songs_view.dart new file mode 100644 index 0000000..d407cd3 --- /dev/null +++ b/lib/features/songs/presentation/views/songs_view.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:music_player/core/domain/entities/song.dart'; +import 'package:music_player/core/mixins/mixins.dart'; +import 'package:music_player/core/widgets/song_item.dart'; +import 'package:music_player/extensions/extensions.dart'; +import 'package:music_player/features/favorite/presentation/bloc/bloc.dart'; +import 'package:music_player/features/music_plyer/presentation/bloc/bloc.dart'; +import 'package:music_player/features/music_plyer/presentation/pages/pages.dart'; +import 'package:music_player/features/playlist/playlist.dart'; +import 'package:music_player/features/songs/presentation/pages/pages.dart'; + +class SongsView extends StatefulWidget { + const SongsView({ + required this.songs, + this.onRefresh, + super.key, + }); + + final List songs; + final Future Function()? onRefresh; + + @override + State createState() => _SongsViewState(); +} + +class _SongsViewState extends State + with + SongSharingMixin, + RingtoneMixin, + PlaylistManagementMixin, + SongDeletionMixin, + ToggleLikeMixin { + // Event handlers for song actions + Future _handleSongTap(int songIndex, List songs) async { + context.read().add(PlayMusicEvent(songIndex, songs)); + await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const MusicPlayerPage(), + ), + ); + } + + Future onLongPress(Song song, List songs) async { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => SongsSelectionPage( + title: context.localization.songs, + availableSongs: songs, + selectedSongIds: {song.id}, + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + bloc: context.read(), + buildWhen: (previous, next) => + previous.currentSongIndex != next.currentSongIndex || + previous.playList != next.playList || + previous.status != next.status, + builder: (context, musicPlayerState) { + return Expanded( + child: RefreshIndicator( + onRefresh: widget.onRefresh ?? () async {}, + child: + BlocSelector>( + selector: (state) { + return state.favoriteSongIds; + }, + builder: (context, favoriteSongIds) { + return ListView.builder( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 8, + ), + itemCount: widget.songs.length, + itemBuilder: (context, index) { + final song = widget.songs[index]; + final isCurrent = + musicPlayerState.currentSong?.id == song.id; + return SongItem( + track: song, + isCurrentTrack: isCurrent, + isPlayingNow: + musicPlayerState.status == + MusicPlayerStatus.playing && + isCurrent, + isFavorite: favoriteSongIds.contains(song.id), + onSetAsRingtone: () => setAsRingtone(song.data), + onDelete: () => showDeleteSongDialog(song), + onFavoriteToggle: () => onToggleLike(song.id), + onAddToPlaylist: () async { + await PlaylistsPage.showSheet( + context: context, + songIds: {song.id}, + ); + }, + onShare: () => shareSong(song), + onLongPress: () => onLongPress(song, widget.songs), + onTap: () => _handleSongTap(index, widget.songs), + onPlayPause: () { + context.read().add( + const TogglePlayPauseEvent(), + ); + }, + ); + }, + ); + }, + ), + ), + ); + }, + ); + } +} diff --git a/lib/features/songs/presentation/widgets/songs_appbar.dart b/lib/features/songs/presentation/widgets/songs_appbar.dart index 5bf8adf..40d6ec2 100644 --- a/lib/features/songs/presentation/widgets/songs_appbar.dart +++ b/lib/features/songs/presentation/widgets/songs_appbar.dart @@ -1,18 +1,16 @@ import 'package:flutter/material.dart'; -import 'package:music_player/core/domain/enums/enums.dart'; import 'package:music_player/extensions/extensions.dart'; +import 'package:music_player/features/songs/domain/enums/enums.dart'; import 'package:music_player/features/songs/presentation/constants/constants.dart'; typedef OnSortSongsCallback = void Function(SongsSortType); class SongsAppbar extends StatelessWidget implements PreferredSizeWidget { const SongsAppbar({ - required this.numOfSongs, this.onSearchButtonPressed, super.key, }); - final int numOfSongs; final VoidCallback? onSearchButtonPressed; @override @@ -23,7 +21,7 @@ class SongsAppbar extends StatelessWidget implements PreferredSizeWidget { return AppBar( elevation: 0, title: Text( - context.localization.allSongs(numOfSongs), + context.localization.appTitle, ), centerTitle: true, actions: [ diff --git a/lib/features/songs/presentation/widgets/sort_type_ruler.dart b/lib/features/songs/presentation/widgets/sort_type_ruler.dart index 88e5467..0e242fa 100644 --- a/lib/features/songs/presentation/widgets/sort_type_ruler.dart +++ b/lib/features/songs/presentation/widgets/sort_type_ruler.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:music_player/core/domain/enums/enums.dart'; import 'package:music_player/extensions/extensions.dart'; +import 'package:music_player/features/songs/domain/enums/enums.dart'; class SortTypeRuler extends StatelessWidget { const SortTypeRuler({ @@ -16,16 +16,20 @@ class SortTypeRuler extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); final sortTypes = [ - SongsSortType.recentlyAdded, SongsSortType.dateAdded, SongsSortType.duration, SongsSortType.size, + SongsSortType.title, + SongsSortType.album, + SongsSortType.artist, ]; final labels = [ - context.localization.recent, context.localization.dateAdded, context.localization.duration, context.localization.size, + context.localization.title, + context.localization.album, + context.localization.artist, ]; return DefaultTabController( diff --git a/lib/injection/service_locator.dart b/lib/injection/service_locator.dart index 7bb04c1..c08e6f2 100644 --- a/lib/injection/service_locator.dart +++ b/lib/injection/service_locator.dart @@ -78,6 +78,8 @@ void _setupSongsFeature() { getIt ..registerLazySingleton(CommandManager.new) ..registerLazySingleton(() => QuerySongs(getIt.get())) + ..registerLazySingleton(() => QueryAlbums(getIt.get())) + ..registerLazySingleton(() => QueryArtists(getIt.get())) ..registerLazySingleton(() => DeleteSongWithUndo(getIt.get(), getIt.get())) // Repositories ..registerLazySingleton( diff --git a/lib/localization/app_en.arb b/lib/localization/app_en.arb index 449e328..7c62e40 100644 --- a/lib/localization/app_en.arb +++ b/lib/localization/app_en.arb @@ -73,6 +73,9 @@ "dateAdded": "Date Added", "duration": "Duration", "size": "Size", + "title": "Title", + "album": "Album", + "artist": "Artist", "ascending": "Ascending", "descending": "Descending", "dismiss": "DISMISS", diff --git a/lib/localization/app_fa.arb b/lib/localization/app_fa.arb index 9d5e3ed..1d8a021 100644 --- a/lib/localization/app_fa.arb +++ b/lib/localization/app_fa.arb @@ -70,6 +70,9 @@ "dateAdded": "تاریخ افزدون", "duration": "مدت زمان", "size": "اندازه", + "title": "عنوان", + "album": "آلبوم", + "artist": "هنرمند", "ascending": "افزایشی", "descending": "کاهشی", "dismiss": "رد کردن", diff --git a/lib/localization/app_localizations.dart b/lib/localization/app_localizations.dart index df79660..5d72fec 100644 --- a/lib/localization/app_localizations.dart +++ b/lib/localization/app_localizations.dart @@ -392,6 +392,24 @@ abstract class AppLocalizations { /// **'Size'** String get size; + /// No description provided for @title. + /// + /// In en, this message translates to: + /// **'Title'** + String get title; + + /// No description provided for @album. + /// + /// In en, this message translates to: + /// **'Album'** + String get album; + + /// No description provided for @artist. + /// + /// In en, this message translates to: + /// **'Artist'** + String get artist; + /// No description provided for @ascending. /// /// In en, this message translates to: diff --git a/lib/localization/app_localizations_en.dart b/lib/localization/app_localizations_en.dart index 0c9cbf6..a195339 100644 --- a/lib/localization/app_localizations_en.dart +++ b/lib/localization/app_localizations_en.dart @@ -160,6 +160,15 @@ class AppLocalizationsEn extends AppLocalizations { @override String get size => 'Size'; + @override + String get title => 'Title'; + + @override + String get album => 'Album'; + + @override + String get artist => 'Artist'; + @override String get ascending => 'Ascending'; diff --git a/lib/localization/app_localizations_fa.dart b/lib/localization/app_localizations_fa.dart index c2eb6da..182cab6 100644 --- a/lib/localization/app_localizations_fa.dart +++ b/lib/localization/app_localizations_fa.dart @@ -160,6 +160,15 @@ class AppLocalizationsFa extends AppLocalizations { @override String get size => 'اندازه'; + @override + String get title => 'عنوان'; + + @override + String get album => 'آلبوم'; + + @override + String get artist => 'هنرمند'; + @override String get ascending => 'افزایشی'; diff --git a/lib/main.dart b/lib/main.dart index 745b7a0..d5bbb41 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,8 +8,8 @@ import 'package:music_player/core/services/logger/logger.dart'; import 'package:music_player/injection/service_locator.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -Future main() async { - await runZonedGuarded( +void main() { + runZonedGuarded( () async { final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); From 8ee32bb7c3b2df987dfc7e41a98c3dcce4954dfc Mon Sep 17 00:00:00 2001 From: TalebRafiepour Date: Tue, 9 Dec 2025 15:56:23 +0330 Subject: [PATCH 2/9] ref: reuse the SongsView widget in the Favorites and Search pages --- lib/core/views/songs_view.dart | 119 +++++++++++++++++ lib/core/views/views.dart | 1 + lib/core/widgets/no_songs_widget.dart | 81 +++--------- .../pages/favorite_songs_page.dart | 84 +----------- .../presentation/pages/search_songs_page.dart | 80 +----------- .../songs/presentation/pages/songs_page.dart | 20 +-- .../songs/presentation/views/songs_view.dart | 120 ------------------ 7 files changed, 159 insertions(+), 346 deletions(-) create mode 100644 lib/core/views/songs_view.dart create mode 100644 lib/core/views/views.dart delete mode 100644 lib/features/songs/presentation/views/songs_view.dart diff --git a/lib/core/views/songs_view.dart b/lib/core/views/songs_view.dart new file mode 100644 index 0000000..a07fbfd --- /dev/null +++ b/lib/core/views/songs_view.dart @@ -0,0 +1,119 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:music_player/core/domain/entities/song.dart'; +import 'package:music_player/core/mixins/mixins.dart'; +import 'package:music_player/core/widgets/no_songs_widget.dart'; +import 'package:music_player/core/widgets/song_item.dart'; +import 'package:music_player/extensions/extensions.dart'; +import 'package:music_player/features/favorite/presentation/bloc/bloc.dart'; +import 'package:music_player/features/music_plyer/presentation/bloc/bloc.dart'; +import 'package:music_player/features/music_plyer/presentation/pages/pages.dart'; +import 'package:music_player/features/playlist/playlist.dart'; +import 'package:music_player/features/songs/presentation/pages/pages.dart'; + +class SongsView extends StatefulWidget { + const SongsView({ + required this.songs, + this.onRefresh, + super.key, + }); + + final List songs; + final Future Function()? onRefresh; + + @override + State createState() => _SongsViewState(); +} + +class _SongsViewState extends State + with + SongSharingMixin, + RingtoneMixin, + PlaylistManagementMixin, + SongDeletionMixin, + ToggleLikeMixin { + // Event handlers for song actions + Future _handleSongTap(int songIndex, List songs) async { + context.read().add(PlayMusicEvent(songIndex, songs)); + await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const MusicPlayerPage(), + ), + ); + } + + Future onLongPress(Song song, List songs) async { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => SongsSelectionPage( + title: context.localization.songs, + availableSongs: songs, + selectedSongIds: {song.id}, + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + if (widget.songs.isEmpty) { + return const NoSongsWidget(); + } + return BlocBuilder( + bloc: context.read(), + buildWhen: (previous, next) => + previous.currentSongIndex != next.currentSongIndex || + previous.playList != next.playList || + previous.status != next.status, + builder: (context, musicPlayerState) { + return RefreshIndicator( + onRefresh: widget.onRefresh ?? () async {}, + child: BlocSelector>( + selector: (state) { + return state.favoriteSongIds; + }, + builder: (context, favoriteSongIds) { + return ListView.builder( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 8, + ), + itemCount: widget.songs.length, + itemBuilder: (context, index) { + final song = widget.songs[index]; + final isCurrent = musicPlayerState.currentSong?.id == song.id; + return SongItem( + track: song, + isCurrentTrack: isCurrent, + isPlayingNow: + musicPlayerState.status == MusicPlayerStatus.playing && + isCurrent, + isFavorite: favoriteSongIds.contains(song.id), + onSetAsRingtone: () => setAsRingtone(song.data), + onDelete: () => showDeleteSongDialog(song), + onFavoriteToggle: () => onToggleLike(song.id), + onAddToPlaylist: () async { + await PlaylistsPage.showSheet( + context: context, + songIds: {song.id}, + ); + }, + onShare: () => shareSong(song), + onLongPress: () => onLongPress(song, widget.songs), + onTap: () => _handleSongTap(index, widget.songs), + onPlayPause: () { + context.read().add( + const TogglePlayPauseEvent(), + ); + }, + ); + }, + ); + }, + ), + ); + }, + ); + } +} diff --git a/lib/core/views/views.dart b/lib/core/views/views.dart new file mode 100644 index 0000000..b325afe --- /dev/null +++ b/lib/core/views/views.dart @@ -0,0 +1 @@ +export 'songs_view.dart'; diff --git a/lib/core/widgets/no_songs_widget.dart b/lib/core/widgets/no_songs_widget.dart index 0eb4b18..d6df1c7 100644 --- a/lib/core/widgets/no_songs_widget.dart +++ b/lib/core/widgets/no_songs_widget.dart @@ -2,13 +2,15 @@ import 'package:flutter/material.dart'; import 'package:music_player/core/constants/constants.dart'; import 'package:music_player/extensions/extensions.dart'; -class NoSongsWidget2 extends StatelessWidget { - const NoSongsWidget2({ +class NoSongsWidget extends StatelessWidget { + const NoSongsWidget({ super.key, this.message = 'No Songs in Your Library', + this.onRefresh, }); final String message; + final VoidCallback? onRefresh; @override Widget build(BuildContext context) { @@ -43,69 +45,24 @@ class NoSongsWidget2 extends StatelessWidget { ), ), ), - ], - ), - ); - } -} - -class NoSongsWidget extends StatelessWidget { - const NoSongsWidget({ - super.key, - this.onRefresh, - this.message = 'No songs available', - }); - - final String message; - final VoidCallback? onRefresh; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return Center( - child: Card( - elevation: 4, - margin: const EdgeInsets.symmetric(horizontal: 32), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 24), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Image.asset( - ImageAssets.emptySongs, - width: 85, - ), - const SizedBox(height: 20), - Text( - message, - textAlign: TextAlign.center, - style: theme.textTheme.titleMedium?.copyWith( - color: theme.colorScheme.onSurface.withValues( - alpha: 0.8, - ), - fontWeight: FontWeight.w600, + if (onRefresh != null) ...[ + const SizedBox(height: 24), + ElevatedButton.icon( + onPressed: onRefresh, + icon: const Icon(Icons.refresh), + label: Text(context.localization.refresh), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, ), - ), - const SizedBox(height: 24), - ElevatedButton.icon( - onPressed: onRefresh, - icon: const Icon(Icons.refresh), - label: Text(context.localization.refresh), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 12, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), ), ), - ], - ), - ), + ), + ], + ], ), ); } diff --git a/lib/features/favorite/presentation/pages/favorite_songs_page.dart b/lib/features/favorite/presentation/pages/favorite_songs_page.dart index 1bb5c22..689cbd5 100644 --- a/lib/features/favorite/presentation/pages/favorite_songs_page.dart +++ b/lib/features/favorite/presentation/pages/favorite_songs_page.dart @@ -1,14 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:music_player/core/domain/entities/song.dart'; -import 'package:music_player/core/mixins/mixins.dart'; -import 'package:music_player/core/widgets/widgets.dart'; +import 'package:music_player/core/views/views.dart'; import 'package:music_player/extensions/extensions.dart'; import 'package:music_player/features/favorite/presentation/bloc/bloc.dart'; import 'package:music_player/features/favorite/presentation/widgets/widgets.dart'; -import 'package:music_player/features/music_plyer/presentation/bloc/bloc.dart'; -import 'package:music_player/features/music_plyer/presentation/pages/pages.dart'; -import 'package:music_player/features/playlist/playlist.dart'; import 'package:music_player/features/songs/presentation/pages/songs_selection_page.dart'; class FavoriteSongsPage extends StatefulWidget { @@ -18,12 +14,7 @@ class FavoriteSongsPage extends StatefulWidget { State createState() => _FavoriteSongsPageState(); } -class _FavoriteSongsPageState extends State - with - SongSharingMixin, - RingtoneMixin, - PlaylistManagementMixin, - SongDeletionMixin { +class _FavoriteSongsPageState extends State { late final FavoriteSongsBloc favSongsBloc = context.read(); @override void initState() { @@ -31,22 +22,6 @@ class _FavoriteSongsPageState extends State favSongsBloc.add(const LoadFavoriteSongsEvent()); } - Future _handleSongTap(int songIndex, List favoriteSongs) async { - context.read().add( - PlayMusicEvent(songIndex, favoriteSongs), - ); - - await Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => const MusicPlayerPage(), - ), - ); - } - - void _handleToggleLike(int songId) { - favSongsBloc.add(ToggleFavoriteSongEvent(songId)); - } - void onLongPress(Song song) { Navigator.of(context).push( MaterialPageRoute( @@ -132,58 +107,9 @@ class _FavoriteSongsPageState extends State ); } - return _buildFavoritesList(favoriteState.favoriteSongs); - } - - Widget _buildFavoritesList(List favoriteSongs) { - return BlocBuilder( - bloc: context.read(), - buildWhen: (previous, next) => - previous.currentSongIndex != next.currentSongIndex || - previous.playList != next.playList || - previous.status != next.status, - builder: (context, musicPlayerState) { - return RefreshIndicator( - onRefresh: onRefresh, - - child: ListView.builder( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), - itemCount: favoriteSongs.length, - physics: const AlwaysScrollableScrollPhysics( - parent: BouncingScrollPhysics(), - ), - itemBuilder: (context, index) { - final song = favoriteSongs[index]; - final isCurrent = musicPlayerState.currentSong?.id == song.id; - return SongItem( - track: song, - isCurrentTrack: isCurrent, - isPlayingNow: - musicPlayerState.status == MusicPlayerStatus.playing && - isCurrent, - isFavorite: true, - onTap: () => _handleSongTap(index, favoriteSongs), - onLongPress: () => onLongPress(song), - onFavoriteToggle: () => _handleToggleLike(song.id), - onShare: () => shareSong(song), - onSetAsRingtone: () => setAsRingtone(song.data), - onDelete: () => showDeleteSongDialog(song), - onAddToPlaylist: () async { - await PlaylistsPage.showSheet( - context: context, - songIds: {song.id}, - ); - }, - onPlayPause: () { - context.read().add( - const TogglePlayPauseEvent(), - ); - }, - ); - }, - ), - ); - }, + return SongsView( + songs: favoriteState.favoriteSongs, + onRefresh: onRefresh, ); } diff --git a/lib/features/search/presentation/pages/search_songs_page.dart b/lib/features/search/presentation/pages/search_songs_page.dart index aa87838..993cd82 100644 --- a/lib/features/search/presentation/pages/search_songs_page.dart +++ b/lib/features/search/presentation/pages/search_songs_page.dart @@ -3,16 +3,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:music_player/core/domain/entities/song.dart'; -import 'package:music_player/core/mixins/mixins.dart'; +import 'package:music_player/core/views/views.dart'; import 'package:music_player/core/widgets/widgets.dart'; import 'package:music_player/extensions/extensions.dart'; -import 'package:music_player/features/favorite/favorite.dart'; -import 'package:music_player/features/music_plyer/presentation/bloc/bloc.dart'; -import 'package:music_player/features/music_plyer/presentation/pages/pages.dart'; -import 'package:music_player/features/playlist/presentation/pages/pages.dart'; import 'package:music_player/features/search/presentation/widgets/search_songs_appbar.dart'; import 'package:music_player/features/songs/presentation/bloc/bloc.dart'; -import 'package:music_player/features/songs/presentation/pages/pages.dart'; class SearchSongsPage extends StatefulWidget { const SearchSongsPage({super.key}); @@ -21,13 +16,7 @@ class SearchSongsPage extends StatefulWidget { State createState() => _SearchSongsPageState(); } -class _SearchSongsPageState extends State - with - SongSharingMixin, - RingtoneMixin, - PlaylistManagementMixin, - SongDeletionMixin { - MusicPlayerBloc get _musicPlayerBloc => context.read(); +class _SearchSongsPageState extends State { final searchStream = StreamController(); @override @@ -36,32 +25,6 @@ class _SearchSongsPageState extends State super.dispose(); } - void onLongPress(Song song, List allSongs) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => SongsSelectionPage( - title: context.localization.searchSongs, - availableSongs: allSongs, - selectedSongIds: {song.id}, - ), - ), - ); - } - - // Event handlers for song actions - Future _handleSongTap(int songIndex, List songs) async { - _musicPlayerBloc.add(PlayMusicEvent(songIndex, songs)); - await Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => const MusicPlayerPage(), - ), - ); - } - - void onToggleLike(int songId) { - context.read().add(ToggleFavoriteSongEvent(songId)); - } - @override Widget build(BuildContext context) { return Scaffold( @@ -103,43 +66,8 @@ class _SearchSongsPageState extends State } else { filteredSongs = state.allSongs; } - return BlocSelector< - FavoriteSongsBloc, - FavoriteSongsState, - Set - >( - selector: (state) { - return state.favoriteSongIds; - }, - builder: (context, favoriteSongIds) { - if (filteredSongs.isEmpty) { - return const NoSongsWidget2(); - } - return ListView.builder( - padding: const EdgeInsets.only(top: 24), - itemCount: filteredSongs.length, - - itemBuilder: (context, index) { - final song = filteredSongs[index]; - return SongItem( - track: song, - isFavorite: favoriteSongIds.contains(song.id), - onSetAsRingtone: () => setAsRingtone(song.data), - onDelete: () => showDeleteSongDialog(song), - onFavoriteToggle: () => onToggleLike(song.id), - onAddToPlaylist: () async { - await PlaylistsPage.showSheet( - context: context, - songIds: {song.id}, - ); - }, - onShare: () => shareSong(song), - onLongPress: () => onLongPress(song, filteredSongs), - onTap: () => _handleSongTap(index, filteredSongs), - ); - }, - ); - }, + return SongsView( + songs: filteredSongs, ); }, ); diff --git a/lib/features/songs/presentation/pages/songs_page.dart b/lib/features/songs/presentation/pages/songs_page.dart index 17fc0cb..e726b10 100644 --- a/lib/features/songs/presentation/pages/songs_page.dart +++ b/lib/features/songs/presentation/pages/songs_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:music_player/core/views/views.dart'; import 'package:music_player/core/widgets/widgets.dart'; import 'package:music_player/extensions/extensions.dart'; import 'package:music_player/features/music_plyer/presentation/bloc/bloc.dart'; @@ -7,7 +8,6 @@ import 'package:music_player/features/search/presentation/pages/pages.dart'; import 'package:music_player/features/songs/domain/enums/enums.dart'; import 'package:music_player/features/songs/presentation/bloc/bloc.dart'; import 'package:music_player/features/songs/presentation/constants/constants.dart'; -import 'package:music_player/features/songs/presentation/views/songs_view.dart'; import 'package:music_player/features/songs/presentation/widgets/songs_appbar.dart'; import 'package:music_player/features/songs/presentation/widgets/widgets.dart'; @@ -84,14 +84,16 @@ class _SongsPageState extends State { onSortTypeChanged: _onSortSongs, ), - SongsView( - songs: songs, - onRefresh: () async { - _songsBloc.add(const LoadSongsEvent()); - await Future.delayed( - const Duration(milliseconds: 300), - ); - }, + Expanded( + child: SongsView( + songs: songs, + onRefresh: () async { + _songsBloc.add(const LoadSongsEvent()); + await Future.delayed( + const Duration(milliseconds: 300), + ); + }, + ), ), // Bottom spacing for mini player if (_musicPlayerBloc.state.playList.isNotEmpty) diff --git a/lib/features/songs/presentation/views/songs_view.dart b/lib/features/songs/presentation/views/songs_view.dart deleted file mode 100644 index d407cd3..0000000 --- a/lib/features/songs/presentation/views/songs_view.dart +++ /dev/null @@ -1,120 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:music_player/core/domain/entities/song.dart'; -import 'package:music_player/core/mixins/mixins.dart'; -import 'package:music_player/core/widgets/song_item.dart'; -import 'package:music_player/extensions/extensions.dart'; -import 'package:music_player/features/favorite/presentation/bloc/bloc.dart'; -import 'package:music_player/features/music_plyer/presentation/bloc/bloc.dart'; -import 'package:music_player/features/music_plyer/presentation/pages/pages.dart'; -import 'package:music_player/features/playlist/playlist.dart'; -import 'package:music_player/features/songs/presentation/pages/pages.dart'; - -class SongsView extends StatefulWidget { - const SongsView({ - required this.songs, - this.onRefresh, - super.key, - }); - - final List songs; - final Future Function()? onRefresh; - - @override - State createState() => _SongsViewState(); -} - -class _SongsViewState extends State - with - SongSharingMixin, - RingtoneMixin, - PlaylistManagementMixin, - SongDeletionMixin, - ToggleLikeMixin { - // Event handlers for song actions - Future _handleSongTap(int songIndex, List songs) async { - context.read().add(PlayMusicEvent(songIndex, songs)); - await Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => const MusicPlayerPage(), - ), - ); - } - - Future onLongPress(Song song, List songs) async { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => SongsSelectionPage( - title: context.localization.songs, - availableSongs: songs, - selectedSongIds: {song.id}, - ), - ), - ); - } - - @override - Widget build(BuildContext context) { - return BlocBuilder( - bloc: context.read(), - buildWhen: (previous, next) => - previous.currentSongIndex != next.currentSongIndex || - previous.playList != next.playList || - previous.status != next.status, - builder: (context, musicPlayerState) { - return Expanded( - child: RefreshIndicator( - onRefresh: widget.onRefresh ?? () async {}, - child: - BlocSelector>( - selector: (state) { - return state.favoriteSongIds; - }, - builder: (context, favoriteSongIds) { - return ListView.builder( - physics: const BouncingScrollPhysics(), - padding: const EdgeInsets.symmetric( - vertical: 12, - horizontal: 8, - ), - itemCount: widget.songs.length, - itemBuilder: (context, index) { - final song = widget.songs[index]; - final isCurrent = - musicPlayerState.currentSong?.id == song.id; - return SongItem( - track: song, - isCurrentTrack: isCurrent, - isPlayingNow: - musicPlayerState.status == - MusicPlayerStatus.playing && - isCurrent, - isFavorite: favoriteSongIds.contains(song.id), - onSetAsRingtone: () => setAsRingtone(song.data), - onDelete: () => showDeleteSongDialog(song), - onFavoriteToggle: () => onToggleLike(song.id), - onAddToPlaylist: () async { - await PlaylistsPage.showSheet( - context: context, - songIds: {song.id}, - ); - }, - onShare: () => shareSong(song), - onLongPress: () => onLongPress(song, widget.songs), - onTap: () => _handleSongTap(index, widget.songs), - onPlayPause: () { - context.read().add( - const TogglePlayPauseEvent(), - ); - }, - ); - }, - ); - }, - ), - ), - ); - }, - ); - } -} From a7f6226701fbbbbc14d9133040d7e82bc2b9975d Mon Sep 17 00:00:00 2001 From: TalebRafiepour Date: Tue, 9 Dec 2025 17:11:15 +0330 Subject: [PATCH 3/9] feat: show filter button and song count at top of song view --- lib/core/widgets/songs_count.dart | 10 +-- .../songs/domain/enums/songs_sort_type.dart | 39 ++++++++++++ .../songs/presentation/pages/songs_page.dart | 29 ++++++--- .../widgets/songs_filter_bottom_sheet.dart | 63 +++++++++++++++++++ .../presentation/widgets/sort_button.dart | 31 +++++++++ .../songs/presentation/widgets/widgets.dart | 2 + 6 files changed, 162 insertions(+), 12 deletions(-) create mode 100644 lib/features/songs/presentation/widgets/songs_filter_bottom_sheet.dart create mode 100644 lib/features/songs/presentation/widgets/sort_button.dart diff --git a/lib/core/widgets/songs_count.dart b/lib/core/widgets/songs_count.dart index 7a0e17e..70b9b47 100644 --- a/lib/core/widgets/songs_count.dart +++ b/lib/core/widgets/songs_count.dart @@ -7,14 +7,16 @@ class SongsCount extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context); return Row( + spacing: 4, children: [ - Icon(Icons.library_music, size: 18, color: theme.primaryColor), - const SizedBox(width: 8), + const Icon( + Icons.music_note_rounded, + size: 18, + ), Text( '${context.localization.songs}: $songCount', - style: theme.textTheme.labelSmall?.copyWith( + style: context.theme.textTheme.labelSmall?.copyWith( fontWeight: FontWeight.bold, ), ), diff --git a/lib/features/songs/domain/enums/songs_sort_type.dart b/lib/features/songs/domain/enums/songs_sort_type.dart index 1ef0998..f50fff2 100644 --- a/lib/features/songs/domain/enums/songs_sort_type.dart +++ b/lib/features/songs/domain/enums/songs_sort_type.dart @@ -1,3 +1,6 @@ +import 'package:flutter/material.dart' show BuildContext, IconData, Icons; +import 'package:music_player/extensions/extensions.dart'; + enum SongsSortType { dateAdded, title, @@ -6,3 +9,39 @@ enum SongsSortType { duration, size, } + +extension SongsSortTypeExtension on SongsSortType { + String toLocalizationString(BuildContext context) { + switch (this) { + case SongsSortType.dateAdded: + return context.localization.dateAdded; + case SongsSortType.title: + return context.localization.title; + case SongsSortType.artist: + return context.localization.artist; + case SongsSortType.album: + return context.localization.album; + case SongsSortType.duration: + return context.localization.duration; + case SongsSortType.size: + return context.localization.size; + } + } + + IconData toIconData() { + switch (this) { + case SongsSortType.dateAdded: + return Icons.calendar_today; + case SongsSortType.title: + return Icons.text_fields; + case SongsSortType.artist: + return Icons.person; + case SongsSortType.album: + return Icons.album; + case SongsSortType.duration: + return Icons.access_time; + case SongsSortType.size: + return Icons.sd_storage; + } + } +} diff --git a/lib/features/songs/presentation/pages/songs_page.dart b/lib/features/songs/presentation/pages/songs_page.dart index e726b10..d8aa171 100644 --- a/lib/features/songs/presentation/pages/songs_page.dart +++ b/lib/features/songs/presentation/pages/songs_page.dart @@ -21,6 +21,7 @@ class SongsPage extends StatefulWidget { } class _SongsPageState extends State { + SongsSortType _currentSortType = SongsSortType.dateAdded; // Getters for BLoCs SongsBloc get _songsBloc => context.read(); MusicPlayerBloc get _musicPlayerBloc => context.read(); @@ -53,6 +54,17 @@ class _SongsPageState extends State { ); } + Future _onFilterTap() async { + final selectedSortType = await SongsFilterBottomSheet.show( + context: context, + selectedSortType: _currentSortType, + ); + if (selectedSortType != null) { + _currentSortType = selectedSortType; + _songsBloc.add(LoadSongsEvent(sortType: selectedSortType)); + } + } + Widget _buildSongsContent(SongsState songsState) { // Handle loading state if (songsState.status == SongsStatus.loading) { @@ -79,10 +91,15 @@ class _SongsPageState extends State { return Column( children: [ - SortTypeRuler( - currentSortType: songsState.sortType, - onSortTypeChanged: _onSortSongs, - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + FilterButton( + onTap: _onFilterTap, + ), + SongsCount(songCount: songs.length), + ], + ).padding(value: 12), Expanded( child: SongsView( @@ -101,8 +118,4 @@ class _SongsPageState extends State { ], ); } - - void _onSortSongs(SongsSortType sortType) { - _songsBloc.add(LoadSongsEvent(sortType: sortType)); - } } diff --git a/lib/features/songs/presentation/widgets/songs_filter_bottom_sheet.dart b/lib/features/songs/presentation/widgets/songs_filter_bottom_sheet.dart new file mode 100644 index 0000000..5e1193f --- /dev/null +++ b/lib/features/songs/presentation/widgets/songs_filter_bottom_sheet.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:music_player/core/widgets/widgets.dart'; +import 'package:music_player/extensions/extensions.dart'; +import 'package:music_player/features/songs/domain/enums/enums.dart'; + +class SongsFilterBottomSheet extends StatelessWidget { + const SongsFilterBottomSheet._(this.selectedSortType); + + final SongsSortType selectedSortType; + + static Future show({ + required BuildContext context, + SongsSortType selectedSortType = SongsSortType.dateAdded, + }) { + return showModalBottomSheet( + context: context, + builder: (_) => SongsFilterBottomSheet._(selectedSortType), + ); + } + + @override + Widget build(BuildContext context) { + return GlassCard( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12), + child: Column( + children: [ + Text( + context.localization.sortSongs, + style: context.theme.textTheme.titleLarge, + ), + const SizedBox(height: 12), + ...SongsSortType.values.map( + (sortType) { + final isSelected = sortType == selectedSortType; + return ListTile( + leading: Icon( + sortType.toIconData(), + color: isSelected ? context.theme.primaryColor : null, + ), + title: Text( + sortType.toLocalizationString(context), + style: context.theme.textTheme.bodyLarge?.copyWith( + color: isSelected ? context.theme.primaryColor : null, + ), + ), + trailing: isSelected + ? Icon(Icons.check, color: context.theme.primaryColor) + : null, + onTap: () { + Navigator.of(context).pop(sortType); + }, + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/features/songs/presentation/widgets/sort_button.dart b/lib/features/songs/presentation/widgets/sort_button.dart new file mode 100644 index 0000000..0733a09 --- /dev/null +++ b/lib/features/songs/presentation/widgets/sort_button.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:music_player/core/widgets/widgets.dart'; +import 'package:music_player/extensions/extensions.dart'; + +class FilterButton extends StatelessWidget { + const FilterButton({super.key, this.onTap}); + + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + return GlassCard( + onTap: onTap, + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + spacing: 3, + children: [ + const Icon(Icons.sort_rounded), + Text( + context.localization.sortSongs, + style: context.theme.textTheme.labelMedium, + ), + ], + ), + ); + } +} diff --git a/lib/features/songs/presentation/widgets/widgets.dart b/lib/features/songs/presentation/widgets/widgets.dart index 1d6bc2f..9ee5878 100644 --- a/lib/features/songs/presentation/widgets/widgets.dart +++ b/lib/features/songs/presentation/widgets/widgets.dart @@ -1,4 +1,6 @@ export 'selection_action_bar.dart'; export 'selection_more_button.dart'; export 'selection_song_card.dart'; +export 'songs_filter_bottom_sheet.dart'; +export 'sort_button.dart'; export 'sort_type_ruler.dart'; From 3e17d74da56de725deb1821160241980edc59b12 Mon Sep 17 00:00:00 2001 From: TalebRafiepour Date: Tue, 9 Dec 2025 18:01:44 +0330 Subject: [PATCH 4/9] chore: initialize new views for all songs, albums, artists and syncronize the tabbar with PageView --- .../presentation/constants/constants.dart | 1 - .../constants/songs_page_constants.dart | 29 ---- .../songs/presentation/pages/songs_page.dart | 134 ++++++------------ .../songs/presentation/views/albums_view.dart | 15 ++ .../presentation/views/all_songs_view.dart | 88 ++++++++++++ .../presentation/views/artists_view.dart | 15 ++ .../songs/presentation/views/views.dart | 3 + .../presentation/widgets/category_tabbar.dart | 43 ++++++ .../presentation/widgets/songs_appbar.dart | 5 +- .../presentation/widgets/sort_type_ruler.dart | 58 -------- .../songs/presentation/widgets/widgets.dart | 2 +- lib/localization/app_en.arb | 12 +- lib/localization/app_fa.arb | 12 +- lib/localization/app_localizations.dart | 18 ++- lib/localization/app_localizations_en.dart | 10 +- lib/localization/app_localizations_fa.dart | 10 +- 16 files changed, 247 insertions(+), 208 deletions(-) delete mode 100644 lib/features/songs/presentation/constants/constants.dart delete mode 100644 lib/features/songs/presentation/constants/songs_page_constants.dart create mode 100644 lib/features/songs/presentation/views/albums_view.dart create mode 100644 lib/features/songs/presentation/views/all_songs_view.dart create mode 100644 lib/features/songs/presentation/views/artists_view.dart create mode 100644 lib/features/songs/presentation/views/views.dart create mode 100644 lib/features/songs/presentation/widgets/category_tabbar.dart delete mode 100644 lib/features/songs/presentation/widgets/sort_type_ruler.dart diff --git a/lib/features/songs/presentation/constants/constants.dart b/lib/features/songs/presentation/constants/constants.dart deleted file mode 100644 index 06e228f..0000000 --- a/lib/features/songs/presentation/constants/constants.dart +++ /dev/null @@ -1 +0,0 @@ -export 'songs_page_constants.dart'; diff --git a/lib/features/songs/presentation/constants/songs_page_constants.dart b/lib/features/songs/presentation/constants/songs_page_constants.dart deleted file mode 100644 index 77f9ea5..0000000 --- a/lib/features/songs/presentation/constants/songs_page_constants.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:flutter/material.dart'; - -/// Constants used throughout the Songs feature -class SongsPageConstants { - // Private constructor to prevent instantiation - SongsPageConstants._(); - // Colors - static const appBarColor = Color(0xFF7F53AC); - - // Sizes - static const searchIconSize = 28.0; - static const fabIconSize = 28.0; - static const toolbarHeight = 56.0; // Standard toolbar height - - // Durations - static const undoSnackbarDuration = Duration(seconds: 20); - static const defaultSnackbarDuration = Duration(seconds: 3); - - // Padding and Margins - static const listVerticalPadding = 12.0; - static const listHorizontalPadding = 8.0; - static const addButtonPadding = 4.0; - static const minPlayerHeight = 80.0; - static const headerSpacing = 8.0; - - // Text Styles - static const appBarFontSize = 26.0; - static const appBarLetterSpacing = 1.2; -} diff --git a/lib/features/songs/presentation/pages/songs_page.dart b/lib/features/songs/presentation/pages/songs_page.dart index d8aa171..4504df8 100644 --- a/lib/features/songs/presentation/pages/songs_page.dart +++ b/lib/features/songs/presentation/pages/songs_page.dart @@ -1,13 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:music_player/core/views/views.dart'; -import 'package:music_player/core/widgets/widgets.dart'; -import 'package:music_player/extensions/extensions.dart'; -import 'package:music_player/features/music_plyer/presentation/bloc/bloc.dart'; import 'package:music_player/features/search/presentation/pages/pages.dart'; -import 'package:music_player/features/songs/domain/enums/enums.dart'; -import 'package:music_player/features/songs/presentation/bloc/bloc.dart'; -import 'package:music_player/features/songs/presentation/constants/constants.dart'; +import 'package:music_player/features/songs/presentation/views/views.dart'; import 'package:music_player/features/songs/presentation/widgets/songs_appbar.dart'; import 'package:music_player/features/songs/presentation/widgets/widgets.dart'; @@ -20,29 +13,59 @@ class SongsPage extends StatefulWidget { State createState() => _SongsPageState(); } -class _SongsPageState extends State { - SongsSortType _currentSortType = SongsSortType.dateAdded; - // Getters for BLoCs - SongsBloc get _songsBloc => context.read(); - MusicPlayerBloc get _musicPlayerBloc => context.read(); - +class _SongsPageState extends State + with SingleTickerProviderStateMixin { + late final TabController tabController; + late final PageController pageController; @override void initState() { + tabController = TabController(length: 3, vsync: this); + pageController = PageController(); super.initState(); } + void animateToNewPage(int index) { + pageController.animateToPage( + index, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + + @override + void dispose() { + tabController.dispose(); + pageController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - return BlocBuilder( - bloc: _songsBloc, - builder: (context, songsState) { - return Scaffold( - appBar: SongsAppbar( - onSearchButtonPressed: _onSearchButtonPressed, + return Scaffold( + appBar: SongsAppbar( + onSearchButtonPressed: _onSearchButtonPressed, + ), + body: Column( + children: [ + CategoryTabbar( + tabController: tabController, + onTabChanged: animateToNewPage, + ), + Expanded( + child: PageView( + controller: pageController, + onPageChanged: (newPageIndex) { + tabController.animateTo(newPageIndex); + }, + children: const [ + AlbumsView(), + ArtistsView(), + AllSongsView(), + ], + ), ), - body: _buildSongsContent(songsState), - ); - }, + ], + ), ); } @@ -53,69 +76,4 @@ class _SongsPageState extends State { ), ); } - - Future _onFilterTap() async { - final selectedSortType = await SongsFilterBottomSheet.show( - context: context, - selectedSortType: _currentSortType, - ); - if (selectedSortType != null) { - _currentSortType = selectedSortType; - _songsBloc.add(LoadSongsEvent(sortType: selectedSortType)); - } - } - - Widget _buildSongsContent(SongsState songsState) { - // Handle loading state - if (songsState.status == SongsStatus.loading) { - return const Loading(); - } - - // Handle error state - if (songsState.status == SongsStatus.error) { - return SongsErrorLoading( - message: context.localization.errorLoadingSongs, - onRetry: () => _songsBloc.add(const LoadSongsEvent()), - ); - } - - // Handle empty songs - if (songsState.allSongs.isEmpty) { - return NoSongsWidget( - message: context.localization.noSongTryAgain, - onRefresh: () => _songsBloc.add(const LoadSongsEvent()), - ); - } - - final songs = songsState.allSongs; - - return Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - FilterButton( - onTap: _onFilterTap, - ), - SongsCount(songCount: songs.length), - ], - ).padding(value: 12), - - Expanded( - child: SongsView( - songs: songs, - onRefresh: () async { - _songsBloc.add(const LoadSongsEvent()); - await Future.delayed( - const Duration(milliseconds: 300), - ); - }, - ), - ), - // Bottom spacing for mini player - if (_musicPlayerBloc.state.playList.isNotEmpty) - const SizedBox(height: SongsPageConstants.minPlayerHeight), - ], - ); - } } diff --git a/lib/features/songs/presentation/views/albums_view.dart b/lib/features/songs/presentation/views/albums_view.dart new file mode 100644 index 0000000..d550f86 --- /dev/null +++ b/lib/features/songs/presentation/views/albums_view.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class AlbumsView extends StatelessWidget { + const AlbumsView({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: Text( + 'Albums View', + style: Theme.of(context).textTheme.headlineMedium, + ), + ); + } +} diff --git a/lib/features/songs/presentation/views/all_songs_view.dart b/lib/features/songs/presentation/views/all_songs_view.dart new file mode 100644 index 0000000..8cf7b7a --- /dev/null +++ b/lib/features/songs/presentation/views/all_songs_view.dart @@ -0,0 +1,88 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:music_player/core/views/views.dart'; +import 'package:music_player/core/widgets/widgets.dart'; +import 'package:music_player/extensions/extensions.dart'; +import 'package:music_player/features/music_plyer/presentation/bloc/bloc.dart'; +import 'package:music_player/features/songs/presentation/bloc/bloc.dart'; +import 'package:music_player/features/songs/presentation/widgets/widgets.dart'; + +class AllSongsView extends StatelessWidget { + const AllSongsView({super.key}); + + @override + Widget build(BuildContext context) { + final songsBloc = context.read(); + scheduleMicrotask(() { + songsBloc.add(const LoadSongsEvent()); + }); + return BlocBuilder( + bloc: songsBloc, + builder: (context, songsState) { + // Handle loading state + if (songsState.status == SongsStatus.loading) { + return const Loading(); + } + + // Handle error state + if (songsState.status == SongsStatus.error) { + return SongsErrorLoading( + message: context.localization.errorLoadingSongs, + onRetry: () => songsBloc.add(const LoadSongsEvent()), + ); + } + + // Handle empty songs + if (songsState.allSongs.isEmpty) { + return NoSongsWidget( + message: context.localization.noSongTryAgain, + onRefresh: () => songsBloc.add(const LoadSongsEvent()), + ); + } + + final songs = songsState.allSongs; + + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + FilterButton( + onTap: () async { + final selectedSortType = await SongsFilterBottomSheet.show( + context: context, + selectedSortType: songsState.sortType, + ); + if (selectedSortType != null) { + songsBloc.add( + LoadSongsEvent(sortType: selectedSortType), + ); + } + }, + ), + SongsCount(songCount: songs.length), + ], + ).padding(value: 12), + + Expanded( + child: SongsView( + songs: songs, + onRefresh: () async { + songsBloc.add(const LoadSongsEvent()); + await Future.delayed( + const Duration(milliseconds: 300), + ); + }, + ), + ), + // Bottom spacing for mini player + if (context.read().state.playList.isNotEmpty) + const SizedBox(height: 68), + ], + ); + }, + ); + } +} diff --git a/lib/features/songs/presentation/views/artists_view.dart b/lib/features/songs/presentation/views/artists_view.dart new file mode 100644 index 0000000..4d21744 --- /dev/null +++ b/lib/features/songs/presentation/views/artists_view.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class ArtistsView extends StatelessWidget { + const ArtistsView({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: Text( + 'Artists View', + style: Theme.of(context).textTheme.headlineMedium, + ), + ); + } +} diff --git a/lib/features/songs/presentation/views/views.dart b/lib/features/songs/presentation/views/views.dart new file mode 100644 index 0000000..8a4e3b1 --- /dev/null +++ b/lib/features/songs/presentation/views/views.dart @@ -0,0 +1,3 @@ +export 'albums_view.dart'; +export 'all_songs_view.dart'; +export 'artists_view.dart'; diff --git a/lib/features/songs/presentation/widgets/category_tabbar.dart b/lib/features/songs/presentation/widgets/category_tabbar.dart new file mode 100644 index 0000000..d9d8b7e --- /dev/null +++ b/lib/features/songs/presentation/widgets/category_tabbar.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:music_player/extensions/extensions.dart'; + +class CategoryTabbar extends StatelessWidget { + const CategoryTabbar({ + required this.tabController, + super.key, + this.onTabChanged, + }); + + final TabController tabController; + final void Function(int index)? onTabChanged; + + @override + Widget build(BuildContext context) { + final theme = context.theme; + final labels = [ + context.localization.albums, + context.localization.artists, + context.localization.allSongs, + ]; + + return TabBar( + controller: tabController, + onTap: (index) => onTabChanged?.call(index), + indicatorWeight: 3, + dividerHeight: 1.5, + labelColor: theme.primaryColor, + unselectedLabelColor: theme.textTheme.bodyMedium?.color?.withValues( + alpha: 0.7, + ), + labelStyle: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + ), + unselectedLabelStyle: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + ), + tabs: labels.map((label) => Tab(text: label)).toList(), + ); + } +} diff --git a/lib/features/songs/presentation/widgets/songs_appbar.dart b/lib/features/songs/presentation/widgets/songs_appbar.dart index 40d6ec2..fb80c22 100644 --- a/lib/features/songs/presentation/widgets/songs_appbar.dart +++ b/lib/features/songs/presentation/widgets/songs_appbar.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:music_player/extensions/extensions.dart'; import 'package:music_player/features/songs/domain/enums/enums.dart'; -import 'package:music_player/features/songs/presentation/constants/constants.dart'; typedef OnSortSongsCallback = void Function(SongsSortType); @@ -30,10 +29,8 @@ class SongsAppbar extends StatelessWidget implements PreferredSizeWidget { tooltip: context.localization.searchSongs, icon: const Icon( Icons.search, - size: SongsPageConstants.searchIconSize, - color: Colors.white, ), - ), + ).paddingSymmetric(horizontal: 6), ], ); } diff --git a/lib/features/songs/presentation/widgets/sort_type_ruler.dart b/lib/features/songs/presentation/widgets/sort_type_ruler.dart deleted file mode 100644 index 0e242fa..0000000 --- a/lib/features/songs/presentation/widgets/sort_type_ruler.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:music_player/extensions/extensions.dart'; -import 'package:music_player/features/songs/domain/enums/enums.dart'; - -class SortTypeRuler extends StatelessWidget { - const SortTypeRuler({ - required this.currentSortType, - super.key, - this.onSortTypeChanged, - }); - - final SongsSortType currentSortType; - final void Function(SongsSortType sortType)? onSortTypeChanged; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final sortTypes = [ - SongsSortType.dateAdded, - SongsSortType.duration, - SongsSortType.size, - SongsSortType.title, - SongsSortType.album, - SongsSortType.artist, - ]; - final labels = [ - context.localization.dateAdded, - context.localization.duration, - context.localization.size, - context.localization.title, - context.localization.album, - context.localization.artist, - ]; - - return DefaultTabController( - length: sortTypes.length, - initialIndex: sortTypes.indexOf(currentSortType), - child: TabBar( - onTap: (index) => onSortTypeChanged?.call(sortTypes[index]), - indicatorWeight: 3, - dividerHeight: 1.5, - labelColor: theme.primaryColor, - unselectedLabelColor: theme.textTheme.bodyMedium?.color?.withValues( - alpha: 0.7, - ), - labelStyle: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 14, - ), - unselectedLabelStyle: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 14, - ), - tabs: labels.map((label) => Tab(text: label)).toList(), - ), - ); - } -} diff --git a/lib/features/songs/presentation/widgets/widgets.dart b/lib/features/songs/presentation/widgets/widgets.dart index 9ee5878..545c4b3 100644 --- a/lib/features/songs/presentation/widgets/widgets.dart +++ b/lib/features/songs/presentation/widgets/widgets.dart @@ -1,6 +1,6 @@ +export 'category_tabbar.dart'; export 'selection_action_bar.dart'; export 'selection_more_button.dart'; export 'selection_song_card.dart'; export 'songs_filter_bottom_sheet.dart'; export 'sort_button.dart'; -export 'sort_type_ruler.dart'; diff --git a/lib/localization/app_en.arb b/lib/localization/app_en.arb index 7c62e40..8a79834 100644 --- a/lib/localization/app_en.arb +++ b/lib/localization/app_en.arb @@ -52,15 +52,7 @@ "cancel": "Cancel", "deleteFromDevice": "Delete from device", "favoriteSongs": "Favorite Songs", - "allSongs": "All Songs({count})", - "@allSongs": { - "description": "", - "placeholders": { - "count": { - "type": "int" - } - } - }, + "allSongs": "All Songs", "searchSongs": "Search Songs", "refresh": "Refresh", "errorLoadingSongs": "Some error happend while loading songs", @@ -75,7 +67,9 @@ "size": "Size", "title": "Title", "album": "Album", + "albums": "Albums", "artist": "Artist", + "artists": "Artists", "ascending": "Ascending", "descending": "Descending", "dismiss": "DISMISS", diff --git a/lib/localization/app_fa.arb b/lib/localization/app_fa.arb index 1d8a021..9c7763d 100644 --- a/lib/localization/app_fa.arb +++ b/lib/localization/app_fa.arb @@ -49,15 +49,7 @@ "cancel": "انصراف", "deleteFromDevice": "حذف از دستگاه", "favoriteSongs": "موزیک های محبوب", - "allSongs": "همه موزیک ها({count})", - "@allSongs": { - "description": "", - "placeholders": { - "count": { - "type": "int" - } - } - }, + "allSongs": "همه موزیک ها", "searchSongs": "جستجوی موزیک ها", "refresh": "تازه سازی", "errorLoadingSongs": "در هنگام باگزاری موزیک ها خطایی رخ داده است", @@ -72,7 +64,9 @@ "size": "اندازه", "title": "عنوان", "album": "آلبوم", + "albums": "آلبوم ها", "artist": "هنرمند", + "artists": "هنرمندان", "ascending": "افزایشی", "descending": "کاهشی", "dismiss": "رد کردن", diff --git a/lib/localization/app_localizations.dart b/lib/localization/app_localizations.dart index 5d72fec..5e6711a 100644 --- a/lib/localization/app_localizations.dart +++ b/lib/localization/app_localizations.dart @@ -314,11 +314,11 @@ abstract class AppLocalizations { /// **'Favorite Songs'** String get favoriteSongs; - /// + /// No description provided for @allSongs. /// /// In en, this message translates to: - /// **'All Songs({count})'** - String allSongs(int count); + /// **'All Songs'** + String get allSongs; /// No description provided for @searchSongs. /// @@ -404,12 +404,24 @@ abstract class AppLocalizations { /// **'Album'** String get album; + /// No description provided for @albums. + /// + /// In en, this message translates to: + /// **'Albums'** + String get albums; + /// No description provided for @artist. /// /// In en, this message translates to: /// **'Artist'** String get artist; + /// No description provided for @artists. + /// + /// In en, this message translates to: + /// **'Artists'** + String get artists; + /// No description provided for @ascending. /// /// In en, this message translates to: diff --git a/lib/localization/app_localizations_en.dart b/lib/localization/app_localizations_en.dart index a195339..1d56df2 100644 --- a/lib/localization/app_localizations_en.dart +++ b/lib/localization/app_localizations_en.dart @@ -119,9 +119,7 @@ class AppLocalizationsEn extends AppLocalizations { String get favoriteSongs => 'Favorite Songs'; @override - String allSongs(int count) { - return 'All Songs($count)'; - } + String get allSongs => 'All Songs'; @override String get searchSongs => 'Search Songs'; @@ -166,9 +164,15 @@ class AppLocalizationsEn extends AppLocalizations { @override String get album => 'Album'; + @override + String get albums => 'Albums'; + @override String get artist => 'Artist'; + @override + String get artists => 'Artists'; + @override String get ascending => 'Ascending'; diff --git a/lib/localization/app_localizations_fa.dart b/lib/localization/app_localizations_fa.dart index 182cab6..e2a8ef1 100644 --- a/lib/localization/app_localizations_fa.dart +++ b/lib/localization/app_localizations_fa.dart @@ -119,9 +119,7 @@ class AppLocalizationsFa extends AppLocalizations { String get favoriteSongs => 'موزیک های محبوب'; @override - String allSongs(int count) { - return 'همه موزیک ها($count)'; - } + String get allSongs => 'همه موزیک ها'; @override String get searchSongs => 'جستجوی موزیک ها'; @@ -166,9 +164,15 @@ class AppLocalizationsFa extends AppLocalizations { @override String get album => 'آلبوم'; + @override + String get albums => 'آلبوم ها'; + @override String get artist => 'هنرمند'; + @override + String get artists => 'هنرمندان'; + @override String get ascending => 'افزایشی'; From fd3407b4db41e48273e1b57f6b42e9bc49a8fb3a Mon Sep 17 00:00:00 2001 From: TalebRafiepour Date: Tue, 9 Dec 2025 18:16:39 +0330 Subject: [PATCH 5/9] chore: create AlbumsBloc for managing Albums page widget --- lib/app.dart | 7 +++- .../songs/presentation/bloc/albums_bloc.dart | 39 ++++++++++++++++++ .../songs/presentation/bloc/albums_event.dart | 11 +++++ .../songs/presentation/bloc/albums_state.dart | 40 +++++++++++++++++++ .../songs/presentation/bloc/bloc.dart | 1 + .../songs/presentation/bloc/songs_bloc.dart | 23 ----------- .../songs/presentation/bloc/songs_event.dart | 5 --- 7 files changed, 97 insertions(+), 29 deletions(-) create mode 100644 lib/features/songs/presentation/bloc/albums_bloc.dart create mode 100644 lib/features/songs/presentation/bloc/albums_event.dart create mode 100644 lib/features/songs/presentation/bloc/albums_state.dart diff --git a/lib/app.dart b/lib/app.dart index 54a6148..7fdf363 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -59,6 +59,12 @@ class _MusicPlayerAppState extends State { FlutterNativeSplash.remove(); return MultiBlocProvider( providers: [ + BlocProvider( + create: (_) => AlbumsBloc(getIt()) + ..add( + const LoadAlbumsEvent(), + ), + ), BlocProvider( create: (_) => SongsBloc( getIt(), @@ -66,7 +72,6 @@ class _MusicPlayerAppState extends State { getIt(), getIt(), getIt(), - getIt(), )..add(const LoadSongsEvent()), ), BlocProvider( diff --git a/lib/features/songs/presentation/bloc/albums_bloc.dart b/lib/features/songs/presentation/bloc/albums_bloc.dart new file mode 100644 index 0000000..171a724 --- /dev/null +++ b/lib/features/songs/presentation/bloc/albums_bloc.dart @@ -0,0 +1,39 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart' show immutable; +import 'package:music_player/features/songs/domain/entities/entities.dart'; +import 'package:music_player/features/songs/domain/enums/enums.dart'; +import 'package:music_player/features/songs/domain/usecases/usecases.dart'; + +part 'albums_event.dart'; +part 'albums_state.dart'; + +class AlbumsBloc extends Bloc { + AlbumsBloc( + this.queryAlbums, + ) : super(const AlbumsState()) { + on(onLoadAlbums); + } + + final QueryAlbums queryAlbums; + + Future onLoadAlbums( + LoadAlbumsEvent event, + Emitter emit, + ) async { + emit(const AlbumsState(status: AlbumsStatus.loading)); + final queryResult = await queryAlbums(sortType: event.sortType); + if (queryResult.isSuccess) { + emit( + AlbumsState(allAlbums: queryResult.value!, status: AlbumsStatus.loaded), + ); + } else { + emit( + state.copyWith( + errorMessage: queryResult.error, + status: AlbumsStatus.error, + ), + ); + } + } +} diff --git a/lib/features/songs/presentation/bloc/albums_event.dart b/lib/features/songs/presentation/bloc/albums_event.dart new file mode 100644 index 0000000..20330a2 --- /dev/null +++ b/lib/features/songs/presentation/bloc/albums_event.dart @@ -0,0 +1,11 @@ +part of 'albums_bloc.dart'; + +@immutable +sealed class AlbumsEvent { + const AlbumsEvent(); +} + +final class LoadAlbumsEvent extends AlbumsEvent { + const LoadAlbumsEvent({this.sortType = AlbumsSortType.album}); + final AlbumsSortType sortType; +} diff --git a/lib/features/songs/presentation/bloc/albums_state.dart b/lib/features/songs/presentation/bloc/albums_state.dart new file mode 100644 index 0000000..3cfb474 --- /dev/null +++ b/lib/features/songs/presentation/bloc/albums_state.dart @@ -0,0 +1,40 @@ +part of 'albums_bloc.dart'; + +@immutable +final class AlbumsState extends Equatable { + const AlbumsState({ + this.allAlbums = const [], + this.status = AlbumsStatus.initial, + this.sortType = AlbumsSortType.album, + this.errorMessage, + }); + + final List allAlbums; + final AlbumsStatus status; + final AlbumsSortType sortType; + final String? errorMessage; + + AlbumsState copyWith({ + List? allAlbums, + AlbumsStatus? status, + AlbumsSortType? sortType, + String? errorMessage, + }) { + return AlbumsState( + allAlbums: allAlbums ?? this.allAlbums, + status: status ?? this.status, + sortType: sortType ?? this.sortType, + errorMessage: errorMessage, + ); + } + + @override + List get props => [ + allAlbums, + status, + sortType, + if (errorMessage != null) errorMessage!, + ]; +} + +enum AlbumsStatus { initial, loading, loaded, error } diff --git a/lib/features/songs/presentation/bloc/bloc.dart b/lib/features/songs/presentation/bloc/bloc.dart index 7c1cf6a..404fc04 100644 --- a/lib/features/songs/presentation/bloc/bloc.dart +++ b/lib/features/songs/presentation/bloc/bloc.dart @@ -1 +1,2 @@ +export 'albums_bloc.dart'; export 'songs_bloc.dart'; diff --git a/lib/features/songs/presentation/bloc/songs_bloc.dart b/lib/features/songs/presentation/bloc/songs_bloc.dart index 737b24b..77cf0e2 100644 --- a/lib/features/songs/presentation/bloc/songs_bloc.dart +++ b/lib/features/songs/presentation/bloc/songs_bloc.dart @@ -16,12 +16,10 @@ class SongsBloc extends Bloc { this.ensureMediaPermission, this.deleteSong, this.querySongs, - this.queryAlbums, this.queryArtists, this.commandManager, ) : super(const SongsState()) { on(onLoadSongs); - on(onLoadAlbums); on(onLoadArtists); on(onDeleteSong); on(onUndoDeleteSong); @@ -33,7 +31,6 @@ class SongsBloc extends Bloc { final CommandManager commandManager; final DeleteSongWithUndo deleteSong; final QuerySongs querySongs; - final QueryAlbums queryAlbums; final QueryArtists queryArtists; final EnsureMediaPermission ensureMediaPermission; @@ -133,26 +130,6 @@ class SongsBloc extends Bloc { } } - Future onLoadAlbums( - LoadAlbumsEvent event, - Emitter emit, - ) async { - emit(const SongsState(status: SongsStatus.loading)); - final queryResult = await queryAlbums(sortType: event.sortType); - if (queryResult.isSuccess) { - emit( - SongsState(allAlbums: queryResult.value!, status: SongsStatus.loaded), - ); - } else { - emit( - state.copyWith( - errorMessage: queryResult.error, - status: SongsStatus.error, - ), - ); - } - } - Future onLoadArtists( LoadArtistsEvent event, Emitter emit, diff --git a/lib/features/songs/presentation/bloc/songs_event.dart b/lib/features/songs/presentation/bloc/songs_event.dart index 2146109..f462af9 100644 --- a/lib/features/songs/presentation/bloc/songs_event.dart +++ b/lib/features/songs/presentation/bloc/songs_event.dart @@ -10,11 +10,6 @@ final class LoadSongsEvent extends SongsEvent { final SongsSortType sortType; } -final class LoadAlbumsEvent extends SongsEvent { - const LoadAlbumsEvent({this.sortType = AlbumsSortType.album}); - final AlbumsSortType sortType; -} - final class LoadArtistsEvent extends SongsEvent { const LoadArtistsEvent({this.sortType = ArtistsSortType.artist}); final ArtistsSortType sortType; From b42ddcc3873738b2aeac6f6de526205cb82adf8c Mon Sep 17 00:00:00 2001 From: TalebRafiepour Date: Tue, 9 Dec 2025 18:24:23 +0330 Subject: [PATCH 6/9] chore: create separate bloc for Artists page widget --- lib/app.dart | 9 +++- .../songs/presentation/bloc/artists_bloc.dart | 42 +++++++++++++++++++ .../presentation/bloc/artists_event.dart | 11 +++++ .../presentation/bloc/artists_state.dart | 40 ++++++++++++++++++ .../songs/presentation/bloc/bloc.dart | 1 + .../songs/presentation/bloc/songs_bloc.dart | 23 ---------- .../songs/presentation/bloc/songs_event.dart | 5 --- 7 files changed, 102 insertions(+), 29 deletions(-) create mode 100644 lib/features/songs/presentation/bloc/artists_bloc.dart create mode 100644 lib/features/songs/presentation/bloc/artists_event.dart create mode 100644 lib/features/songs/presentation/bloc/artists_state.dart diff --git a/lib/app.dart b/lib/app.dart index 7fdf363..efea311 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -65,13 +65,20 @@ class _MusicPlayerAppState extends State { const LoadAlbumsEvent(), ), ), + BlocProvider( + create: (_) => + ArtistsBloc( + getIt(), + )..add( + const LoadArtistsEvent(), + ), + ), BlocProvider( create: (_) => SongsBloc( getIt(), getIt(), getIt(), getIt(), - getIt(), )..add(const LoadSongsEvent()), ), BlocProvider( diff --git a/lib/features/songs/presentation/bloc/artists_bloc.dart b/lib/features/songs/presentation/bloc/artists_bloc.dart new file mode 100644 index 0000000..578dba2 --- /dev/null +++ b/lib/features/songs/presentation/bloc/artists_bloc.dart @@ -0,0 +1,42 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart' show immutable; +import 'package:music_player/features/songs/domain/entities/entities.dart'; +import 'package:music_player/features/songs/domain/enums/enums.dart'; +import 'package:music_player/features/songs/domain/usecases/usecases.dart'; + +part 'artists_event.dart'; +part 'artists_state.dart'; + +class ArtistsBloc extends Bloc { + ArtistsBloc( + this.queryArtists, + ) : super(const ArtistsState()) { + on(onLoadArtists); + } + + final QueryArtists queryArtists; + + Future onLoadArtists( + LoadArtistsEvent event, + Emitter emit, + ) async { + emit(const ArtistsState(status: ArtistsStatus.loading)); + final queryResult = await queryArtists(sortType: event.sortType); + if (queryResult.isSuccess) { + emit( + ArtistsState( + allArtists: queryResult.value!, + status: ArtistsStatus.loaded, + ), + ); + } else { + emit( + state.copyWith( + errorMessage: queryResult.error, + status: ArtistsStatus.error, + ), + ); + } + } +} diff --git a/lib/features/songs/presentation/bloc/artists_event.dart b/lib/features/songs/presentation/bloc/artists_event.dart new file mode 100644 index 0000000..573bd65 --- /dev/null +++ b/lib/features/songs/presentation/bloc/artists_event.dart @@ -0,0 +1,11 @@ +part of 'artists_bloc.dart'; + +@immutable +sealed class ArtistsEvent { + const ArtistsEvent(); +} + +final class LoadArtistsEvent extends ArtistsEvent { + const LoadArtistsEvent({this.sortType = ArtistsSortType.artist}); + final ArtistsSortType sortType; +} diff --git a/lib/features/songs/presentation/bloc/artists_state.dart b/lib/features/songs/presentation/bloc/artists_state.dart new file mode 100644 index 0000000..5da743c --- /dev/null +++ b/lib/features/songs/presentation/bloc/artists_state.dart @@ -0,0 +1,40 @@ +part of 'artists_bloc.dart'; + +@immutable +final class ArtistsState extends Equatable { + const ArtistsState({ + this.allArtists = const [], + this.status = ArtistsStatus.initial, + this.sortType = ArtistsSortType.artist, + this.errorMessage, + }); + + final List allArtists; + final ArtistsStatus status; + final ArtistsSortType sortType; + final String? errorMessage; + + ArtistsState copyWith({ + List? allArtists, + ArtistsStatus? status, + ArtistsSortType? sortType, + String? errorMessage, + }) { + return ArtistsState( + allArtists: allArtists ?? this.allArtists, + status: status ?? this.status, + sortType: sortType ?? this.sortType, + errorMessage: errorMessage, + ); + } + + @override + List get props => [ + allArtists, + status, + sortType, + if (errorMessage != null) errorMessage!, + ]; +} + +enum ArtistsStatus { initial, loading, loaded, error } diff --git a/lib/features/songs/presentation/bloc/bloc.dart b/lib/features/songs/presentation/bloc/bloc.dart index 404fc04..69fda70 100644 --- a/lib/features/songs/presentation/bloc/bloc.dart +++ b/lib/features/songs/presentation/bloc/bloc.dart @@ -1,2 +1,3 @@ export 'albums_bloc.dart'; +export 'artists_bloc.dart'; export 'songs_bloc.dart'; diff --git a/lib/features/songs/presentation/bloc/songs_bloc.dart b/lib/features/songs/presentation/bloc/songs_bloc.dart index 77cf0e2..07a1ecc 100644 --- a/lib/features/songs/presentation/bloc/songs_bloc.dart +++ b/lib/features/songs/presentation/bloc/songs_bloc.dart @@ -16,11 +16,9 @@ class SongsBloc extends Bloc { this.ensureMediaPermission, this.deleteSong, this.querySongs, - this.queryArtists, this.commandManager, ) : super(const SongsState()) { on(onLoadSongs); - on(onLoadArtists); on(onDeleteSong); on(onUndoDeleteSong); on(onCanUndoChanged); @@ -31,7 +29,6 @@ class SongsBloc extends Bloc { final CommandManager commandManager; final DeleteSongWithUndo deleteSong; final QuerySongs querySongs; - final QueryArtists queryArtists; final EnsureMediaPermission ensureMediaPermission; void _onCanUndoChanged() { @@ -130,26 +127,6 @@ class SongsBloc extends Bloc { } } - Future onLoadArtists( - LoadArtistsEvent event, - Emitter emit, - ) async { - emit(const SongsState(status: SongsStatus.loading)); - final queryResult = await queryArtists(sortType: event.sortType); - if (queryResult.isSuccess) { - emit( - SongsState(allArtists: queryResult.value!, status: SongsStatus.loaded), - ); - } else { - emit( - state.copyWith( - errorMessage: queryResult.error, - status: SongsStatus.error, - ), - ); - } - } - @override Future close() { commandManager.canUndoNotifier.removeListener(_onCanUndoChanged); diff --git a/lib/features/songs/presentation/bloc/songs_event.dart b/lib/features/songs/presentation/bloc/songs_event.dart index f462af9..339f111 100644 --- a/lib/features/songs/presentation/bloc/songs_event.dart +++ b/lib/features/songs/presentation/bloc/songs_event.dart @@ -10,11 +10,6 @@ final class LoadSongsEvent extends SongsEvent { final SongsSortType sortType; } -final class LoadArtistsEvent extends SongsEvent { - const LoadArtistsEvent({this.sortType = ArtistsSortType.artist}); - final ArtistsSortType sortType; -} - final class DeleteSongEvent extends SongsEvent { const DeleteSongEvent(this.song); From 0c8dc6ccd1cc48fcf44663fc142f0ff277765ab0 Mon Sep 17 00:00:00 2001 From: TalebRafiepour Date: Thu, 11 Dec 2025 11:41:00 +0330 Subject: [PATCH 7/9] chore: initialize album view widget --- .../songs/presentation/views/albums_view.dart | 23 +++++++++++++++---- .../presentation/widgets/album_item.dart | 21 +++++++++++++++++ .../songs/presentation/widgets/widgets.dart | 1 + 3 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 lib/features/songs/presentation/widgets/album_item.dart diff --git a/lib/features/songs/presentation/views/albums_view.dart b/lib/features/songs/presentation/views/albums_view.dart index d550f86..794361b 100644 --- a/lib/features/songs/presentation/views/albums_view.dart +++ b/lib/features/songs/presentation/views/albums_view.dart @@ -1,15 +1,28 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:music_player/core/widgets/widgets.dart'; +import 'package:music_player/features/songs/presentation/bloc/bloc.dart'; +import 'package:music_player/features/songs/presentation/widgets/widgets.dart'; class AlbumsView extends StatelessWidget { const AlbumsView({super.key}); @override Widget build(BuildContext context) { - return Center( - child: Text( - 'Albums View', - style: Theme.of(context).textTheme.headlineMedium, - ), + return BlocBuilder( + builder: (context, albumState) { + if (albumState.status == AlbumsStatus.loading) { + return const Loading(); + } + + final albums = albumState.allAlbums; + return ListView.builder( + itemCount: albumState.allAlbums.length, + itemBuilder: (context, index) { + return AlbumItem(album: albums[index]); + }, + ); + }, ); } } diff --git a/lib/features/songs/presentation/widgets/album_item.dart b/lib/features/songs/presentation/widgets/album_item.dart new file mode 100644 index 0000000..7e41a5a --- /dev/null +++ b/lib/features/songs/presentation/widgets/album_item.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:music_player/core/widgets/widgets.dart'; +import 'package:music_player/features/songs/domain/entities/entities.dart'; +import 'package:on_audio_query_pluse/on_audio_query.dart'; + +class AlbumItem extends StatelessWidget { + const AlbumItem({required this.album, super.key}); + + final Album album; + + @override + Widget build(BuildContext context) { + return GlassCard( + child: ListTile( + leading: QueryArtworkWidget(id: album.id, type: ArtworkType.ALBUM), + title: Text(album.album), + subtitle: Text(album.numOfSongs.toString()), + ), + ); + } +} diff --git a/lib/features/songs/presentation/widgets/widgets.dart b/lib/features/songs/presentation/widgets/widgets.dart index 545c4b3..1f28c08 100644 --- a/lib/features/songs/presentation/widgets/widgets.dart +++ b/lib/features/songs/presentation/widgets/widgets.dart @@ -1,3 +1,4 @@ +export 'album_item.dart'; export 'category_tabbar.dart'; export 'selection_action_bar.dart'; export 'selection_more_button.dart'; From d2804cc9435b75f1573b836ff6d64b409f284db1 Mon Sep 17 00:00:00 2001 From: TalebRafiepour Date: Fri, 12 Dec 2025 22:39:38 +0330 Subject: [PATCH 8/9] chore: implement the AlbumView widget to show the list of albums --- assets/images/album_cover.png | Bin 0 -> 20780 bytes lib/core/constants/image_assets.dart | 1 + lib/core/widgets/song_image_widget.dart | 19 +++++++----- lib/core/widgets/song_item.dart | 2 +- .../presentation/pages/mini_player_page.dart | 4 +-- .../presentation/pages/music_player_page.dart | 4 +-- .../widgets/upnext_musics_sheet.dart | 4 +-- .../presentation/widgets/playlist_appbar.dart | 7 ++--- .../widgets/playlist_image_widget.dart | 5 ++-- .../widgets/recently_playlist_item.dart | 5 ++-- .../presentation/pages/add_songs_page.dart | 4 +-- .../presentation/widgets/album_item.dart | 28 ++++++++++++++++-- .../widgets/selection_song_card.dart | 2 +- pubspec.lock | 20 ++++++------- 14 files changed, 67 insertions(+), 38 deletions(-) create mode 100644 assets/images/album_cover.png diff --git a/assets/images/album_cover.png b/assets/images/album_cover.png new file mode 100644 index 0000000000000000000000000000000000000000..9ca49a1fee0ab94b76d87b2a76d0e6edf82ef9bd GIT binary patch literal 20780 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 ), leading: Hero( tag: 'song_cover_${state.currentSong?.id ?? 0}', - child: SongImageWidget( - songId: state.currentSong?.id ?? 0, + child: ArtImageWidget( + id: state.currentSong?.id ?? 0, size: 54, ), ), diff --git a/lib/features/music_plyer/presentation/pages/music_player_page.dart b/lib/features/music_plyer/presentation/pages/music_player_page.dart index c5a76fd..354abf4 100644 --- a/lib/features/music_plyer/presentation/pages/music_player_page.dart +++ b/lib/features/music_plyer/presentation/pages/music_player_page.dart @@ -105,9 +105,9 @@ class _MusicPlayerPageState extends State Hero( tag: 'song_cover_${state.currentSong?.id ?? 0}', child: Center( - child: SongImageWidget( + child: ArtImageWidget( qualitySize: 400, - songId: state.currentSong?.id ?? 0, + id: state.currentSong?.id ?? 0, size: MediaQuery.of(context).size.height * 0.35, ), ), diff --git a/lib/features/music_plyer/presentation/widgets/upnext_musics_sheet.dart b/lib/features/music_plyer/presentation/widgets/upnext_musics_sheet.dart index 60f2bf9..b460a63 100644 --- a/lib/features/music_plyer/presentation/widgets/upnext_musics_sheet.dart +++ b/lib/features/music_plyer/presentation/widgets/upnext_musics_sheet.dart @@ -333,8 +333,8 @@ class _ExpandedHeader extends StatelessWidget { ), Hero( tag: 'song_cover_${currentSong.id}', - child: SongImageWidget( - songId: currentSong.id, + child: ArtImageWidget( + id: currentSong.id, size: MediaQuery.of(context).size.width * 0.2, ), ), diff --git a/lib/features/playlist/presentation/widgets/playlist_appbar.dart b/lib/features/playlist/presentation/widgets/playlist_appbar.dart index 8181add..d95a054 100644 --- a/lib/features/playlist/presentation/widgets/playlist_appbar.dart +++ b/lib/features/playlist/presentation/widgets/playlist_appbar.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; import 'package:music_player/core/constants/image_assets.dart'; -import 'package:music_player/core/domain/enums/enums.dart'; import 'package:music_player/extensions/extensions.dart'; -import 'package:music_player/features/songs/presentation/constants/constants.dart'; +import 'package:music_player/features/songs/domain/enums/enums.dart'; typedef OnSortSongsCallback = void Function(SongsSortType); @@ -35,8 +34,8 @@ class PlaylistAppbar extends StatelessWidget implements PreferredSizeWidget { child: Image.asset( ImageAssets.search, color: isDark ? Colors.white : Colors.black, - height: SongsPageConstants.searchIconSize, - width: SongsPageConstants.searchIconSize, + height: 18, + width: 18, ), ), ), diff --git a/lib/features/playlist/presentation/widgets/playlist_image_widget.dart b/lib/features/playlist/presentation/widgets/playlist_image_widget.dart index 7f895b1..2965ed5 100644 --- a/lib/features/playlist/presentation/widgets/playlist_image_widget.dart +++ b/lib/features/playlist/presentation/widgets/playlist_image_widget.dart @@ -22,9 +22,10 @@ class PlaylistImageWidget extends StatelessWidget { final coverSongId = state.playlistCoverSongIds[playlistId]; final artworkId = coverSongId ?? playlistId; - return SongImageWidget( - songId: artworkId, + return ArtImageWidget( + id: artworkId, defaultCover: ImageAssets.playlistCover, + defaultCoverBg: Colors.white, borderRadius: borderRadius, ); }, diff --git a/lib/features/playlist/presentation/widgets/recently_playlist_item.dart b/lib/features/playlist/presentation/widgets/recently_playlist_item.dart index 65d39f1..5b2e137 100644 --- a/lib/features/playlist/presentation/widgets/recently_playlist_item.dart +++ b/lib/features/playlist/presentation/widgets/recently_playlist_item.dart @@ -35,10 +35,11 @@ class PinnedPlaylistItem extends StatelessWidget { return GestureDetector( onTap: onTap, - child: SongImageWidget( - songId: artworkId, + child: ArtImageWidget( + id: artworkId, size: size, borderRadius: borderRadius, + defaultCoverBg: Colors.white, defaultCover: ImageAssets.playlistCover, ), ); diff --git a/lib/features/songs/presentation/pages/add_songs_page.dart b/lib/features/songs/presentation/pages/add_songs_page.dart index 6b4d248..61823c2 100644 --- a/lib/features/songs/presentation/pages/add_songs_page.dart +++ b/lib/features/songs/presentation/pages/add_songs_page.dart @@ -82,8 +82,8 @@ class _AddSongsPageState extends State { secondary: SizedBox( width: 54, height: 54, - child: SongImageWidget( - songId: song.id, + child: ArtImageWidget( + id: song.id, size: 54, borderRadius: 8, ), diff --git a/lib/features/songs/presentation/widgets/album_item.dart b/lib/features/songs/presentation/widgets/album_item.dart index 7e41a5a..e4d677c 100644 --- a/lib/features/songs/presentation/widgets/album_item.dart +++ b/lib/features/songs/presentation/widgets/album_item.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:music_player/core/constants/image_assets.dart'; import 'package:music_player/core/widgets/widgets.dart'; +import 'package:music_player/extensions/extensions.dart'; import 'package:music_player/features/songs/domain/entities/entities.dart'; import 'package:on_audio_query_pluse/on_audio_query.dart'; @@ -10,11 +12,31 @@ class AlbumItem extends StatelessWidget { @override Widget build(BuildContext context) { + final songLocalized = album.numOfSongs > 1 + ? context.localization.songs + : context.localization.song; return GlassCard( + margin: const EdgeInsets.all(12), + borderRadius: const BorderRadius.all(Radius.circular(12)), child: ListTile( - leading: QueryArtworkWidget(id: album.id, type: ArtworkType.ALBUM), - title: Text(album.album), - subtitle: Text(album.numOfSongs.toString()), + leading: ArtImageWidget( + id: album.id, + type: ArtworkType.ALBUM, + defaultCover: ImageAssets.albumCover, + ), + title: Text( + album.album, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + subtitle: Text( + '${album.numOfSongs} $songLocalized', + style: context.theme.textTheme.labelMedium, + ), + trailing: const Icon(Icons.arrow_right), ), ); } diff --git a/lib/features/songs/presentation/widgets/selection_song_card.dart b/lib/features/songs/presentation/widgets/selection_song_card.dart index 78fb143..249e62c 100644 --- a/lib/features/songs/presentation/widgets/selection_song_card.dart +++ b/lib/features/songs/presentation/widgets/selection_song_card.dart @@ -22,7 +22,7 @@ class SelectionSongCard extends StatelessWidget { ), child: Row( children: [ - SongImageWidget(songId: song.id, size: 54), + ArtImageWidget(id: song.id, size: 54), const SizedBox(width: 8), Expanded( child: Column( diff --git a/pubspec.lock b/pubspec.lock index 6e5e2c6..65e43f7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -553,26 +553,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "11.0.2" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.10" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.1" logging: dependency: transitive description: @@ -1174,10 +1174,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.4" timing: dependency: transitive description: @@ -1278,10 +1278,10 @@ packages: dependency: transitive description: name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.1.4" very_good_analysis: dependency: "direct dev" description: From e1d03f15ac4eeb6ad13b6aafb601dc9e81340aac Mon Sep 17 00:00:00 2001 From: TalebRafiepour Date: Fri, 12 Dec 2025 23:45:59 +0330 Subject: [PATCH 9/9] chore: show list of artist in the ArtistsView widget --- assets/images/artist_cover.png | Bin 0 -> 29502 bytes lib/core/constants/image_assets.dart | 1 + lib/core/widgets/song_image_widget.dart | 2 +- .../songs/presentation/pages/songs_page.dart | 2 +- .../songs/presentation/views/albums_view.dart | 7 +++ .../presentation/views/artists_view.dart | 31 +++++++++++-- .../presentation/widgets/artist_item.dart | 43 ++++++++++++++++++ .../presentation/widgets/category_tabbar.dart | 2 +- .../songs/presentation/widgets/widgets.dart | 1 + 9 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 assets/images/artist_cover.png create mode 100644 lib/features/songs/presentation/widgets/artist_item.dart diff --git a/assets/images/artist_cover.png b/assets/images/artist_cover.png new file mode 100644 index 0000000000000000000000000000000000000000..2268c3bb2b09a1ff506be8b8d10c072787844a8b GIT binary patch literal 29502 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 tabController.animateTo(newPageIndex); }, children: const [ + AllSongsView(), AlbumsView(), ArtistsView(), - AllSongsView(), ], ), ), diff --git a/lib/features/songs/presentation/views/albums_view.dart b/lib/features/songs/presentation/views/albums_view.dart index 794361b..553666d 100644 --- a/lib/features/songs/presentation/views/albums_view.dart +++ b/lib/features/songs/presentation/views/albums_view.dart @@ -15,7 +15,14 @@ class AlbumsView extends StatelessWidget { return const Loading(); } + if (albumState.status == AlbumsStatus.error) { + return const SongsErrorLoading(); + } + final albums = albumState.allAlbums; + if (albums.isEmpty) { + return const NoSongsWidget(); + } return ListView.builder( itemCount: albumState.allAlbums.length, itemBuilder: (context, index) { diff --git a/lib/features/songs/presentation/views/artists_view.dart b/lib/features/songs/presentation/views/artists_view.dart index 4d21744..1e7350d 100644 --- a/lib/features/songs/presentation/views/artists_view.dart +++ b/lib/features/songs/presentation/views/artists_view.dart @@ -1,15 +1,36 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:music_player/core/widgets/widgets.dart'; +import 'package:music_player/features/songs/presentation/bloc/bloc.dart'; +import 'package:music_player/features/songs/presentation/widgets/widgets.dart'; class ArtistsView extends StatelessWidget { const ArtistsView({super.key}); @override Widget build(BuildContext context) { - return Center( - child: Text( - 'Artists View', - style: Theme.of(context).textTheme.headlineMedium, - ), + return BlocBuilder( + builder: (context, artistsState) { + if (artistsState.status == ArtistsStatus.loading) { + return const Loading(); + } + + if (artistsState.status == ArtistsStatus.error) { + return const SongsErrorLoading(); + } + + final artists = artistsState.allArtists; + if (artists.isEmpty) { + return const NoSongsWidget(); + } + + return ListView.builder( + itemCount: artistsState.allArtists.length, + itemBuilder: (context, index) { + return ArtistItem(artist: artists[index]); + }, + ); + }, ); } } diff --git a/lib/features/songs/presentation/widgets/artist_item.dart b/lib/features/songs/presentation/widgets/artist_item.dart new file mode 100644 index 0000000..3f74ab6 --- /dev/null +++ b/lib/features/songs/presentation/widgets/artist_item.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:music_player/core/constants/image_assets.dart'; +import 'package:music_player/core/widgets/widgets.dart'; +import 'package:music_player/extensions/extensions.dart'; +import 'package:music_player/features/songs/domain/entities/entities.dart'; +import 'package:on_audio_query_pluse/on_audio_query.dart'; + +class ArtistItem extends StatelessWidget { + const ArtistItem({required this.artist, super.key}); + + final Artist artist; + + @override + Widget build(BuildContext context) { + final songLocalized = (artist.numberOfTracks ?? 0) > 1 + ? context.localization.songs + : context.localization.song; + return GlassCard( + margin: const EdgeInsets.all(12), + borderRadius: const BorderRadius.all(Radius.circular(12)), + child: ListTile( + leading: ArtImageWidget( + id: artist.id, + type: ArtworkType.ARTIST, + defaultCover: ImageAssets.artistCover, + ), + title: Text( + artist.artist, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + subtitle: Text( + '${artist.numberOfTracks ?? 0} $songLocalized', + style: context.theme.textTheme.labelMedium, + ), + trailing: const Icon(Icons.arrow_right), + ), + ); + } +} diff --git a/lib/features/songs/presentation/widgets/category_tabbar.dart b/lib/features/songs/presentation/widgets/category_tabbar.dart index d9d8b7e..97c87d5 100644 --- a/lib/features/songs/presentation/widgets/category_tabbar.dart +++ b/lib/features/songs/presentation/widgets/category_tabbar.dart @@ -15,9 +15,9 @@ class CategoryTabbar extends StatelessWidget { Widget build(BuildContext context) { final theme = context.theme; final labels = [ + context.localization.allSongs, context.localization.albums, context.localization.artists, - context.localization.allSongs, ]; return TabBar( diff --git a/lib/features/songs/presentation/widgets/widgets.dart b/lib/features/songs/presentation/widgets/widgets.dart index 1f28c08..4725965 100644 --- a/lib/features/songs/presentation/widgets/widgets.dart +++ b/lib/features/songs/presentation/widgets/widgets.dart @@ -1,4 +1,5 @@ export 'album_item.dart'; +export 'artist_item.dart'; export 'category_tabbar.dart'; export 'selection_action_bar.dart'; export 'selection_more_button.dart';