Skip to content
Snippets Groups Projects
Commit f09c06f8 authored by Benoît Harrault's avatar Benoît Harrault
Browse files

Merge branch '53-show-last-discovered-artists-and-tracks' into 'master'

Resolve "Show last discovered artists and tracks"

Closes #53

See merge request !50
parents 5d22b55c a58945e9
Branches 54-improve-discoveries-page
Tags Release_0.0.49_49
1 merge request!50Resolve "Show last discovered artists and tracks"
Pipeline #4747 passed
Showing
with 461 additions and 3 deletions
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
app.versionName=0.0.48
app.versionCode=48
app.versionName=0.0.49
app.versionCode=49
......@@ -22,6 +22,8 @@
"discoveries_title": "Discoveries ({daysCount} days)",
"discoveries_artists_title": "Artists",
"discoveries_tracks_title": "Tracks",
"new_artists_title": "New artists",
"new_tracks_title": "New tracks",
"top_artists_title": "Top artists ({daysCount} days)",
......@@ -35,6 +37,9 @@
"settings_label_statistics_recent_days_count": "Statistics: ",
"settings_label_timeline_days_count": "Global timeline: ",
"settings_label_top_artists_days_count": "Top Artists: ",
"settings_title_counts": "Counts: ",
"settings_label_new_artists_count": "New artists: ",
"settings_label_new_tracks_count": "New tracks: ",
"settings_button_save": "Save",
......
......@@ -22,6 +22,8 @@
"discoveries_title": "Découvertes ({daysCount} jours)",
"discoveries_artists_title": "Artistes",
"discoveries_tracks_title": "Morceaux",
"new_artists_title": "Nouveaux artistes",
"new_tracks_title": "Nouveaux morceaux",
"top_artists_title": "Top artistes ({daysCount} jours)",
......@@ -35,6 +37,10 @@
"settings_label_statistics_recent_days_count": "Statistiques : ",
"settings_label_timeline_days_count": "Timeline globale : ",
"settings_label_top_artists_days_count": "Top Artistes : ",
"settings_title_counts": "Nombres :",
"settings_label_new_artists_count": "Nouveaux artistes :",
"settings_label_new_tracks_count": "Nouveaux morceaux :",
"settings_button_save": "Enregistrer",
"MON": "LUN",
......
Display new discovered tracks and artists.
Affiche les nouveaux morceaux et artistes découverts.
class DefaultSettings {
static const List<int> allowedValues = [
static const List<int> allowedDaysCountValues = [
7,
14,
21,
......@@ -7,9 +7,17 @@ class DefaultSettings {
60,
90,
];
static const List<int> allowedCountValues = [
5,
10,
20,
];
static const int defaultDiscoveriesDaysCount = 14;
static const int defaultDistributionDaysCount = 21;
static const int defaultStatisticsRecentDaysCount = 21;
static const int defaultTimelineDaysCount = 14;
static const int defaultTopArtistsDaysCount = 14;
static const int defaultNewArtistsCount = 5;
static const int defaultNewTracksCount = 5;
}
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:scrobbles/models/new_artists.dart';
part 'data_new_artists_state.dart';
class DataNewArtistsCubit extends HydratedCubit<DataNewArtistsState> {
DataNewArtistsCubit() : super(const DataNewArtistsState());
void getData(DataNewArtistsState state) {
emit(state);
}
NewArtistsData? getValue() {
return state.newArtists;
}
void update(NewArtistsData? newArtists) {
if ((newArtists != null) && (state.newArtists.toString() != newArtists.toString())) {
setValue(newArtists);
}
}
void setValue(NewArtistsData? newArtists) {
emit(DataNewArtistsState(
newArtists: newArtists,
));
}
@override
DataNewArtistsState? fromJson(Map<String, dynamic> json) {
return DataNewArtistsState(
newArtists: NewArtistsData.fromJson(json['newArtists']),
);
}
@override
Map<String, Object?>? toJson(DataNewArtistsState state) {
return <String, Object?>{
'newArtists': state.newArtists?.toJson(),
};
}
}
part of 'data_new_artists_cubit.dart';
@immutable
class DataNewArtistsState extends Equatable {
const DataNewArtistsState({
this.newArtists,
});
final NewArtistsData? newArtists;
@override
List<Object?> get props => <Object?>[
newArtists,
];
}
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:scrobbles/models/new_tracks.dart';
part 'data_new_tracks_state.dart';
class DataNewTracksCubit extends HydratedCubit<DataNewTracksState> {
DataNewTracksCubit() : super(const DataNewTracksState());
void getData(DataNewTracksState state) {
emit(state);
}
NewTracksData? getValue() {
return state.newTracks;
}
void update(NewTracksData? newTracks) {
if ((newTracks != null) && (state.newTracks.toString() != newTracks.toString())) {
setValue(newTracks);
}
}
void setValue(NewTracksData? newTracks) {
emit(DataNewTracksState(
newTracks: newTracks,
));
}
@override
DataNewTracksState? fromJson(Map<String, dynamic> json) {
return DataNewTracksState(
newTracks: NewTracksData.fromJson(json['newTracks']),
);
}
@override
Map<String, Object?>? toJson(DataNewTracksState state) {
return <String, Object?>{
'newTracks': state.newTracks?.toJson(),
};
}
}
part of 'data_new_tracks_cubit.dart';
@immutable
class DataNewTracksState extends Equatable {
const DataNewTracksState({
this.newTracks,
});
final NewTracksData? newTracks;
@override
List<Object?> get props => <Object?>[
newTracks,
];
}
......@@ -37,6 +37,14 @@ class SettingsCubit extends HydratedCubit<SettingsState> {
return state.topArtistsDaysCount ?? DefaultSettings.defaultTopArtistsDaysCount;
}
int getNewArtistsCount() {
return state.newArtistsCount ?? DefaultSettings.defaultNewArtistsCount;
}
int getNewTracksCount() {
return state.newTracksCount ?? DefaultSettings.defaultNewTracksCount;
}
void setValues({
String? username,
String? securityToken,
......@@ -45,6 +53,8 @@ class SettingsCubit extends HydratedCubit<SettingsState> {
int? statisticsRecentDaysCount,
int? timelineDaysCount,
int? topArtistsDaysCount,
int? newArtistsCount,
int? newTracksCount,
}) {
emit(SettingsState(
username: username ?? state.username,
......@@ -54,6 +64,8 @@ class SettingsCubit extends HydratedCubit<SettingsState> {
statisticsRecentDaysCount: statisticsRecentDaysCount ?? state.statisticsRecentDaysCount,
timelineDaysCount: timelineDaysCount ?? state.timelineDaysCount,
topArtistsDaysCount: topArtistsDaysCount ?? state.topArtistsDaysCount,
newArtistsCount: newArtistsCount ?? state.newArtistsCount,
newTracksCount: newTracksCount ?? state.newTracksCount,
));
}
......@@ -66,6 +78,8 @@ class SettingsCubit extends HydratedCubit<SettingsState> {
int statisticsRecentDaysCount = json['statisticsRecentDaysCount'] as int;
int timelineDaysCount = json['timelineDaysCount'] as int;
int topArtistsDaysCount = json['topArtistsDaysCount'] as int;
int newArtistsCount = json['newArtistsCount'] as int;
int newTracksCount = json['newTracksCount'] as int;
return SettingsState(
username: username,
......@@ -75,6 +89,8 @@ class SettingsCubit extends HydratedCubit<SettingsState> {
statisticsRecentDaysCount: statisticsRecentDaysCount,
timelineDaysCount: timelineDaysCount,
topArtistsDaysCount: topArtistsDaysCount,
newArtistsCount: newArtistsCount,
newTracksCount: newTracksCount,
);
}
......@@ -92,6 +108,8 @@ class SettingsCubit extends HydratedCubit<SettingsState> {
'timelineDaysCount': state.timelineDaysCount ?? DefaultSettings.defaultTimelineDaysCount,
'topArtistsDaysCount':
state.topArtistsDaysCount ?? DefaultSettings.defaultTopArtistsDaysCount,
'newArtistsCount': state.newArtistsCount ?? DefaultSettings.defaultNewArtistsCount,
'newTracksCount': state.newTracksCount ?? DefaultSettings.defaultNewTracksCount,
};
}
}
......@@ -10,6 +10,8 @@ class SettingsState extends Equatable {
this.statisticsRecentDaysCount,
this.timelineDaysCount,
this.topArtistsDaysCount,
this.newArtistsCount,
this.newTracksCount,
});
final String? username;
......@@ -19,6 +21,8 @@ class SettingsState extends Equatable {
final int? statisticsRecentDaysCount;
final int? timelineDaysCount;
final int? topArtistsDaysCount;
final int? newArtistsCount;
final int? newTracksCount;
@override
List<dynamic> get props => <dynamic>[
......@@ -29,6 +33,8 @@ class SettingsState extends Equatable {
statisticsRecentDaysCount,
timelineDaysCount,
topArtistsDaysCount,
newArtistsCount,
newTracksCount,
];
Map<String, dynamic> get values => <String, dynamic>{
......@@ -39,5 +45,7 @@ class SettingsState extends Equatable {
'statisticsRecentDaysCount': statisticsRecentDaysCount,
'timelineDaysCount': timelineDaysCount,
'topArtistsDaysCount': topArtistsDaysCount,
'newArtistsCount': newArtistsCount,
'newTracksCount': newTracksCount,
};
}
......@@ -13,6 +13,8 @@ import 'package:scrobbles/cubit/data_counts_by_day_cubit.dart';
import 'package:scrobbles/cubit/data_counts_by_hour_cubit.dart';
import 'package:scrobbles/cubit/data_discoveries_cubit.dart';
import 'package:scrobbles/cubit/data_heatmap_cubit.dart';
import 'package:scrobbles/cubit/data_new_artists_cubit.dart';
import 'package:scrobbles/cubit/data_new_tracks_cubit.dart';
import 'package:scrobbles/cubit/data_statistics_global_cubit.dart';
import 'package:scrobbles/cubit/data_statistics_recent_cubit.dart';
import 'package:scrobbles/cubit/data_timeline_cubit.dart';
......@@ -57,6 +59,8 @@ class MyApp extends StatelessWidget {
BlocProvider<DataCountsByHourCubit>(create: (context) => DataCountsByHourCubit()),
BlocProvider<DataDiscoveriesCubit>(create: (context) => DataDiscoveriesCubit()),
BlocProvider<DataHeatmapCubit>(create: (context) => DataHeatmapCubit()),
BlocProvider<DataNewArtistsCubit>(create: (context) => DataNewArtistsCubit()),
BlocProvider<DataNewTracksCubit>(create: (context) => DataNewTracksCubit()),
BlocProvider<DataStatisticsGlobalCubit>(
create: (context) => DataStatisticsGlobalCubit()),
BlocProvider<DataStatisticsRecentCubit>(
......
import 'dart:convert';
class Artist {
final int id;
final String name;
final String mbid;
const Artist({
required this.id,
required this.name,
required this.mbid,
});
factory Artist.fromJson(Map<String, dynamic> json) {
return Artist(
id: json['id'] as int,
name: json['name'] as String,
mbid: (json['mbid'] != null) ? (json['mbid'] as String) : '',
);
}
Map<String, Object?>? toJson() {
return {
'id': this.id,
'name': this.name,
'mbid': this.mbid,
};
}
String toString() {
return jsonEncode(this.toJson());
}
}
import 'dart:convert';
import 'package:scrobbles/models/artists.dart';
class NewArtistData {
final DateTime? firstPlayed;
final Artist? artist;
const NewArtistData({
required this.firstPlayed,
required this.artist,
});
factory NewArtistData.fromJson(Map<String, dynamic>? json) {
return NewArtistData(
firstPlayed: (json?['firstPlayed'] != null && json?['firstPlayed']['date'] != null)
? DateTime.parse(
json?['firstPlayed']['date'],
)
: null,
artist: Artist.fromJson(json?['artist']),
);
}
}
class NewArtistsData {
final List<NewArtistData> data;
const NewArtistsData({
required this.data,
});
factory NewArtistsData.fromJson(List<dynamic>? json) {
List<NewArtistData> list = [];
json?.forEach((item) {
list.add(NewArtistData.fromJson(item));
});
return NewArtistsData(data: list);
}
List<Map<String, dynamic>> toJson() {
List<Map<String, dynamic>> list = [];
this.data.forEach((item) {
list.add({
'firstPlayed': {
'date': item.firstPlayed != null ? item.firstPlayed.toString() : null,
},
'artist': item.artist?.toJson(),
});
});
return list;
}
String toString() {
return jsonEncode(this.toJson());
}
}
import 'dart:convert';
import 'package:scrobbles/models/track.dart';
class NewTrackData {
final DateTime? firstPlayed;
final Track? track;
const NewTrackData({
required this.firstPlayed,
required this.track,
});
factory NewTrackData.fromJson(Map<String, dynamic>? json) {
return NewTrackData(
firstPlayed: (json?['firstPlayed'] != null && json?['firstPlayed']['date'] != null)
? DateTime.parse(
json?['firstPlayed']['date'],
)
: null,
track: Track.fromJson(json?['track']),
);
}
}
class NewTracksData {
final List<NewTrackData> data;
const NewTracksData({
required this.data,
});
factory NewTracksData.fromJson(List<dynamic>? json) {
List<NewTrackData> list = [];
json?.forEach((item) {
list.add(NewTrackData.fromJson(item));
});
return NewTracksData(data: list);
}
List<Map<String, dynamic>> toJson() {
List<Map<String, dynamic>> list = [];
this.data.forEach((item) {
list.add({
'firstPlayed': {
'date': item.firstPlayed != null ? item.firstPlayed.toString() : null,
},
'track': item.track?.toJson(),
});
});
return list;
}
String toString() {
return jsonEncode(this.toJson());
}
}
import 'dart:convert';
import 'package:scrobbles/models/artists.dart';
class Track {
final int id;
final String name;
final String mbid;
final Artist artist;
const Track({
required this.id,
required this.name,
required this.mbid,
required this.artist,
});
factory Track.fromJson(Map<String, dynamic> json) {
return Track(
id: json['id'] as int,
name: json['name'] as String,
mbid: (json['mbid'] != null) ? (json['mbid'] as String) : '',
artist: Artist.fromJson(json['artist']),
);
}
Map<String, Object?>? toJson() {
return {
'id': this.id,
'name': this.name,
'mbid': this.mbid,
'artist': this.artist.toJson(),
};
}
String toString() {
return jsonEncode(this.toJson());
}
}
......@@ -5,6 +5,8 @@ import 'package:scrobbles/models/counts_by_day.dart';
import 'package:scrobbles/models/counts_by_hour.dart';
import 'package:scrobbles/models/discoveries.dart';
import 'package:scrobbles/models/heatmap.dart';
import 'package:scrobbles/models/new_artists.dart';
import 'package:scrobbles/models/new_tracks.dart';
import 'package:scrobbles/models/statistics_global.dart';
import 'package:scrobbles/models/statistics_recent.dart';
import 'package:scrobbles/models/timeline.dart';
......@@ -100,4 +102,26 @@ class ScrobblesApi {
throw Exception('Failed to get data from API.');
}
}
static Future<NewArtistsData> fetchNewArtists(int count) async {
final String url = baseUrl + '/data/discoveries/artists/' + count.toString();
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
return NewArtistsData.fromJson(jsonDecode(response.body) as List<dynamic>);
} else {
throw Exception('Failed to get data from API.');
}
}
static Future<NewTracksData> fetchNewTracks(int count) async {
final String url = baseUrl + '/data/discoveries/tracks/' + count.toString();
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
return NewTracksData.fromJson(jsonDecode(response.body) as List<dynamic>);
} else {
throw Exception('Failed to get data from API.');
}
}
}
import 'package:flutter/material.dart';
import 'package:scrobbles/ui/widgets/cards/discoveries.dart';
import 'package:scrobbles/ui/widgets/cards/new_artists.dart';
import 'package:scrobbles/ui/widgets/cards/new_tracks.dart';
class ScreenDiscoveries extends StatelessWidget {
final Function() notifyParent;
......@@ -21,6 +23,10 @@ class ScreenDiscoveries extends StatelessWidget {
children: <Widget>[
const SizedBox(height: 8),
const CardDiscoveries(),
const SizedBox(height: 6),
const CardNewArtists(),
const SizedBox(height: 6),
const CardNewTracks(),
const SizedBox(height: 36),
],
),
......
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:scrobbles/cubit/data_new_artists_cubit.dart';
import 'package:scrobbles/cubit/settings_cubit.dart';
import 'package:scrobbles/models/new_artists.dart';
import 'package:scrobbles/network/scrobbles.dart';
import 'package:scrobbles/ui/widgets/card_content.dart';
import 'package:scrobbles/ui/widgets/error.dart';
class CardNewArtists extends StatelessWidget {
const CardNewArtists({super.key});
@override
Widget build(BuildContext context) {
SettingsCubit settings = BlocProvider.of<SettingsCubit>(context);
final int count = settings.getNewArtistsCount();
return BlocBuilder<DataNewArtistsCubit, DataNewArtistsState>(
builder: (BuildContext context, DataNewArtistsState state) {
return CardContent(
color: Theme.of(context).colorScheme.surface,
title: 'new_artists_title'.tr(),
loader: update(count),
content: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: state.newArtists?.data
.map((newArtist) => Text(newArtist.artist?.name ?? ''))
.toList() ??
[],
),
);
},
);
}
Widget update(int count) {
final Widget loading = const Text('⏳');
final Widget done = const Text('');
late Future<NewArtistsData> future = ScrobblesApi.fetchNewArtists(count);
return BlocBuilder<DataNewArtistsCubit, DataNewArtistsState>(
builder: (BuildContext context, DataNewArtistsState state) {
return FutureBuilder<NewArtistsData>(
future: future,
builder: (context, snapshot) {
if (snapshot.hasError) {
return ShowErrorWidget(message: '${snapshot.error}');
}
BlocProvider.of<DataNewArtistsCubit>(context).update(snapshot.data);
return !snapshot.hasData ? loading : done;
},
);
},
);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment