diff --git a/android/gradle.properties b/android/gradle.properties index 6c1d873456149a8611e43a05ae56e4f50c73274f..24add27a90a4accaf6a1ee28ec651d0d6bda4f8e 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 708e712862d32d3fcfa2e45b575a0298303a1bdc..18cc8f17e0aace45f020d971f2a407219386a933 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 4394698622d36ddf4d04c373aaf341256ab824e9..5b69d011ff20878504766332810d8d8a54026afd 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 0000000000000000000000000000000000000000..9aa3f3b77ea21efd31190764fd44f9faedec4e2f --- /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 0000000000000000000000000000000000000000..8f6a7414874a7806865d338e7163cd29dc23a126 --- /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 0000000000000000000000000000000000000000..3e67649b1eab4e2acb7133fab1e243052afcaefb --- /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 845afb93e660cdb755dd89b7fc789665e618ae57..d2ba828195ae6f9b6f9f4b02af99919669073551 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 9943616e7d487fe49da2c986db697d315b2ce256..ee6b34419ae089fcb11c2dabb75fec04cf0c0a45 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 97ea9301f46eb505cb58d798a947afaa4cfc3e39..162d53b69516dbd81b56f860a37e5dfe23bc2282 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 a29117bad54efe2fef3dc4012a18253633e4377a..360dcc16406f687b4d4b179c34d6999a54189f78 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 bf61e6ae621ca0d20d12f693177754a10fe6fb56..ec5766a96a978a05b31baeafddde8527d5a456a5 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 0000000000000000000000000000000000000000..98d1065ad7831db2085ad02496c0e94f29493501 --- /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 0000000000000000000000000000000000000000..991f7c3e659af8cf6a71432eb6ece1833b9b4e1d --- /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 0000000000000000000000000000000000000000..5dccaf0d4ebb393ce01ead1e19c80a79513e4931 --- /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 02534a9d5f761c4a3212e57ac369bdddc4259250..c5bf7ebb38df7fa26fae341104c2b79a72eeb813 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 e633b4982b095480b6affd192838b989c6c892d0..855197294fb02977a987485152258ab3f7b138c1 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'