Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 55 additions & 37 deletions lib/providers/overview_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,30 @@ class OverviewProvider with ChangeNotifier, StreamSubscriber {
final mostPlayedSongs = <Playable>[];
final recentlyAddedSongs = <Playable>[];
final recentlyPlayedSongs = <Playable>[];
final recentlyAddedAlbums = <Album>[];
final leastPlayedSongs = <Playable>[];
final randomSongs = <Playable>[];
final similarSongs = <Playable>[];
final mostPlayedAlbums = <Album>[];
final recentlyAddedAlbums = <Album>[];
final randomAlbums = <Album>[];
final mostPlayedArtists = <Artist>[];
final recentlyAddedArtists = <Artist>[];
final randomArtists = <Artist>[];

late final List<List<dynamic>> _allSections = [
mostPlayedSongs,
recentlyAddedSongs,
recentlyPlayedSongs,
leastPlayedSongs,
randomSongs,
similarSongs,
mostPlayedAlbums,
recentlyAddedAlbums,
randomAlbums,
mostPlayedArtists,
recentlyAddedArtists,
randomArtists,
];

OverviewProvider({
required playableProvider,
Expand All @@ -27,58 +48,55 @@ class OverviewProvider with ChangeNotifier, StreamSubscriber {
_artistProvider = artistProvider,
_recentlyPlayedProvider = recentlyPlayedProvider {
subscribe(AuthProvider.userLoggedOutStream.listen((_) {
mostPlayedSongs.clear();
recentlyAddedSongs.clear();
recentlyPlayedSongs.clear();
mostPlayedAlbums.clear();
mostPlayedArtists.clear();

for (final section in _allSections) section.clear();
notifyListeners();
}));
}

bool get isEmpty =>
mostPlayedSongs.isEmpty &&
recentlyAddedSongs.isEmpty &&
recentlyPlayedSongs.isEmpty &&
mostPlayedAlbums.isEmpty &&
mostPlayedArtists.isEmpty;
bool get isEmpty => _allSections.every((section) => section.isEmpty);

Future<void> refresh() async {
final Map<String, dynamic> response = await get('overview');

mostPlayedSongs
..clear()
..addAll(_playableProvider.parseFromJson(response['most_played_songs']));
_fill(mostPlayedSongs, _parseSongs(response['most_played_songs']));
_fill(recentlyAddedSongs, _parseSongs(response['recently_added_songs']));
_fill(recentlyPlayedSongs, _parseSongs(response['recently_played_songs']));
_fill(leastPlayedSongs, _parseSongs(response['least_played_songs']));
_fill(randomSongs, _parseSongs(response['random_songs']));
_fill(similarSongs, _parseSongs(response['similar_songs']));

recentlyAddedSongs
..clear()
..addAll(
_playableProvider.parseFromJson(response['recently_added_songs']));
_fill(mostPlayedAlbums, _parseAlbums(response['most_played_albums']));
_fill(recentlyAddedAlbums, _parseAlbums(response['recently_added_albums']));
_fill(randomAlbums, _parseAlbums(response['random_albums']));

recentlyPlayedSongs
..clear()
..addAll(
_playableProvider.parseFromJson(response['recently_played_songs']));
_fill(mostPlayedArtists, _parseArtists(response['most_played_artists']));
_fill(recentlyAddedArtists,
_parseArtists(response['recently_added_artists']));
_fill(randomArtists, _parseArtists(response['random_artists']));

_recentlyPlayedProvider.seed(recentlyPlayedSongs);

final _mostPlayedAlbums = response['most_played_albums']
.map<Album>((j) => Album.fromJson(j))
.toList();
notifyListeners();
}

mostPlayedAlbums
..clear()
..addAll(_albumProvider.syncWithVault(_mostPlayedAlbums));
List<Playable> _parseSongs(dynamic json) =>
_playableProvider.parseFromJson(json ?? const []);

final _mostPlayedArtist = response['most_played_artists']
.map<Artist>((j) => Artist.fromJson(j))
.toList();
List<Album> _parseAlbums(dynamic json) => _albumProvider.syncWithVault(
((json ?? const []) as List)
.map<Album>((j) => Album.fromJson(j))
.toList(),
);

mostPlayedArtists
..clear()
..addAll(_artistProvider.syncWithVault(_mostPlayedArtist));
List<Artist> _parseArtists(dynamic json) => _artistProvider.syncWithVault(
((json ?? const []) as List)
.map<Artist>((j) => Artist.fromJson(j))
.toList(),
);

notifyListeners();
void _fill<T>(List<T> target, List<T> source) {
target
..clear()
..addAll(source);
}
}
116 changes: 71 additions & 45 deletions lib/ui/screens/home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,62 +46,88 @@ class _HomeScreenState extends State<HomeScreen> {
}
}

Widget _songBlock(String heading, List<Playable> songs) {
return HorizontalCardScroller(
headingText: heading,
cards: <Widget>[
...songs.map((playable) => SongCard(playable: playable)),
PlaceholderCard(
icon: CupertinoIcons.music_note,
onPressed: () => Navigator.of(context).push(
CupertinoPageRoute(builder: (_) => SongsScreen()),
),
),
],
);
}

Widget _albumBlock(String heading, List<Album> albums) {
return HorizontalCardScroller(
headingText: heading,
cards: <Widget>[
...albums.map((album) => AlbumCard(album: album)),
PlaceholderCard(
icon: CupertinoIcons.music_albums,
onPressed: () => Navigator.of(context).push(
CupertinoPageRoute(builder: (_) => AlbumsScreen()),
),
),
],
);
}

Widget _artistBlock(String heading, List<Artist> artists) {
return HorizontalCardScroller(
headingText: heading,
cards: <Widget>[
...artists.map((artist) => ArtistCard(artist: artist)),
PlaceholderCard(
icon: CupertinoIcons.music_mic,
circular: true,
onPressed: () => Navigator.of(context).push(
CupertinoPageRoute(builder: (_) => const ArtistsScreen()),
),
),
],
);
}

@override
Widget build(BuildContext context) {
return Consumer<OverviewProvider>(
builder: (_, overviewProvider, __) {
if (_loading) return const HomeScreenPlaceholder();
if (_errored) return OopsBox(onRetry: fetchData);

final op = overviewProvider;
final blocks = <Widget>[
if (overviewProvider.mostPlayedSongs.isNotEmpty)
HorizontalCardScroller(
headingText: 'Most played',
cards: <Widget>[
...overviewProvider.mostPlayedSongs
.map((playable) => SongCard(playable: playable)),
PlaceholderCard(
icon: CupertinoIcons.music_note,
onPressed: () => Navigator.of(context).push(
CupertinoPageRoute(builder: (_) => SongsScreen()),
),
),
],
),
if (overviewProvider.mostPlayedAlbums.isNotEmpty)
HorizontalCardScroller(
headingText: 'Top albums',
cards: <Widget>[
...overviewProvider.mostPlayedAlbums
.map((album) => AlbumCard(album: album)),
PlaceholderCard(
icon: CupertinoIcons.music_albums,
onPressed: () => Navigator.of(context).push(
CupertinoPageRoute(builder: (_) => AlbumsScreen()),
),
),
],
),
if (overviewProvider.mostPlayedArtists.isNotEmpty)
HorizontalCardScroller(
headingText: 'Top artists',
cards: <Widget>[
...overviewProvider.mostPlayedArtists
.map((artist) => ArtistCard(artist: artist)),
PlaceholderCard(
icon: CupertinoIcons.music_mic,
circular: true,
onPressed: () => Navigator.of(context).push(
CupertinoPageRoute(builder: (_) => const ArtistsScreen()),
),
),
],
),
if (op.recentlyAddedAlbums.isNotEmpty)
_albumBlock('Latest Albums', op.recentlyAddedAlbums),
if (op.similarSongs.isNotEmpty)
_songBlock('You Might Also Like', op.similarSongs),
if (op.mostPlayedAlbums.isNotEmpty)
_albumBlock('Top Albums', op.mostPlayedAlbums),
if (op.mostPlayedSongs.isNotEmpty)
_songBlock('Most Played', op.mostPlayedSongs),
if (op.mostPlayedArtists.isNotEmpty)
_artistBlock('Top Artists', op.mostPlayedArtists),
if (op.recentlyAddedSongs.isNotEmpty)
_songBlock('New Songs', op.recentlyAddedSongs),
if (op.recentlyAddedArtists.isNotEmpty)
_artistBlock('New Artists', op.recentlyAddedArtists),
if (op.leastPlayedSongs.isNotEmpty)
_songBlock('Hidden Gems', op.leastPlayedSongs),
if (op.randomSongs.isNotEmpty)
_songBlock('Random Songs', op.randomSongs),
if (op.randomAlbums.isNotEmpty)
_albumBlock('Random Albums', op.randomAlbums),
if (op.randomArtists.isNotEmpty)
_artistBlock('Random Artists', op.randomArtists),
]
.map(
(widget) => Padding(
(block) => Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: widget,
child: block,
),
)
.toList();
Expand Down
121 changes: 121 additions & 0 deletions test/providers/overview_provider_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import 'package:app/models/song.dart';
import 'package:app/providers/album_provider.dart';
import 'package:app/providers/artist_provider.dart';
import 'package:app/providers/overview_provider.dart';
import 'package:app/providers/playable_provider.dart';
import 'package:app/providers/recently_played_provider.dart';
import 'package:flutter_test/flutter_test.dart';

import '../helpers/api_test_setup.dart';

void main() {
late OverviewProvider overview;
late CapturingClient client;
var _seq = 0;

List<Map<String, dynamic>> songs(int count) =>
List.generate(count, (_) => Song.fake().toJson());

List<Map<String, dynamic>> albums(int count) => List.generate(count, (_) {
final id = 'album-${_seq++}';
return {
'id': id,
'name': 'Album $id',
'cover': null,
'artist_id': 'artist-$id',
'artist_name': 'Artist $id',
'year': null,
};
});

List<Map<String, dynamic>> artists(int count) => List.generate(count, (_) {
final id = 'artist-${_seq++}';
return {'id': id, 'name': 'Artist $id', 'image': null};
});

setUpAll(() async => await initApiTestEnvironment());

setUp(() {
_seq = 0;
final playableProvider = PlayableProvider();
overview = OverviewProvider(
playableProvider: playableProvider,
albumProvider: AlbumProvider(),
artistProvider: ArtistProvider(),
recentlyPlayedProvider:
RecentlyPlayedProvider(playableProvider: playableProvider),
);
client = CapturingClient();
client.install();
setUpApiTest();
});

tearDown(tearDownApiTest);

test('refresh populates every overview section', () async {
client.willReturn(json: {
'most_played_songs': songs(3),
'recently_added_songs': songs(2),
'recently_played_songs': songs(4),
'least_played_songs': songs(1),
'random_songs': songs(5),
'similar_songs': songs(6),
'most_played_albums': albums(3),
'recently_added_albums': albums(2),
'random_albums': albums(4),
'most_played_artists': artists(3),
'recently_added_artists': artists(2),
'random_artists': artists(4),
});

await overview.refresh();

expect(overview.mostPlayedSongs, hasLength(3));
expect(overview.recentlyAddedSongs, hasLength(2));
expect(overview.recentlyPlayedSongs, hasLength(4));
expect(overview.leastPlayedSongs, hasLength(1));
expect(overview.randomSongs, hasLength(5));
expect(overview.similarSongs, hasLength(6));
expect(overview.mostPlayedAlbums, hasLength(3));
expect(overview.recentlyAddedAlbums, hasLength(2));
expect(overview.randomAlbums, hasLength(4));
expect(overview.mostPlayedArtists, hasLength(3));
expect(overview.recentlyAddedArtists, hasLength(2));
expect(overview.randomArtists, hasLength(4));
});

test('refresh leaves sections empty when an older API omits them', () async {
client.willReturn(json: {
'most_played_songs': songs(2),
'recently_played_songs': songs(1),
'most_played_albums': albums(2),
'most_played_artists': artists(2),
});

await overview.refresh();

expect(overview.mostPlayedSongs, hasLength(2));
expect(overview.recentlyPlayedSongs, hasLength(1));
expect(overview.mostPlayedAlbums, hasLength(2));
expect(overview.mostPlayedArtists, hasLength(2));

expect(overview.recentlyAddedSongs, isEmpty);
expect(overview.leastPlayedSongs, isEmpty);
expect(overview.randomSongs, isEmpty);
expect(overview.similarSongs, isEmpty);
expect(overview.recentlyAddedAlbums, isEmpty);
expect(overview.randomAlbums, isEmpty);
expect(overview.recentlyAddedArtists, isEmpty);
expect(overview.randomArtists, isEmpty);

expect(overview.isEmpty, isFalse);
});

test('isEmpty is true when the API returns nothing', () async {
client.willReturn(json: {});

await overview.refresh();

expect(overview.isEmpty, isTrue);
});
}
Loading