From a58945e91f9ab296c08dd07d3cd46493d2d9ebc8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Beno=C3=AEt=20Harrault?= <benoit@harrault.fr>
Date: Sat, 9 Dec 2023 18:21:37 +0100
Subject: [PATCH] Show last discovered artists and tracks

---
 android/gradle.properties                     |   4 +-
 assets/translations/en.json                   |   5 +
 assets/translations/fr.json                   |   6 +
 .../metadata/android/en-US/changelogs/49.txt  |   1 +
 .../metadata/android/fr-FR/changelogs/49.txt  |   1 +
 lib/config/default_settings.dart              |  10 +-
 lib/cubit/data_new_artists_cubit.dart         |  45 +++++++
 lib/cubit/data_new_artists_state.dart         |  15 +++
 lib/cubit/data_new_tracks_cubit.dart          |  45 +++++++
 lib/cubit/data_new_tracks_state.dart          |  15 +++
 lib/cubit/settings_cubit.dart                 |  18 +++
 lib/cubit/settings_state.dart                 |   8 ++
 lib/main.dart                                 |   4 +
 lib/models/artists.dart                       |  33 +++++
 lib/models/new_artists.dart                   |  61 ++++++++++
 lib/models/new_tracks.dart                    |  61 ++++++++++
 lib/models/track.dart                         |  39 ++++++
 lib/network/scrobbles.dart                    |  24 ++++
 lib/ui/screens/discoveries.dart               |   6 +
 lib/ui/widgets/cards/new_artists.dart         |  63 ++++++++++
 lib/ui/widgets/cards/new_tracks.dart          |  65 ++++++++++
 lib/ui/widgets/settings_form.dart             | 114 +++++++++++++++---
 pubspec.yaml                                  |   2 +-
 23 files changed, 623 insertions(+), 22 deletions(-)
 create mode 100644 fastlane/metadata/android/en-US/changelogs/49.txt
 create mode 100644 fastlane/metadata/android/fr-FR/changelogs/49.txt
 create mode 100644 lib/cubit/data_new_artists_cubit.dart
 create mode 100644 lib/cubit/data_new_artists_state.dart
 create mode 100644 lib/cubit/data_new_tracks_cubit.dart
 create mode 100644 lib/cubit/data_new_tracks_state.dart
 create mode 100644 lib/models/artists.dart
 create mode 100644 lib/models/new_artists.dart
 create mode 100644 lib/models/new_tracks.dart
 create mode 100644 lib/models/track.dart
 create mode 100644 lib/ui/widgets/cards/new_artists.dart
 create mode 100644 lib/ui/widgets/cards/new_tracks.dart

diff --git a/android/gradle.properties b/android/gradle.properties
index 9afb4c6..007b41f 100644
--- a/android/gradle.properties
+++ b/android/gradle.properties
@@ -1,5 +1,5 @@
 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
diff --git a/assets/translations/en.json b/assets/translations/en.json
index 4ff99c8..8ebe491 100644
--- a/assets/translations/en.json
+++ b/assets/translations/en.json
@@ -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",
 
diff --git a/assets/translations/fr.json b/assets/translations/fr.json
index bbf6d69..39625ae 100644
--- a/assets/translations/fr.json
+++ b/assets/translations/fr.json
@@ -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",
diff --git a/fastlane/metadata/android/en-US/changelogs/49.txt b/fastlane/metadata/android/en-US/changelogs/49.txt
new file mode 100644
index 0000000..d54a19e
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/49.txt
@@ -0,0 +1 @@
+Display new discovered tracks and artists.
diff --git a/fastlane/metadata/android/fr-FR/changelogs/49.txt b/fastlane/metadata/android/fr-FR/changelogs/49.txt
new file mode 100644
index 0000000..c5f4178
--- /dev/null
+++ b/fastlane/metadata/android/fr-FR/changelogs/49.txt
@@ -0,0 +1 @@
+Affiche les nouveaux morceaux et artistes découverts.
diff --git a/lib/config/default_settings.dart b/lib/config/default_settings.dart
index e564605..525784f 100644
--- a/lib/config/default_settings.dart
+++ b/lib/config/default_settings.dart
@@ -1,5 +1,5 @@
 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;
 }
diff --git a/lib/cubit/data_new_artists_cubit.dart b/lib/cubit/data_new_artists_cubit.dart
new file mode 100644
index 0000000..6ef782b
--- /dev/null
+++ b/lib/cubit/data_new_artists_cubit.dart
@@ -0,0 +1,45 @@
+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(),
+    };
+  }
+}
diff --git a/lib/cubit/data_new_artists_state.dart b/lib/cubit/data_new_artists_state.dart
new file mode 100644
index 0000000..eb6678c
--- /dev/null
+++ b/lib/cubit/data_new_artists_state.dart
@@ -0,0 +1,15 @@
+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,
+      ];
+}
diff --git a/lib/cubit/data_new_tracks_cubit.dart b/lib/cubit/data_new_tracks_cubit.dart
new file mode 100644
index 0000000..1092388
--- /dev/null
+++ b/lib/cubit/data_new_tracks_cubit.dart
@@ -0,0 +1,45 @@
+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(),
+    };
+  }
+}
diff --git a/lib/cubit/data_new_tracks_state.dart b/lib/cubit/data_new_tracks_state.dart
new file mode 100644
index 0000000..c23abe4
--- /dev/null
+++ b/lib/cubit/data_new_tracks_state.dart
@@ -0,0 +1,15 @@
+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,
+      ];
+}
diff --git a/lib/cubit/settings_cubit.dart b/lib/cubit/settings_cubit.dart
index 5b4374a..6c86444 100644
--- a/lib/cubit/settings_cubit.dart
+++ b/lib/cubit/settings_cubit.dart
@@ -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,
     };
   }
 }
diff --git a/lib/cubit/settings_state.dart b/lib/cubit/settings_state.dart
index 46d965e..45ef6bb 100644
--- a/lib/cubit/settings_state.dart
+++ b/lib/cubit/settings_state.dart
@@ -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,
       };
 }
diff --git a/lib/main.dart b/lib/main.dart
index d5123a1..6d990ff 100644
--- a/lib/main.dart
+++ b/lib/main.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_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>(
diff --git a/lib/models/artists.dart b/lib/models/artists.dart
new file mode 100644
index 0000000..e9eaf5e
--- /dev/null
+++ b/lib/models/artists.dart
@@ -0,0 +1,33 @@
+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());
+  }
+}
diff --git a/lib/models/new_artists.dart b/lib/models/new_artists.dart
new file mode 100644
index 0000000..d0c4b6a
--- /dev/null
+++ b/lib/models/new_artists.dart
@@ -0,0 +1,61 @@
+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());
+  }
+}
diff --git a/lib/models/new_tracks.dart b/lib/models/new_tracks.dart
new file mode 100644
index 0000000..101c433
--- /dev/null
+++ b/lib/models/new_tracks.dart
@@ -0,0 +1,61 @@
+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());
+  }
+}
diff --git a/lib/models/track.dart b/lib/models/track.dart
new file mode 100644
index 0000000..4630b88
--- /dev/null
+++ b/lib/models/track.dart
@@ -0,0 +1,39 @@
+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());
+  }
+}
diff --git a/lib/network/scrobbles.dart b/lib/network/scrobbles.dart
index 7f5c6f9..3e58697 100644
--- a/lib/network/scrobbles.dart
+++ b/lib/network/scrobbles.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/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.');
+    }
+  }
 }
diff --git a/lib/ui/screens/discoveries.dart b/lib/ui/screens/discoveries.dart
index 7a039c7..c30be66 100644
--- a/lib/ui/screens/discoveries.dart
+++ b/lib/ui/screens/discoveries.dart
@@ -1,6 +1,8 @@
 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),
           ],
         ),
diff --git a/lib/ui/widgets/cards/new_artists.dart b/lib/ui/widgets/cards/new_artists.dart
new file mode 100644
index 0000000..d9e898b
--- /dev/null
+++ b/lib/ui/widgets/cards/new_artists.dart
@@ -0,0 +1,63 @@
+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;
+          },
+        );
+      },
+    );
+  }
+}
diff --git a/lib/ui/widgets/cards/new_tracks.dart b/lib/ui/widgets/cards/new_tracks.dart
new file mode 100644
index 0000000..d2bcc0b
--- /dev/null
+++ b/lib/ui/widgets/cards/new_tracks.dart
@@ -0,0 +1,65 @@
+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_tracks_cubit.dart';
+import 'package:scrobbles/cubit/settings_cubit.dart';
+import 'package:scrobbles/models/new_tracks.dart';
+import 'package:scrobbles/network/scrobbles.dart';
+import 'package:scrobbles/ui/widgets/card_content.dart';
+import 'package:scrobbles/ui/widgets/error.dart';
+
+class CardNewTracks extends StatelessWidget {
+  const CardNewTracks({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    SettingsCubit settings = BlocProvider.of<SettingsCubit>(context);
+
+    final int count = settings.getNewTracksCount();
+
+    return BlocBuilder<DataNewTracksCubit, DataNewTracksState>(
+      builder: (BuildContext context, DataNewTracksState state) {
+        return CardContent(
+          color: Theme.of(context).colorScheme.surface,
+          title: 'new_tracks_title'.tr(),
+          loader: update(count),
+          content: Column(
+            mainAxisAlignment: MainAxisAlignment.start,
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: state.newTracks?.data
+                    .map((newTrack) => Text((newTrack.track?.artist.name ?? '') +
+                        ' - ' +
+                        (newTrack.track?.name ?? '')))
+                    .toList() ??
+                [],
+          ),
+        );
+      },
+    );
+  }
+
+  Widget update(int count) {
+    final Widget loading = const Text('⏳');
+    final Widget done = const Text('');
+
+    late Future<NewTracksData> future = ScrobblesApi.fetchNewTracks(count);
+
+    return BlocBuilder<DataNewTracksCubit, DataNewTracksState>(
+      builder: (BuildContext context, DataNewTracksState state) {
+        return FutureBuilder<NewTracksData>(
+          future: future,
+          builder: (context, snapshot) {
+            if (snapshot.hasError) {
+              return ShowErrorWidget(message: '${snapshot.error}');
+            }
+
+            BlocProvider.of<DataNewTracksCubit>(context).update(snapshot.data);
+
+            return !snapshot.hasData ? loading : done;
+          },
+        );
+      },
+    );
+  }
+}
diff --git a/lib/ui/widgets/settings_form.dart b/lib/ui/widgets/settings_form.dart
index aad22f5..de09c91 100644
--- a/lib/ui/widgets/settings_form.dart
+++ b/lib/ui/widgets/settings_form.dart
@@ -23,12 +23,16 @@ class _SettingsFormState extends State<SettingsForm> {
   int statisticsRecentDaysCount = DefaultSettings.defaultStatisticsRecentDaysCount;
   int timelineDaysCount = DefaultSettings.defaultTimelineDaysCount;
   int topArtistsDaysCount = DefaultSettings.defaultTopArtistsDaysCount;
+  int newArtistsCount = DefaultSettings.defaultNewArtistsCount;
+  int newTracksCount = DefaultSettings.defaultNewTracksCount;
 
   List<bool> _selectedDiscoveriesDaysCount = [];
   List<bool> _selectedDistributionDaysCount = [];
   List<bool> _selectedStatisticsRecentDaysCount = [];
   List<bool> _selectedTimelineDaysCount = [];
   List<bool> _selectedTopArtistsDaysCount = [];
+  List<bool> _selectedNewArtistsCount = [];
+  List<bool> _selectedNewTracksCount = [];
 
   @override
   void didChangeDependencies() {
@@ -42,17 +46,26 @@ class _SettingsFormState extends State<SettingsForm> {
     statisticsRecentDaysCount = settings.getStatisticsRecentDaysCount();
     timelineDaysCount = settings.getTimelineDaysCount();
     topArtistsDaysCount = settings.getTopArtistsDaysCount();
+    newArtistsCount = settings.getNewArtistsCount();
+    newTracksCount = settings.getNewTracksCount();
 
-    _selectedDiscoveriesDaysCount =
-        DefaultSettings.allowedValues.map((e) => (e == discoveriesDaysCount)).toList();
-    _selectedDistributionDaysCount =
-        DefaultSettings.allowedValues.map((e) => (e == distributionDaysCount)).toList();
-    _selectedStatisticsRecentDaysCount =
-        DefaultSettings.allowedValues.map((e) => (e == statisticsRecentDaysCount)).toList();
+    _selectedDiscoveriesDaysCount = DefaultSettings.allowedDaysCountValues
+        .map((e) => (e == discoveriesDaysCount))
+        .toList();
+    _selectedDistributionDaysCount = DefaultSettings.allowedDaysCountValues
+        .map((e) => (e == distributionDaysCount))
+        .toList();
+    _selectedStatisticsRecentDaysCount = DefaultSettings.allowedDaysCountValues
+        .map((e) => (e == statisticsRecentDaysCount))
+        .toList();
     _selectedTimelineDaysCount =
-        DefaultSettings.allowedValues.map((e) => (e == timelineDaysCount)).toList();
+        DefaultSettings.allowedDaysCountValues.map((e) => (e == timelineDaysCount)).toList();
     _selectedTopArtistsDaysCount =
-        DefaultSettings.allowedValues.map((e) => (e == topArtistsDaysCount)).toList();
+        DefaultSettings.allowedDaysCountValues.map((e) => (e == topArtistsDaysCount)).toList();
+    _selectedNewArtistsCount =
+        DefaultSettings.allowedCountValues.map((e) => (e == newArtistsCount)).toList();
+    _selectedNewTracksCount =
+        DefaultSettings.allowedCountValues.map((e) => (e == newTracksCount)).toList();
 
     super.didChangeDependencies();
   }
@@ -75,6 +88,8 @@ class _SettingsFormState extends State<SettingsForm> {
         statisticsRecentDaysCount: statisticsRecentDaysCount,
         timelineDaysCount: timelineDaysCount,
         topArtistsDaysCount: topArtistsDaysCount,
+        newArtistsCount: newArtistsCount,
+        newTracksCount: newTracksCount,
       );
     }
 
@@ -118,7 +133,7 @@ class _SettingsFormState extends State<SettingsForm> {
             ToggleButtons(
               onPressed: (int index) {
                 setState(() {
-                  statisticsRecentDaysCount = DefaultSettings.allowedValues[index];
+                  statisticsRecentDaysCount = DefaultSettings.allowedDaysCountValues[index];
                   for (int i = 0; i < _selectedStatisticsRecentDaysCount.length; i++) {
                     _selectedStatisticsRecentDaysCount[i] = i == index;
                   }
@@ -128,7 +143,9 @@ class _SettingsFormState extends State<SettingsForm> {
               borderRadius: const BorderRadius.all(Radius.circular(8)),
               constraints: const BoxConstraints(minHeight: 30.0, minWidth: 30.0),
               isSelected: _selectedStatisticsRecentDaysCount,
-              children: DefaultSettings.allowedValues.map((e) => Text(e.toString())).toList(),
+              children: DefaultSettings.allowedDaysCountValues
+                  .map((e) => Text(e.toString()))
+                  .toList(),
             ),
           ],
         ),
@@ -142,7 +159,7 @@ class _SettingsFormState extends State<SettingsForm> {
             ToggleButtons(
               onPressed: (int index) {
                 setState(() {
-                  timelineDaysCount = DefaultSettings.allowedValues[index];
+                  timelineDaysCount = DefaultSettings.allowedDaysCountValues[index];
                   for (int i = 0; i < _selectedTimelineDaysCount.length; i++) {
                     _selectedTimelineDaysCount[i] = i == index;
                   }
@@ -152,7 +169,9 @@ class _SettingsFormState extends State<SettingsForm> {
               borderRadius: const BorderRadius.all(Radius.circular(8)),
               constraints: const BoxConstraints(minHeight: 30.0, minWidth: 30.0),
               isSelected: _selectedTimelineDaysCount,
-              children: DefaultSettings.allowedValues.map((e) => Text(e.toString())).toList(),
+              children: DefaultSettings.allowedDaysCountValues
+                  .map((e) => Text(e.toString()))
+                  .toList(),
             ),
           ],
         ),
@@ -166,7 +185,7 @@ class _SettingsFormState extends State<SettingsForm> {
             ToggleButtons(
               onPressed: (int index) {
                 setState(() {
-                  topArtistsDaysCount = DefaultSettings.allowedValues[index];
+                  topArtistsDaysCount = DefaultSettings.allowedDaysCountValues[index];
                   for (int i = 0; i < _selectedTopArtistsDaysCount.length; i++) {
                     _selectedTopArtistsDaysCount[i] = i == index;
                   }
@@ -176,7 +195,9 @@ class _SettingsFormState extends State<SettingsForm> {
               borderRadius: const BorderRadius.all(Radius.circular(8)),
               constraints: const BoxConstraints(minHeight: 30.0, minWidth: 30.0),
               isSelected: _selectedTopArtistsDaysCount,
-              children: DefaultSettings.allowedValues.map((e) => Text(e.toString())).toList(),
+              children: DefaultSettings.allowedDaysCountValues
+                  .map((e) => Text(e.toString()))
+                  .toList(),
             ),
           ],
         ),
@@ -190,7 +211,7 @@ class _SettingsFormState extends State<SettingsForm> {
             ToggleButtons(
               onPressed: (int index) {
                 setState(() {
-                  discoveriesDaysCount = DefaultSettings.allowedValues[index];
+                  discoveriesDaysCount = DefaultSettings.allowedDaysCountValues[index];
                   for (int i = 0; i < _selectedDiscoveriesDaysCount.length; i++) {
                     _selectedDiscoveriesDaysCount[i] = i == index;
                   }
@@ -200,7 +221,9 @@ class _SettingsFormState extends State<SettingsForm> {
               borderRadius: const BorderRadius.all(Radius.circular(8)),
               constraints: const BoxConstraints(minHeight: 30.0, minWidth: 30.0),
               isSelected: _selectedDiscoveriesDaysCount,
-              children: DefaultSettings.allowedValues.map((e) => Text(e.toString())).toList(),
+              children: DefaultSettings.allowedDaysCountValues
+                  .map((e) => Text(e.toString()))
+                  .toList(),
             ),
           ],
         ),
@@ -214,7 +237,7 @@ class _SettingsFormState extends State<SettingsForm> {
             ToggleButtons(
               onPressed: (int index) {
                 setState(() {
-                  distributionDaysCount = DefaultSettings.allowedValues[index];
+                  distributionDaysCount = DefaultSettings.allowedDaysCountValues[index];
                   for (int i = 0; i < _selectedDistributionDaysCount.length; i++) {
                     _selectedDistributionDaysCount[i] = i == index;
                   }
@@ -224,7 +247,62 @@ class _SettingsFormState extends State<SettingsForm> {
               borderRadius: const BorderRadius.all(Radius.circular(8)),
               constraints: const BoxConstraints(minHeight: 30.0, minWidth: 30.0),
               isSelected: _selectedDistributionDaysCount,
-              children: DefaultSettings.allowedValues.map((e) => Text(e.toString())).toList(),
+              children: DefaultSettings.allowedDaysCountValues
+                  .map((e) => Text(e.toString()))
+                  .toList(),
+            ),
+          ],
+        ),
+
+        SizedBox(height: 8),
+        AppTitle2(text: tr('settings_title_counts')),
+
+        // New artists count
+        Row(
+          mainAxisAlignment: MainAxisAlignment.end,
+          crossAxisAlignment: CrossAxisAlignment.center,
+          children: [
+            Text('settings_label_new_artists_count').tr(),
+            ToggleButtons(
+              onPressed: (int index) {
+                setState(() {
+                  newArtistsCount = DefaultSettings.allowedCountValues[index];
+                  for (int i = 0; i < _selectedNewArtistsCount.length; i++) {
+                    _selectedNewArtistsCount[i] = i == index;
+                  }
+                });
+                saveSettings();
+              },
+              borderRadius: const BorderRadius.all(Radius.circular(8)),
+              constraints: const BoxConstraints(minHeight: 30.0, minWidth: 30.0),
+              isSelected: _selectedNewArtistsCount,
+              children:
+                  DefaultSettings.allowedCountValues.map((e) => Text(e.toString())).toList(),
+            ),
+          ],
+        ),
+
+        // New tracks count
+        Row(
+          mainAxisAlignment: MainAxisAlignment.end,
+          crossAxisAlignment: CrossAxisAlignment.center,
+          children: [
+            Text('settings_label_new_tracks_count').tr(),
+            ToggleButtons(
+              onPressed: (int index) {
+                setState(() {
+                  newTracksCount = DefaultSettings.allowedCountValues[index];
+                  for (int i = 0; i < _selectedNewTracksCount.length; i++) {
+                    _selectedNewTracksCount[i] = i == index;
+                  }
+                });
+                saveSettings();
+              },
+              borderRadius: const BorderRadius.all(Radius.circular(8)),
+              constraints: const BoxConstraints(minHeight: 30.0, minWidth: 30.0),
+              isSelected: _selectedNewTracksCount,
+              children:
+                  DefaultSettings.allowedCountValues.map((e) => Text(e.toString())).toList(),
             ),
           ],
         ),
diff --git a/pubspec.yaml b/pubspec.yaml
index 2e058f8..bb7e923 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -3,7 +3,7 @@ description: Display scrobbles data and charts
 
 publish_to: 'none'
 
-version: 0.0.48+48
+version: 0.0.49+49
 
 environment:
   sdk: '^3.0.0'
-- 
GitLab