From 88a9dbb05f7c16a22e3e21d50b79a78d9480b212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Harrault?= <benoit@harrault.fr> Date: Mon, 6 Nov 2023 13:55:02 +0100 Subject: [PATCH] Add "discoveries" chart --- android/gradle.properties | 4 +- assets/translations/en.json | 2 + assets/translations/fr.json | 2 + .../metadata/android/en-US/changelogs/20.txt | 1 + .../metadata/android/fr-FR/changelogs/20.txt | 1 + lib/models/discoveries.dart | 56 +++++++++++++ lib/network/scrobbles_api.dart | 13 +++ lib/ui/screens/main_screen.dart | 3 + lib/ui/widgets/charts/custom_bar_chart.dart | 25 ++++-- .../main_screen/counts_by_day_chart.dart | 4 +- .../main_screen/counts_by_hour_chart.dart | 4 +- .../widgets/main_screen/discoveries_card.dart | 48 +++++++++++ .../main_screen/discoveries_chart.dart | 79 +++++++++++++++++++ .../main_screen/discoveries_content.dart | 43 ++++++++++ .../main_screen/timeline_chart_counts.dart | 4 +- pubspec.yaml | 2 +- 16 files changed, 274 insertions(+), 17 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/20.txt create mode 100644 fastlane/metadata/android/fr-FR/changelogs/20.txt create mode 100644 lib/models/discoveries.dart create mode 100644 lib/ui/widgets/main_screen/discoveries_card.dart create mode 100644 lib/ui/widgets/main_screen/discoveries_chart.dart create mode 100644 lib/ui/widgets/main_screen/discoveries_content.dart diff --git a/android/gradle.properties b/android/gradle.properties index 6c1d873..24add27 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.19 -app.versionCode=19 +app.versionName=0.0.20 +app.versionCode=20 diff --git a/assets/translations/en.json b/assets/translations/en.json index 708e712..18cc8f1 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -12,6 +12,8 @@ "counts_by_day": "Counts by day ({daysCount} days)", "counts_by_hour": "Counts by hour ({daysCount} days)", + "discoveries_title": "Discoveries ({daysCount} days)", + "MON": "MON", "TUE": "TUE", "WED": "WED", diff --git a/assets/translations/fr.json b/assets/translations/fr.json index 4394698..5b69d01 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -12,6 +12,8 @@ "counts_by_day": "Écoutes par jour ({daysCount} jours)", "counts_by_hour": "Écoutes par heure ({daysCount} jours)", + "discoveries_title": "Découvertes ({daysCount} jours)", + "MON": "LUN", "TUE": "MAR", "WED": "MER", diff --git a/fastlane/metadata/android/en-US/changelogs/20.txt b/fastlane/metadata/android/en-US/changelogs/20.txt new file mode 100644 index 0000000..9aa3f3b --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/20.txt @@ -0,0 +1 @@ +Add discoveries chart. diff --git a/fastlane/metadata/android/fr-FR/changelogs/20.txt b/fastlane/metadata/android/fr-FR/changelogs/20.txt new file mode 100644 index 0000000..8f6a741 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/20.txt @@ -0,0 +1 @@ +Ajout du graphique des découvertes. diff --git a/lib/models/discoveries.dart b/lib/models/discoveries.dart new file mode 100644 index 0000000..3e67649 --- /dev/null +++ b/lib/models/discoveries.dart @@ -0,0 +1,56 @@ +import 'dart:convert'; + +class DiscoveriesDataValue { + final int newArtistsCount; + final int newTracksCount; + + const DiscoveriesDataValue({required this.newArtistsCount, required this.newTracksCount}); + + factory DiscoveriesDataValue.fromJson(Map<String, dynamic> json) { + return DiscoveriesDataValue( + newArtistsCount: json['new-artists'] as int, + newTracksCount: json['new-tracks'] as int, + ); + } +} + +class DiscoveriesData { + final Map<String, DiscoveriesDataValue> data; + + const DiscoveriesData({ + required this.data, + }); + + factory DiscoveriesData.fromJson(Map<String, dynamic> json) { + Map<String, DiscoveriesDataValue> data = {}; + + json.keys.forEach((date) { + DiscoveriesDataValue value = DiscoveriesDataValue( + newArtistsCount: json[date]['new-artists'] as int, + newTracksCount: json[date]['new-tracks'] as int, + ); + + data[date] = value; + }); + + return DiscoveriesData(data: data); + } + + factory DiscoveriesData.createEmpty() { + return DiscoveriesData.fromJson({}); + } + + String toString() { + Map<String, Map<String, int>> map = {}; + + this.data.keys.forEach((element) { + DiscoveriesDataValue? item = this.data[element]; + map[element] = { + 'new-artists': item != null ? item.newArtistsCount : 0, + 'new-tracks': item != null ? item.newTracksCount : 0, + }; + }); + + return jsonEncode(map); + } +} diff --git a/lib/network/scrobbles_api.dart b/lib/network/scrobbles_api.dart index 845afb9..d2ba828 100644 --- a/lib/network/scrobbles_api.dart +++ b/lib/network/scrobbles_api.dart @@ -3,6 +3,7 @@ import 'package:http/http.dart' as http; import '../models/counts_by_day.dart'; import '../models/counts_by_hour.dart'; +import '../models/discoveries.dart'; import '../models/statistics.dart'; import '../models/timeline.dart'; @@ -56,4 +57,16 @@ class ScrobblesApi { throw Exception('Failed to get data from API.'); } } + + static Future<DiscoveriesData> fetchDiscoveries(int daysCount) async { + final String url = baseUrl + '/data/' + daysCount.toString() + '/news'; + print('fetching ' + url); + final response = await http.get(Uri.parse(url)); + + if (response.statusCode == 200) { + return DiscoveriesData.fromJson(jsonDecode(response.body) as Map<String, dynamic>); + } else { + throw Exception('Failed to get data from API.'); + } + } } diff --git a/lib/ui/screens/main_screen.dart b/lib/ui/screens/main_screen.dart index 9943616..ee6b344 100644 --- a/lib/ui/screens/main_screen.dart +++ b/lib/ui/screens/main_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import '../widgets/main_screen/counts_by_day_card.dart'; import '../widgets/main_screen/counts_by_hour_card.dart'; +import '../widgets/main_screen/discoveries_card.dart'; import '../widgets/main_screen/statistics_card.dart'; import '../widgets/main_screen/timeline_card.dart'; @@ -21,6 +22,8 @@ class MainScreen extends StatelessWidget { const SizedBox(height: 8), const ChartTimelineCard(), const SizedBox(height: 8), + const ChartDiscoveriesCard(), + const SizedBox(height: 8), const ChartCountsByDayCard(), const SizedBox(height: 8), const ChartCountsByHourCard(), diff --git a/lib/ui/widgets/charts/custom_bar_chart.dart b/lib/ui/widgets/charts/custom_bar_chart.dart index 97ea930..162d53b 100644 --- a/lib/ui/widgets/charts/custom_bar_chart.dart +++ b/lib/ui/widgets/charts/custom_bar_chart.dart @@ -54,15 +54,19 @@ class CustomBarChart extends StatelessWidget { BarChartGroupData getBarItem( {required int x, - required double value, - required Color barColor, + required List<double> values, + required List<Color> barColors, required double barWidth}) { - final gradient = this.getGradient(barColor, value, this.getMaxCountsValue()); - final borderColor = barColor.darken(20); + List<BarChartRodData> barRods = []; - return BarChartGroupData( - x: x, - barRods: [ + for (int i = 0; i < values.length; i++) { + double value = values[i]; + Color barColor = barColors[i]; + + final gradient = this.getGradient(barColor, value, this.getMaxCountsValue()); + final borderColor = barColor.darken(20); + + barRods.add( BarChartRodData( toY: value, color: barColor, @@ -73,7 +77,12 @@ class CustomBarChart extends StatelessWidget { color: borderColor, ), ), - ], + ); + } + + return BarChartGroupData( + x: x, + barRods: barRods, ); } diff --git a/lib/ui/widgets/main_screen/counts_by_day_chart.dart b/lib/ui/widgets/main_screen/counts_by_day_chart.dart index a29117b..360dcc1 100644 --- a/lib/ui/widgets/main_screen/counts_by_day_chart.dart +++ b/lib/ui/widgets/main_screen/counts_by_day_chart.dart @@ -82,8 +82,8 @@ class CountsByDayCardContentChart extends CustomBarChart { data.add(this.getBarItem( x: day, - value: counts, - barColor: barColor, + values: [counts], + barColors: [barColor], barWidth: barWidth, )); } diff --git a/lib/ui/widgets/main_screen/counts_by_hour_chart.dart b/lib/ui/widgets/main_screen/counts_by_hour_chart.dart index bf61e6a..ec5766a 100644 --- a/lib/ui/widgets/main_screen/counts_by_hour_chart.dart +++ b/lib/ui/widgets/main_screen/counts_by_hour_chart.dart @@ -53,8 +53,8 @@ class CountsByHourCardContentChart extends CustomBarChart { if (counts != null) { data.add(getBarItem( x: day, - value: counts, - barColor: barColor, + values: [counts], + barColors: [barColor], barWidth: barWidth, )); } diff --git a/lib/ui/widgets/main_screen/discoveries_card.dart b/lib/ui/widgets/main_screen/discoveries_card.dart new file mode 100644 index 0000000..98d1065 --- /dev/null +++ b/lib/ui/widgets/main_screen/discoveries_card.dart @@ -0,0 +1,48 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; + +import '../../../models/discoveries.dart'; +import '../../../network/scrobbles_api.dart'; +import '../../../ui/widgets/error.dart'; +import '../../../ui/widgets/main_screen/discoveries_content.dart'; + +class ChartDiscoveriesCard extends StatelessWidget { + const ChartDiscoveriesCard({super.key}); + + @override + Widget build(BuildContext context) { + final int daysCount = 14; + late Future<DiscoveriesData> futureTimeline = ScrobblesApi.fetchDiscoveries(daysCount); + + return FutureBuilder<DiscoveriesData>( + future: futureTimeline, + builder: (context, snapshot) { + if (snapshot.hasError) { + return ShowErrorWidget(message: '${snapshot.error}'); + } + + return Card( + elevation: 2, + shadowColor: Theme.of(context).colorScheme.shadow, + color: Theme.of(context).colorScheme.surface, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(12), + ), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: ChartDiscoveriesCardContent( + daysCount: daysCount, + chartData: snapshot.hasData + ? DiscoveriesData.fromJson(jsonDecode(snapshot.data.toString())) + : DiscoveriesData.createEmpty(), + isLoading: !snapshot.hasData, + ), + ), + ); + }, + ); + } +} diff --git a/lib/ui/widgets/main_screen/discoveries_chart.dart b/lib/ui/widgets/main_screen/discoveries_chart.dart new file mode 100644 index 0000000..991f7c3 --- /dev/null +++ b/lib/ui/widgets/main_screen/discoveries_chart.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:fl_chart/fl_chart.dart'; + +import '../../../config/app_colors.dart'; +import '../../../models/discoveries.dart'; +import '../../../utils/color_extensions.dart'; +import '../../../ui/widgets/charts/custom_bar_chart.dart'; + +class ChartDiscoveries extends CustomBarChart { + final DiscoveriesData chartData; + + ChartDiscoveries({super.key, required this.chartData}); + + final double chartHeight = 120.0; + final double verticalTicksInterval = 10; + + @override + Widget build(BuildContext context) { + return Container( + height: this.chartHeight, + child: LayoutBuilder(builder: (context, constraints) { + return getBarChart( + barWidth: this.getBarWidth(constraints.maxWidth, this.chartData.data.keys.length), + backgroundColor: Theme.of(context).colorScheme.onSurface, + ); + }), + ); + } + + double getMaxCountsValue() { + double maxValue = 0; + + this.chartData.data.keys.forEach((key) { + DiscoveriesDataValue? value = this.chartData.data[key]; + if (value != null) { + double newArtistsCount = value.newArtistsCount.toDouble(); + double newTracksCount = value.newTracksCount.toDouble(); + + if (newArtistsCount > maxValue) { + maxValue = newArtistsCount; + } + if (newTracksCount > maxValue) { + maxValue = newTracksCount; + } + } + }); + + return maxValue; + } + + List<BarChartGroupData> getDataCounts(double barWidth) { + List<BarChartGroupData> data = []; + + final newArtistsBarColor = AppColors.contentColorGreen.darken(20); + final newTracksBarColor = AppColors.contentColorBlue.darken(20); + + this.chartData.data.keys.forEach((key) { + DiscoveriesDataValue? value = this.chartData.data[key]; + if (value != null) { + final int date = DateTime.parse(key).millisecondsSinceEpoch; + + data.add(this.getBarItem( + x: date, + values: [ + value.newArtistsCount.toDouble(), + value.newTracksCount.toDouble(), + ], + barColors: [ + newArtistsBarColor, + newTracksBarColor, + ], + barWidth: barWidth / 2.5, + )); + } + }); + + return data; + } +} diff --git a/lib/ui/widgets/main_screen/discoveries_content.dart b/lib/ui/widgets/main_screen/discoveries_content.dart new file mode 100644 index 0000000..5dccaf0 --- /dev/null +++ b/lib/ui/widgets/main_screen/discoveries_content.dart @@ -0,0 +1,43 @@ +import 'dart:convert'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +import '../../../models/discoveries.dart'; +import '../../../ui/widgets/main_screen/discoveries_chart.dart'; + +class ChartDiscoveriesCardContent extends StatelessWidget { + final int daysCount; + final DiscoveriesData chartData; + final bool isLoading; + + const ChartDiscoveriesCardContent( + {super.key, required this.daysCount, required this.chartData, required this.isLoading}); + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).primaryTextTheme; + final String placeholder = 'â³'; + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: <Widget>[ + Text( + 'discoveries_title', + style: textTheme.titleLarge!.apply(fontWeightDelta: 2), + ).tr( + namedArgs: { + 'daysCount': this.daysCount.toString(), + }, + ), + const SizedBox(height: 8), + this.isLoading + ? Text(placeholder) + : ChartDiscoveries( + chartData: DiscoveriesData.fromJson(jsonDecode(this.chartData.toString())), + ), + ], + ); + } +} diff --git a/lib/ui/widgets/main_screen/timeline_chart_counts.dart b/lib/ui/widgets/main_screen/timeline_chart_counts.dart index 02534a9..c5bf7eb 100644 --- a/lib/ui/widgets/main_screen/timeline_chart_counts.dart +++ b/lib/ui/widgets/main_screen/timeline_chart_counts.dart @@ -55,8 +55,8 @@ class ChartTimelineCounts extends CustomBarChart { data.add(this.getBarItem( x: date, - value: counts, - barColor: barColor, + values: [counts], + barColors: [barColor], barWidth: barWidth, )); } diff --git a/pubspec.yaml b/pubspec.yaml index e633b49..8551972 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: Display scrobbles data and charts publish_to: 'none' -version: 0.0.19+19 +version: 0.0.20+20 environment: sdk: '^3.0.0' -- GitLab