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

Merge branch '31-split-global-period-statistics' into 'master'

Resolve "Split global / period statistics"

Closes #31

See merge request !26
parents 13a8455b 6f7fa8bf
No related branches found
No related tags found
1 merge request!26Resolve "Split global / period statistics"
Pipeline #4569 passed
Showing
with 185 additions and 62 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.23 app.versionName=0.0.24
app.versionCode=23 app.versionCode=24
...@@ -8,6 +8,8 @@ ...@@ -8,6 +8,8 @@
"global_statistics": "Global statistics", "global_statistics": "Global statistics",
"statistics_total_scrobbles_count": "Total scrobbles count: {count}", "statistics_total_scrobbles_count": "Total scrobbles count: {count}",
"statistics_last_scrobble": "Last scrobble: {datetime}", "statistics_last_scrobble": "Last scrobble: {datetime}",
"recent_statistics": "Recent statistics",
"statistics_selected_period": "On last {daysCount} days:", "statistics_selected_period": "On last {daysCount} days:",
"statistics_recent_scrobbles_count_and_discoveries": "{count} scrobbles and {artistsCount} artists / {tracksCount} tracks discovered.", "statistics_recent_scrobbles_count_and_discoveries": "{count} scrobbles and {artistsCount} artists / {tracksCount} tracks discovered.",
......
...@@ -8,6 +8,8 @@ ...@@ -8,6 +8,8 @@
"global_statistics": "Statistiques globales d'écoutes", "global_statistics": "Statistiques globales d'écoutes",
"statistics_total_scrobbles_count": "Nombre total d'écoutes : {count}", "statistics_total_scrobbles_count": "Nombre total d'écoutes : {count}",
"statistics_last_scrobble": "Dernière écoute : {datetime}", "statistics_last_scrobble": "Dernière écoute : {datetime}",
"recent_statistics": "Statistiques récentes",
"statistics_selected_period": "Sur les {daysCount} derniers jours :", "statistics_selected_period": "Sur les {daysCount} derniers jours :",
"statistics_recent_scrobbles_count_and_discoveries": "{count} écoutes et {artistsCount} artistes / {tracksCount} morceaux découverts.", "statistics_recent_scrobbles_count_and_discoveries": "{count} écoutes et {artistsCount} artistes / {tracksCount} morceaux découverts.",
......
Split global and recent statistics.
Séparation des statistiques globales et récentes.
import 'dart:convert';
class StatisticsGlobalData {
final int totalCount;
final DateTime lastScrobble;
const StatisticsGlobalData({
required this.totalCount,
required this.lastScrobble,
});
factory StatisticsGlobalData.fromJson(Map<String, dynamic> json) {
return StatisticsGlobalData(
totalCount: json['totalCount'] != null ? json['totalCount'] as int : 0,
lastScrobble: (json['lastScrobble'] != null && json['lastScrobble']['date'] != null)
? DateTime.parse(
json['lastScrobble']['date'],
)
: DateTime.now(),
);
}
factory StatisticsGlobalData.createEmpty() {
return StatisticsGlobalData.fromJson({});
}
String toString() {
Map<String, dynamic> map = {
'totalCount': this.totalCount,
'lastScrobble': {
'date': this.lastScrobble.toString(),
},
};
return jsonEncode(map);
}
}
import 'dart:convert'; import 'dart:convert';
class StatisticsData { class StatisticsRecentData {
final int totalCount;
final int recentCount; final int recentCount;
final int firstPlayedArtistsCount; final int firstPlayedArtistsCount;
final int firstPlayedTracksCount; final int firstPlayedTracksCount;
final int selectedPeriod; final int selectedPeriod;
final DateTime lastScrobble;
const StatisticsData({ const StatisticsRecentData({
required this.totalCount,
required this.recentCount, required this.recentCount,
required this.firstPlayedArtistsCount, required this.firstPlayedArtistsCount,
required this.firstPlayedTracksCount, required this.firstPlayedTracksCount,
required this.selectedPeriod, required this.selectedPeriod,
required this.lastScrobble,
}); });
factory StatisticsData.fromJson(Map<String, dynamic> json) { factory StatisticsRecentData.fromJson(Map<String, dynamic> json) {
return StatisticsData( return StatisticsRecentData(
totalCount: json['totalCount'] != null ? json['totalCount'] as int : 0,
recentCount: json['recentCount'] != null ? json['recentCount'] as int : 0, recentCount: json['recentCount'] != null ? json['recentCount'] as int : 0,
firstPlayedArtistsCount: firstPlayedArtistsCount:
json['firstPlayedArtistsCount'] != null ? json['firstPlayedArtistsCount'] as int : 0, json['firstPlayedArtistsCount'] != null ? json['firstPlayedArtistsCount'] as int : 0,
firstPlayedTracksCount: firstPlayedTracksCount:
json['firstPlayedTracksCount'] != null ? json['firstPlayedTracksCount'] as int : 0, json['firstPlayedTracksCount'] != null ? json['firstPlayedTracksCount'] as int : 0,
selectedPeriod: json['selectedPeriod'] != null ? json['selectedPeriod'] as int : 0, selectedPeriod: json['selectedPeriod'] != null ? json['selectedPeriod'] as int : 0,
lastScrobble: (json['lastScrobble'] != null && json['lastScrobble']['date'] != null)
? DateTime.parse(
json['lastScrobble']['date'],
)
: DateTime.now(),
); );
} }
factory StatisticsData.createEmpty() { factory StatisticsRecentData.createEmpty() {
return StatisticsData.fromJson({}); return StatisticsRecentData.fromJson({});
} }
String toString() { String toString() {
Map<String, dynamic> map = { Map<String, dynamic> map = {
'totalCount': this.totalCount,
'recentCount': this.recentCount, 'recentCount': this.recentCount,
'firstPlayedArtistsCount': this.firstPlayedArtistsCount, 'firstPlayedArtistsCount': this.firstPlayedArtistsCount,
'firstPlayedTracksCount': this.firstPlayedTracksCount, 'firstPlayedTracksCount': this.firstPlayedTracksCount,
'selectedPeriod': this.selectedPeriod, 'selectedPeriod': this.selectedPeriod,
'lastScrobble': {
'date': this.lastScrobble.toString(),
},
}; };
return jsonEncode(map); return jsonEncode(map);
......
...@@ -4,19 +4,32 @@ import 'package:http/http.dart' as http; ...@@ -4,19 +4,32 @@ import 'package:http/http.dart' as http;
import '../models/counts_by_day.dart'; import '../models/counts_by_day.dart';
import '../models/counts_by_hour.dart'; import '../models/counts_by_hour.dart';
import '../models/discoveries.dart'; import '../models/discoveries.dart';
import '../models/statistics.dart'; import '../models/statistics_global.dart';
import '../models/statistics_recent.dart';
import '../models/timeline.dart'; import '../models/timeline.dart';
class ScrobblesApi { class ScrobblesApi {
static String baseUrl = 'https://scrobble.harrault.fr'; static String baseUrl = 'https://scrobble.harrault.fr';
static Future<StatisticsData> fetchStatistics(int daysCount) async { static Future<StatisticsGlobalData> fetchGlobalStatistics() async {
final String url = baseUrl + '/stats';
print('fetching ' + url);
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
return StatisticsGlobalData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
throw Exception('Failed to get data from API.');
}
}
static Future<StatisticsRecentData> fetchRecentStatistics(int daysCount) async {
final String url = baseUrl + '/' + daysCount.toString() + '/stats'; final String url = baseUrl + '/' + daysCount.toString() + '/stats';
print('fetching ' + url); print('fetching ' + url);
final response = await http.get(Uri.parse(url)); final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) { if (response.statusCode == 200) {
return StatisticsData.fromJson(jsonDecode(response.body) as Map<String, dynamic>); return StatisticsRecentData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else { } else {
throw Exception('Failed to get data from API.'); throw Exception('Failed to get data from API.');
} }
......
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../widgets/main_screen/statistics_card.dart'; import '../widgets/main_screen/statistics_global_card.dart';
import '../widgets/main_screen/statistics_recent_card.dart';
import '../widgets/main_screen/timeline_card.dart'; import '../widgets/main_screen/timeline_card.dart';
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
...@@ -20,7 +21,9 @@ class _HomeScreenState extends State<HomeScreen> { ...@@ -20,7 +21,9 @@ class _HomeScreenState extends State<HomeScreen> {
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
children: <Widget>[ children: <Widget>[
const SizedBox(height: 8), const SizedBox(height: 8),
const StatisticsCard(), const StatisticsGlobalCard(),
const SizedBox(height: 6),
const StatisticsRecentCard(),
const SizedBox(height: 6), const SizedBox(height: 6),
const ChartTimelineCard(), const ChartTimelineCard(),
const SizedBox(height: 36), const SizedBox(height: 36),
......
...@@ -2,21 +2,20 @@ import 'dart:convert'; ...@@ -2,21 +2,20 @@ import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../models/statistics.dart'; import '../../../models/statistics_global.dart';
import '../../../network/scrobbles_api.dart'; import '../../../network/scrobbles_api.dart';
import '../../../ui/widgets/error.dart'; import '../../../ui/widgets/error.dart';
import '../../../ui/widgets/main_screen/statistics_content.dart'; import 'statistics_global_content.dart';
class StatisticsCard extends StatelessWidget { class StatisticsGlobalCard extends StatelessWidget {
const StatisticsCard({super.key}); const StatisticsGlobalCard({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final int daysCount = 14; late Future<StatisticsGlobalData> future = ScrobblesApi.fetchGlobalStatistics();
late Future<StatisticsData> futureStatistics = ScrobblesApi.fetchStatistics(daysCount);
return FutureBuilder<StatisticsData>( return FutureBuilder<StatisticsGlobalData>(
future: futureStatistics, future: future,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasError) { if (snapshot.hasError) {
return ShowErrorWidget(message: '${snapshot.error}'); return ShowErrorWidget(message: '${snapshot.error}');
...@@ -33,10 +32,10 @@ class StatisticsCard extends StatelessWidget { ...@@ -33,10 +32,10 @@ class StatisticsCard extends StatelessWidget {
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: StatisticsContent( child: StatisticsGlobalContent(
statistics: snapshot.hasData statistics: snapshot.hasData
? StatisticsData.fromJson(jsonDecode(snapshot.data.toString())) ? StatisticsGlobalData.fromJson(jsonDecode(snapshot.data.toString()))
: StatisticsData.createEmpty(), : StatisticsGlobalData.createEmpty(),
isLoading: !snapshot.hasData, isLoading: !snapshot.hasData,
), ),
), ),
......
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import '../../../models/statistics_global.dart';
class StatisticsGlobalContent extends StatelessWidget {
final StatisticsGlobalData statistics;
final bool isLoading;
const StatisticsGlobalContent(
{super.key, required this.statistics, 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(
'global_statistics',
style: textTheme.titleLarge!.apply(fontWeightDelta: 2),
).tr(),
Text(
'statistics_total_scrobbles_count',
style: textTheme.bodyMedium,
).tr(
namedArgs: {
'count': this.isLoading ? placeholder : this.statistics.totalCount.toString(),
},
),
Text(
'statistics_last_scrobble',
style: textTheme.bodyMedium,
).tr(
namedArgs: {
'datetime': this.isLoading
? placeholder
: DateFormat().format(this.statistics.lastScrobble),
},
),
],
);
}
}
import 'dart:convert';
import 'package:flutter/material.dart';
import '../../../models/statistics_recent.dart';
import '../../../network/scrobbles_api.dart';
import '../../../ui/widgets/error.dart';
import 'statistics_recent_content.dart';
class StatisticsRecentCard extends StatelessWidget {
const StatisticsRecentCard({super.key});
@override
Widget build(BuildContext context) {
final int daysCount = 21;
late Future<StatisticsRecentData> future = ScrobblesApi.fetchRecentStatistics(daysCount);
return FutureBuilder<StatisticsRecentData>(
future: future,
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.primary,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(8),
),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: StatisticsRecentContent(
statistics: snapshot.hasData
? StatisticsRecentData.fromJson(jsonDecode(snapshot.data.toString()))
: StatisticsRecentData.createEmpty(),
isLoading: !snapshot.hasData,
),
),
);
},
);
}
}
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../models/statistics.dart'; import '../../../models/statistics_recent.dart';
class StatisticsContent extends StatelessWidget { class StatisticsRecentContent extends StatelessWidget {
final StatisticsData statistics; final StatisticsRecentData statistics;
final bool isLoading; final bool isLoading;
const StatisticsContent({super.key, required this.statistics, required this.isLoading}); const StatisticsRecentContent(
{super.key, required this.statistics, required this.isLoading});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
...@@ -20,27 +21,9 @@ class StatisticsContent extends StatelessWidget { ...@@ -20,27 +21,9 @@ class StatisticsContent extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Text( Text(
'global_statistics', 'recent_statistics',
style: textTheme.titleLarge!.apply(fontWeightDelta: 2), style: textTheme.titleLarge!.apply(fontWeightDelta: 2),
).tr(), ).tr(),
Text(
'statistics_total_scrobbles_count',
style: textTheme.bodyMedium,
).tr(
namedArgs: {
'count': this.isLoading ? placeholder : this.statistics.totalCount.toString(),
},
),
Text(
'statistics_last_scrobble',
style: textTheme.bodyMedium,
).tr(
namedArgs: {
'datetime': this.isLoading
? placeholder
: DateFormat().format(this.statistics.lastScrobble),
},
),
Text( Text(
'statistics_selected_period', 'statistics_selected_period',
style: textTheme.bodyMedium!.apply(fontWeightDelta: 2), style: textTheme.bodyMedium!.apply(fontWeightDelta: 2),
......
...@@ -3,7 +3,7 @@ description: Display scrobbles data and charts ...@@ -3,7 +3,7 @@ description: Display scrobbles data and charts
publish_to: 'none' publish_to: 'none'
version: 0.0.23+23 version: 0.0.24+24
environment: environment:
sdk: '^3.0.0' sdk: '^3.0.0'
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment