diff --git a/assets/images/album_cover.png b/assets/images/album_cover.png new file mode 100644 index 0000000..9ca49a1 Binary files /dev/null and b/assets/images/album_cover.png differ diff --git a/assets/images/artist_cover.png b/assets/images/artist_cover.png new file mode 100644 index 0000000..2268c3b Binary files /dev/null and b/assets/images/artist_cover.png differ diff --git a/lib/app.dart b/lib/app.dart index e992323..efea311 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -59,6 +59,20 @@ class _MusicPlayerAppState extends State { FlutterNativeSplash.remove(); return MultiBlocProvider( providers: [ + BlocProvider( + create: (_) => AlbumsBloc(getIt()) + ..add( + const LoadAlbumsEvent(), + ), + ), + BlocProvider( + create: (_) => + ArtistsBloc( + getIt(), + )..add( + const LoadArtistsEvent(), + ), + ), BlocProvider( create: (_) => SongsBloc( getIt(), 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/constants/image_assets.dart b/lib/core/constants/image_assets.dart index 853d61f..8e59c54 100644 --- a/lib/core/constants/image_assets.dart +++ b/lib/core/constants/image_assets.dart @@ -2,6 +2,8 @@ sealed class ImageAssets { static const String logo = 'assets/logo/app_logo.png'; static const String logoBranding = 'assets/logo/logo_branding.png'; static const String songCover = 'assets/images/song_cover.png'; + static const String albumCover = 'assets/images/album_cover.png'; + static const String artistCover = 'assets/images/artist_cover.png'; static const String emptySongs = 'assets/images/empty_songs.png'; static const String errorLoadSongs = 'assets/images/error_load_songs.png'; static const String emptyPlaylists = 'assets/images/empty_playlist.png'; 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/core/domain/enums/songs_sort_type.dart b/lib/core/domain/enums/songs_sort_type.dart deleted file mode 100644 index 8efe74c..0000000 --- a/lib/core/domain/enums/songs_sort_type.dart +++ /dev/null @@ -1,6 +0,0 @@ -enum SongsSortType { - recentlyAdded, - dateAdded, - duration, - size, -} 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/core/widgets/song_image_widget.dart b/lib/core/widgets/song_image_widget.dart index 6b2b783..778f512 100644 --- a/lib/core/widgets/song_image_widget.dart +++ b/lib/core/widgets/song_image_widget.dart @@ -1,13 +1,16 @@ import 'package:flutter/material.dart'; import 'package:music_player/core/constants/constants.dart'; +import 'package:music_player/extensions/extensions.dart'; import 'package:on_audio_query_pluse/on_audio_query.dart'; -class SongImageWidget extends StatelessWidget { - const SongImageWidget({ - required this.songId, +class ArtImageWidget extends StatelessWidget { + const ArtImageWidget({ + required this.id, + this.type = ArtworkType.AUDIO, this.size = 50, this.quality = 70, this.qualitySize = 200, + this.defaultCoverBg, this.artworkQuality = FilterQuality.medium, this.borderRadius, this.artworkFit = BoxFit.cover, @@ -15,7 +18,8 @@ class SongImageWidget extends StatelessWidget { super.key, }); - final int songId; + final int id; + final ArtworkType type; final double size; final double? borderRadius; final int quality; @@ -23,13 +27,14 @@ class SongImageWidget extends StatelessWidget { final FilterQuality artworkQuality; final BoxFit artworkFit; final String defaultCover; + final Color? defaultCoverBg; @override Widget build(BuildContext context) { return QueryArtworkWidget( - id: songId, + id: id, quality: quality, - type: ArtworkType.AUDIO, + type: type, size: qualitySize, artworkWidth: size, artworkHeight: size, @@ -40,10 +45,10 @@ class SongImageWidget extends StatelessWidget { nullArtworkWidget: ClipRRect( borderRadius: BorderRadius.circular(borderRadius ?? (size / 2)), child: ColoredBox( - color: Colors.white, + color: defaultCoverBg ?? context.theme.scaffoldBackgroundColor, child: Image.asset( defaultCover, - fit: BoxFit.cover, + fit: artworkFit, width: size, height: size, ), diff --git a/lib/core/widgets/song_item.dart b/lib/core/widgets/song_item.dart index 8ab36de..5d4d392 100644 --- a/lib/core/widgets/song_item.dart +++ b/lib/core/widgets/song_item.dart @@ -59,7 +59,7 @@ class SongItem extends StatelessWidget { // Common row content used by both blurred and plain variants final content = Row( children: [ - SongImageWidget(songId: track.id, size: songImageSize), + ArtImageWidget(id: track.id, size: songImageSize), const SizedBox(width: 12), Expanded(child: _buildTitleAndArtist(context)), const SizedBox(width: 8), 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/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/music_plyer/presentation/pages/mini_player_page.dart b/lib/features/music_plyer/presentation/pages/mini_player_page.dart index 477f063..55cab59 100644 --- a/lib/features/music_plyer/presentation/pages/mini_player_page.dart +++ b/lib/features/music_plyer/presentation/pages/mini_player_page.dart @@ -153,8 +153,8 @@ class _MiniPlayerPageState extends State ), 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/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/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/features/songs/domain/enums/songs_sort_type.dart b/lib/features/songs/domain/enums/songs_sort_type.dart new file mode 100644 index 0000000..f50fff2 --- /dev/null +++ b/lib/features/songs/domain/enums/songs_sort_type.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart' show BuildContext, IconData, Icons; +import 'package:music_player/extensions/extensions.dart'; + +enum SongsSortType { + dateAdded, + title, + artist, + album, + 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/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/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/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 7c1cf6a..69fda70 100644 --- a/lib/features/songs/presentation/bloc/bloc.dart +++ b/lib/features/songs/presentation/bloc/bloc.dart @@ -1 +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 aaab630..07a1ecc 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'; @@ -18,7 +19,6 @@ class SongsBloc extends Bloc { this.commandManager, ) : super(const SongsState()) { on(onLoadSongs); - on(onSortSongs); on(onDeleteSong); on(onUndoDeleteSong); on(onCanUndoChanged); @@ -107,17 +107,13 @@ 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(SongsEvent event, Emitter emit) async { + Future onLoadSongs( + LoadSongsEvent event, + Emitter emit, + ) async { emit(const SongsState(status: SongsStatus.loading)); - final queryResult = await querySongs(); + final queryResult = await querySongs(sortType: event.sortType); if (queryResult.isSuccess) { - _sortSongs(queryResult.value!, state.sortType); emit( SongsState(allSongs: queryResult.value!, status: SongsStatus.loaded), ); @@ -131,19 +127,6 @@ 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)); - } - } - @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 f36aede..339f111 100644 --- a/lib/features/songs/presentation/bloc/songs_event.dart +++ b/lib/features/songs/presentation/bloc/songs_event.dart @@ -6,12 +6,7 @@ sealed class SongsEvent { } final class LoadSongsEvent extends SongsEvent { - const LoadSongsEvent(); -} - -final class SortSongsEvent extends SongsEvent { - const SortSongsEvent(this.sortType); - + const LoadSongsEvent({this.sortType = SongsSortType.dateAdded}); final SongsSortType sortType; } 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/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/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/pages/songs_page.dart b/lib/features/songs/presentation/pages/songs_page.dart index a66ef4b..6f129ad 100644 --- a/lib/features/songs/presentation/pages/songs_page.dart +++ b/lib/features/songs/presentation/pages/songs_page.dart @@ -1,18 +1,6 @@ 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/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/views.dart'; import 'package:music_player/features/songs/presentation/widgets/songs_appbar.dart'; import 'package:music_player/features/songs/presentation/widgets/widgets.dart'; @@ -26,44 +14,58 @@ class SongsPage extends StatefulWidget { } class _SongsPageState extends State - with - SongSharingMixin, - RingtoneMixin, - PlaylistManagementMixin, - SongDeletionMixin, - ToggleLikeMixin { - // Getters for BLoCs - SongsBloc get _songsBloc => context.read(); - MusicPlayerBloc get _musicPlayerBloc => context.read(); - + with SingleTickerProviderStateMixin { + late final TabController tabController; + late final PageController pageController; @override void initState() { + tabController = TabController(length: 3, vsync: this); + pageController = PageController(); 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(), - ), + 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( - numOfSongs: songsState.allSongs.length, - onSearchButtonPressed: _onSearchButtonPressed, + return Scaffold( + appBar: SongsAppbar( + onSearchButtonPressed: _onSearchButtonPressed, + ), + body: Column( + children: [ + CategoryTabbar( + tabController: tabController, + onTabChanged: animateToNewPage, ), - body: _buildSongsContent(songsState), - ); - }, + Expanded( + child: PageView( + controller: pageController, + onPageChanged: (newPageIndex) { + tabController.animateTo(newPageIndex); + }, + children: const [ + AllSongsView(), + AlbumsView(), + ArtistsView(), + ], + ), + ), + ], + ), ); } @@ -74,124 +76,4 @@ 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) { - 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 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, - ), - - 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), - ], - ); - }, - ); - } - - void _onSortSongs(SongsSortType sortType) { - _songsBloc.add(SortSongsEvent(sortType)); - } } 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..553666d --- /dev/null +++ b/lib/features/songs/presentation/views/albums_view.dart @@ -0,0 +1,35 @@ +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 BlocBuilder( + builder: (context, albumState) { + if (albumState.status == AlbumsStatus.loading) { + 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) { + return AlbumItem(album: albums[index]); + }, + ); + }, + ); + } +} 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..1e7350d --- /dev/null +++ b/lib/features/songs/presentation/views/artists_view.dart @@ -0,0 +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 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/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/album_item.dart b/lib/features/songs/presentation/widgets/album_item.dart new file mode 100644 index 0000000..e4d677c --- /dev/null +++ b/lib/features/songs/presentation/widgets/album_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 AlbumItem extends StatelessWidget { + const AlbumItem({required this.album, super.key}); + + final Album album; + + @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: 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/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 new file mode 100644 index 0000000..97c87d5 --- /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.allSongs, + context.localization.albums, + context.localization.artists, + ]; + + 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/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/lib/features/songs/presentation/widgets/songs_appbar.dart b/lib/features/songs/presentation/widgets/songs_appbar.dart index 5bf8adf..fb80c22 100644 --- a/lib/features/songs/presentation/widgets/songs_appbar.dart +++ b/lib/features/songs/presentation/widgets/songs_appbar.dart @@ -1,18 +1,15 @@ 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/presentation/constants/constants.dart'; +import 'package:music_player/features/songs/domain/enums/enums.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 +20,7 @@ class SongsAppbar extends StatelessWidget implements PreferredSizeWidget { return AppBar( elevation: 0, title: Text( - context.localization.allSongs(numOfSongs), + context.localization.appTitle, ), centerTitle: true, actions: [ @@ -32,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/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/sort_type_ruler.dart b/lib/features/songs/presentation/widgets/sort_type_ruler.dart deleted file mode 100644 index 88e5467..0000000 --- a/lib/features/songs/presentation/widgets/sort_type_ruler.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:music_player/core/domain/enums/enums.dart'; -import 'package:music_player/extensions/extensions.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.recentlyAdded, - SongsSortType.dateAdded, - SongsSortType.duration, - SongsSortType.size, - ]; - final labels = [ - context.localization.recent, - context.localization.dateAdded, - context.localization.duration, - context.localization.size, - ]; - - 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 1d6bc2f..4725965 100644 --- a/lib/features/songs/presentation/widgets/widgets.dart +++ b/lib/features/songs/presentation/widgets/widgets.dart @@ -1,4 +1,8 @@ +export 'album_item.dart'; +export 'artist_item.dart'; +export 'category_tabbar.dart'; export 'selection_action_bar.dart'; export 'selection_more_button.dart'; export 'selection_song_card.dart'; -export 'sort_type_ruler.dart'; +export 'songs_filter_bottom_sheet.dart'; +export 'sort_button.dart'; 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..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", @@ -73,6 +65,11 @@ "dateAdded": "Date Added", "duration": "Duration", "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 9d5e3ed..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": "در هنگام باگزاری موزیک ها خطایی رخ داده است", @@ -70,6 +62,11 @@ "dateAdded": "تاریخ افزدون", "duration": "مدت زمان", "size": "اندازه", + "title": "عنوان", + "album": "آلبوم", + "albums": "آلبوم ها", + "artist": "هنرمند", + "artists": "هنرمندان", "ascending": "افزایشی", "descending": "کاهشی", "dismiss": "رد کردن", diff --git a/lib/localization/app_localizations.dart b/lib/localization/app_localizations.dart index df79660..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. /// @@ -392,6 +392,36 @@ 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 @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 0c9cbf6..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'; @@ -160,6 +158,21 @@ class AppLocalizationsEn extends AppLocalizations { @override String get size => 'Size'; + @override + String get title => 'Title'; + + @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 c2eb6da..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 => 'جستجوی موزیک ها'; @@ -160,6 +158,21 @@ class AppLocalizationsFa extends AppLocalizations { @override String get size => 'اندازه'; + @override + String get title => 'عنوان'; + + @override + String get album => 'آلبوم'; + + @override + String get albums => 'آلبوم ها'; + + @override + String get artist => 'هنرمند'; + + @override + String get artists => 'هنرمندان'; + @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); 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: