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

Show last discovered artists and tracks

parent 5d22b55c
No related branches found
No related tags found
1 merge request!50Resolve "Show last discovered artists and tracks"
Pipeline #4739 passed
This commit is part of merge request !50. Comments created here will be created in the context of that merge request.
Showing
with 461 additions and 3 deletions
org.gradle.jvmargs=-Xmx1536M org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
app.versionName=0.0.48 app.versionName=0.0.49
app.versionCode=48 app.versionCode=49
...@@ -22,6 +22,8 @@ ...@@ -22,6 +22,8 @@
"discoveries_title": "Discoveries ({daysCount} days)", "discoveries_title": "Discoveries ({daysCount} days)",
"discoveries_artists_title": "Artists", "discoveries_artists_title": "Artists",
"discoveries_tracks_title": "Tracks", "discoveries_tracks_title": "Tracks",
"new_artists_title": "New artists",
"new_tracks_title": "New tracks",
"top_artists_title": "Top artists ({daysCount} days)", "top_artists_title": "Top artists ({daysCount} days)",
...@@ -35,6 +37,9 @@ ...@@ -35,6 +37,9 @@
"settings_label_statistics_recent_days_count": "Statistics: ", "settings_label_statistics_recent_days_count": "Statistics: ",
"settings_label_timeline_days_count": "Global timeline: ", "settings_label_timeline_days_count": "Global timeline: ",
"settings_label_top_artists_days_count": "Top Artists: ", "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", "settings_button_save": "Save",
......
...@@ -22,6 +22,8 @@ ...@@ -22,6 +22,8 @@
"discoveries_title": "Découvertes ({daysCount} jours)", "discoveries_title": "Découvertes ({daysCount} jours)",
"discoveries_artists_title": "Artistes", "discoveries_artists_title": "Artistes",
"discoveries_tracks_title": "Morceaux", "discoveries_tracks_title": "Morceaux",
"new_artists_title": "Nouveaux artistes",
"new_tracks_title": "Nouveaux morceaux",
"top_artists_title": "Top artistes ({daysCount} jours)", "top_artists_title": "Top artistes ({daysCount} jours)",
...@@ -35,6 +37,10 @@ ...@@ -35,6 +37,10 @@
"settings_label_statistics_recent_days_count": "Statistiques : ", "settings_label_statistics_recent_days_count": "Statistiques : ",
"settings_label_timeline_days_count": "Timeline globale : ", "settings_label_timeline_days_count": "Timeline globale : ",
"settings_label_top_artists_days_count": "Top Artistes : ", "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", "settings_button_save": "Enregistrer",
"MON": "LUN", "MON": "LUN",
......
Display new discovered tracks and artists.
Affiche les nouveaux morceaux et artistes découverts.
class DefaultSettings { class DefaultSettings {
static const List<int> allowedValues = [ static const List<int> allowedDaysCountValues = [
7, 7,
14, 14,
21, 21,
...@@ -7,9 +7,17 @@ class DefaultSettings { ...@@ -7,9 +7,17 @@ class DefaultSettings {
60, 60,
90, 90,
]; ];
static const List<int> allowedCountValues = [
5,
10,
20,
];
static const int defaultDiscoveriesDaysCount = 14; static const int defaultDiscoveriesDaysCount = 14;
static const int defaultDistributionDaysCount = 21; static const int defaultDistributionDaysCount = 21;
static const int defaultStatisticsRecentDaysCount = 21; static const int defaultStatisticsRecentDaysCount = 21;
static const int defaultTimelineDaysCount = 14; static const int defaultTimelineDaysCount = 14;
static const int defaultTopArtistsDaysCount = 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> { ...@@ -37,6 +37,14 @@ class SettingsCubit extends HydratedCubit<SettingsState> {
return state.topArtistsDaysCount ?? DefaultSettings.defaultTopArtistsDaysCount; return state.topArtistsDaysCount ?? DefaultSettings.defaultTopArtistsDaysCount;
} }
int getNewArtistsCount() {
return state.newArtistsCount ?? DefaultSettings.defaultNewArtistsCount;
}
int getNewTracksCount() {
return state.newTracksCount ?? DefaultSettings.defaultNewTracksCount;
}
void setValues({ void setValues({
String? username, String? username,
String? securityToken, String? securityToken,
...@@ -45,6 +53,8 @@ class SettingsCubit extends HydratedCubit<SettingsState> { ...@@ -45,6 +53,8 @@ class SettingsCubit extends HydratedCubit<SettingsState> {
int? statisticsRecentDaysCount, int? statisticsRecentDaysCount,
int? timelineDaysCount, int? timelineDaysCount,
int? topArtistsDaysCount, int? topArtistsDaysCount,
int? newArtistsCount,
int? newTracksCount,
}) { }) {
emit(SettingsState( emit(SettingsState(
username: username ?? state.username, username: username ?? state.username,
...@@ -54,6 +64,8 @@ class SettingsCubit extends HydratedCubit<SettingsState> { ...@@ -54,6 +64,8 @@ class SettingsCubit extends HydratedCubit<SettingsState> {
statisticsRecentDaysCount: statisticsRecentDaysCount ?? state.statisticsRecentDaysCount, statisticsRecentDaysCount: statisticsRecentDaysCount ?? state.statisticsRecentDaysCount,
timelineDaysCount: timelineDaysCount ?? state.timelineDaysCount, timelineDaysCount: timelineDaysCount ?? state.timelineDaysCount,
topArtistsDaysCount: topArtistsDaysCount ?? state.topArtistsDaysCount, topArtistsDaysCount: topArtistsDaysCount ?? state.topArtistsDaysCount,
newArtistsCount: newArtistsCount ?? state.newArtistsCount,
newTracksCount: newTracksCount ?? state.newTracksCount,
)); ));
} }
...@@ -66,6 +78,8 @@ class SettingsCubit extends HydratedCubit<SettingsState> { ...@@ -66,6 +78,8 @@ class SettingsCubit extends HydratedCubit<SettingsState> {
int statisticsRecentDaysCount = json['statisticsRecentDaysCount'] as int; int statisticsRecentDaysCount = json['statisticsRecentDaysCount'] as int;
int timelineDaysCount = json['timelineDaysCount'] as int; int timelineDaysCount = json['timelineDaysCount'] as int;
int topArtistsDaysCount = json['topArtistsDaysCount'] as int; int topArtistsDaysCount = json['topArtistsDaysCount'] as int;
int newArtistsCount = json['newArtistsCount'] as int;
int newTracksCount = json['newTracksCount'] as int;
return SettingsState( return SettingsState(
username: username, username: username,
...@@ -75,6 +89,8 @@ class SettingsCubit extends HydratedCubit<SettingsState> { ...@@ -75,6 +89,8 @@ class SettingsCubit extends HydratedCubit<SettingsState> {
statisticsRecentDaysCount: statisticsRecentDaysCount, statisticsRecentDaysCount: statisticsRecentDaysCount,
timelineDaysCount: timelineDaysCount, timelineDaysCount: timelineDaysCount,
topArtistsDaysCount: topArtistsDaysCount, topArtistsDaysCount: topArtistsDaysCount,
newArtistsCount: newArtistsCount,
newTracksCount: newTracksCount,
); );
} }
...@@ -92,6 +108,8 @@ class SettingsCubit extends HydratedCubit<SettingsState> { ...@@ -92,6 +108,8 @@ class SettingsCubit extends HydratedCubit<SettingsState> {
'timelineDaysCount': state.timelineDaysCount ?? DefaultSettings.defaultTimelineDaysCount, 'timelineDaysCount': state.timelineDaysCount ?? DefaultSettings.defaultTimelineDaysCount,
'topArtistsDaysCount': 'topArtistsDaysCount':
state.topArtistsDaysCount ?? DefaultSettings.defaultTopArtistsDaysCount, state.topArtistsDaysCount ?? DefaultSettings.defaultTopArtistsDaysCount,
'newArtistsCount': state.newArtistsCount ?? DefaultSettings.defaultNewArtistsCount,
'newTracksCount': state.newTracksCount ?? DefaultSettings.defaultNewTracksCount,
}; };
} }
} }
...@@ -10,6 +10,8 @@ class SettingsState extends Equatable { ...@@ -10,6 +10,8 @@ class SettingsState extends Equatable {
this.statisticsRecentDaysCount, this.statisticsRecentDaysCount,
this.timelineDaysCount, this.timelineDaysCount,
this.topArtistsDaysCount, this.topArtistsDaysCount,
this.newArtistsCount,
this.newTracksCount,
}); });
final String? username; final String? username;
...@@ -19,6 +21,8 @@ class SettingsState extends Equatable { ...@@ -19,6 +21,8 @@ class SettingsState extends Equatable {
final int? statisticsRecentDaysCount; final int? statisticsRecentDaysCount;
final int? timelineDaysCount; final int? timelineDaysCount;
final int? topArtistsDaysCount; final int? topArtistsDaysCount;
final int? newArtistsCount;
final int? newTracksCount;
@override @override
List<dynamic> get props => <dynamic>[ List<dynamic> get props => <dynamic>[
...@@ -29,6 +33,8 @@ class SettingsState extends Equatable { ...@@ -29,6 +33,8 @@ class SettingsState extends Equatable {
statisticsRecentDaysCount, statisticsRecentDaysCount,
timelineDaysCount, timelineDaysCount,
topArtistsDaysCount, topArtistsDaysCount,
newArtistsCount,
newTracksCount,
]; ];
Map<String, dynamic> get values => <String, dynamic>{ Map<String, dynamic> get values => <String, dynamic>{
...@@ -39,5 +45,7 @@ class SettingsState extends Equatable { ...@@ -39,5 +45,7 @@ class SettingsState extends Equatable {
'statisticsRecentDaysCount': statisticsRecentDaysCount, 'statisticsRecentDaysCount': statisticsRecentDaysCount,
'timelineDaysCount': timelineDaysCount, 'timelineDaysCount': timelineDaysCount,
'topArtistsDaysCount': topArtistsDaysCount, 'topArtistsDaysCount': topArtistsDaysCount,
'newArtistsCount': newArtistsCount,
'newTracksCount': newTracksCount,
}; };
} }
...@@ -13,6 +13,8 @@ import 'package:scrobbles/cubit/data_counts_by_day_cubit.dart'; ...@@ -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_counts_by_hour_cubit.dart';
import 'package:scrobbles/cubit/data_discoveries_cubit.dart'; import 'package:scrobbles/cubit/data_discoveries_cubit.dart';
import 'package:scrobbles/cubit/data_heatmap_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_global_cubit.dart';
import 'package:scrobbles/cubit/data_statistics_recent_cubit.dart'; import 'package:scrobbles/cubit/data_statistics_recent_cubit.dart';
import 'package:scrobbles/cubit/data_timeline_cubit.dart'; import 'package:scrobbles/cubit/data_timeline_cubit.dart';
...@@ -57,6 +59,8 @@ class MyApp extends StatelessWidget { ...@@ -57,6 +59,8 @@ class MyApp extends StatelessWidget {
BlocProvider<DataCountsByHourCubit>(create: (context) => DataCountsByHourCubit()), BlocProvider<DataCountsByHourCubit>(create: (context) => DataCountsByHourCubit()),
BlocProvider<DataDiscoveriesCubit>(create: (context) => DataDiscoveriesCubit()), BlocProvider<DataDiscoveriesCubit>(create: (context) => DataDiscoveriesCubit()),
BlocProvider<DataHeatmapCubit>(create: (context) => DataHeatmapCubit()), BlocProvider<DataHeatmapCubit>(create: (context) => DataHeatmapCubit()),
BlocProvider<DataNewArtistsCubit>(create: (context) => DataNewArtistsCubit()),
BlocProvider<DataNewTracksCubit>(create: (context) => DataNewTracksCubit()),
BlocProvider<DataStatisticsGlobalCubit>( BlocProvider<DataStatisticsGlobalCubit>(
create: (context) => DataStatisticsGlobalCubit()), create: (context) => DataStatisticsGlobalCubit()),
BlocProvider<DataStatisticsRecentCubit>( 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'; ...@@ -5,6 +5,8 @@ import 'package:scrobbles/models/counts_by_day.dart';
import 'package:scrobbles/models/counts_by_hour.dart'; import 'package:scrobbles/models/counts_by_hour.dart';
import 'package:scrobbles/models/discoveries.dart'; import 'package:scrobbles/models/discoveries.dart';
import 'package:scrobbles/models/heatmap.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_global.dart';
import 'package:scrobbles/models/statistics_recent.dart'; import 'package:scrobbles/models/statistics_recent.dart';
import 'package:scrobbles/models/timeline.dart'; import 'package:scrobbles/models/timeline.dart';
...@@ -100,4 +102,26 @@ class ScrobblesApi { ...@@ -100,4 +102,26 @@ class ScrobblesApi {
throw Exception('Failed to get data from API.'); 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:flutter/material.dart';
import 'package:scrobbles/ui/widgets/cards/discoveries.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 { class ScreenDiscoveries extends StatelessWidget {
final Function() notifyParent; final Function() notifyParent;
...@@ -21,6 +23,10 @@ class ScreenDiscoveries extends StatelessWidget { ...@@ -21,6 +23,10 @@ class ScreenDiscoveries extends StatelessWidget {
children: <Widget>[ children: <Widget>[
const SizedBox(height: 8), const SizedBox(height: 8),
const CardDiscoveries(), const CardDiscoveries(),
const SizedBox(height: 6),
const CardNewArtists(),
const SizedBox(height: 6),
const CardNewTracks(),
const SizedBox(height: 36), 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