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

Merge branch '4-get-data-from-api-and-display-charts' into 'master'

Resolve "Get data from API and display charts"

Closes #4

See merge request !4
parents d8a440e2 63999e17
No related branches found
No related tags found
1 merge request!4Resolve "Get data from API and display charts"
Pipeline #4441 passed
Showing with 447 additions and 5 deletions
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
app.versionName=0.0.6
app.versionCode=6
app.versionName=0.0.7
app.versionCode=7
......@@ -6,5 +6,7 @@
"statistics_last_scrobble": "Last scrobble: {datetime}",
"statistics_selected_period": "On last {daysCount} days:",
"statistics_recent_scrobbles_count": "Scrobbles: {count}",
"statistics_discoveries": "Discoveries: {artistsCount} artists / {tracksCount} tracks"
"statistics_discoveries": "Discoveries: {artistsCount} artists / {tracksCount} tracks",
"timeline_title": "Recent scrobbles ({daysCount} days)"
}
......@@ -6,5 +6,7 @@
"statistics_last_scrobble": "Dernière écoute : {datetime}",
"statistics_selected_period": "Sur les {daysCount} derniers jours:",
"statistics_recent_scrobbles_count": "Écoutes : {count}",
"statistics_discoveries": "Découvertes : {artistsCount} artistes / {tracksCount} morceaux"
"statistics_discoveries": "Découvertes : {artistsCount} artistes / {tracksCount} morceaux",
"timeline_title": "Écoutes récentes ({daysCount} jours)"
}
Add scrobbles counts main chart.
Ajout du graphique principal de compteur d'écoutes.
import 'package:flutter/material.dart';
class AppColors {
static const Color primary = contentColorCyan;
static const Color menuBackground = Color(0xFF090912);
static const Color itemsBackground = Color(0xFF1B2339);
static const Color pageBackground = Color(0xFF282E45);
static const Color mainTextColor1 = Colors.white;
static const Color mainTextColor2 = Colors.white70;
static const Color mainTextColor3 = Colors.white38;
static const Color mainGridLineColor = Colors.white10;
static const Color borderColor = Colors.white54;
static const Color gridLinesColor = Color(0x11FFFFFF);
static const Color contentColorBlack = Colors.black;
static const Color contentColorWhite = Colors.white;
static const Color contentColorBlue = Color(0xFF2196F3);
static const Color contentColorYellow = Color(0xFFFFC300);
static const Color contentColorOrange = Color(0xFFFF683B);
static const Color contentColorGreen = Color(0xFF3BFF49);
static const Color contentColorPurple = Color(0xFF6E1BFF);
static const Color contentColorPink = Color(0xFFFF3AF2);
static const Color contentColorRed = Color(0xFFE80054);
static const Color contentColorCyan = Color(0xFF50E4FF);
}
import 'dart:convert';
class TimelineDataValue {
final int counts;
final int eclecticism;
const TimelineDataValue({required this.counts, required this.eclecticism});
factory TimelineDataValue.fromJson(Map<String, dynamic> json) {
return TimelineDataValue(
counts: json['counts'] as int,
eclecticism: json['eclecticism'] as int,
);
}
}
class TimelineData {
final Map<String, TimelineDataValue> data;
const TimelineData({
required this.data,
});
factory TimelineData.fromJson(Map<String, dynamic> json) {
Map<String, TimelineDataValue> data = {};
json.keys.forEach((date) {
TimelineDataValue value = TimelineDataValue(
counts: json[date]['counts'] as int,
eclecticism: json[date]['eclecticism'] as int,
);
data[date] = value;
});
return TimelineData(data: data);
}
factory TimelineData.createEmpty() {
return TimelineData.fromJson({});
}
String toString() {
Map<String, Map<String, int>> map = {};
this.data.keys.forEach((element) {
TimelineDataValue? item = this.data[element];
map[element] = {
'counts': item != null ? item.counts : 0,
'eclecticism': item != null ? item.eclecticism : 0,
};
});
return jsonEncode(map);
}
}
......@@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/statistics.dart';
import '../models/timeline.dart';
class ScrobblesApi {
static String baseUrl = 'https://scrobble.harrault.fr';
......@@ -15,4 +16,15 @@ class ScrobblesApi {
throw Exception('Failed to get data from API.');
}
}
static Future<TimelineData> fetchTimeline(int daysCount) async {
final String url = baseUrl + '/data/' + daysCount.toString() + '/timeline';
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
return TimelineData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
throw Exception('Failed to get data from API.');
}
}
}
......@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import '../widgets/header.dart';
import '../widgets/main_screen/statistics_card.dart';
import '../widgets/main_screen/timeline_card.dart';
class MainScreen extends StatelessWidget {
const MainScreen({super.key});
......@@ -16,6 +17,8 @@ class MainScreen extends StatelessWidget {
children: <Widget>[
const Header(text: 'app_name'),
const StatisticsCard(),
const SizedBox(height: 8),
const ChartTimelineCard(),
const SizedBox(height: 36),
],
),
......
import 'dart:convert';
import 'package:flutter/material.dart';
import '../../../models/timeline.dart';
import '../../../network/scrobbles_api.dart';
import '../../../ui/widgets/error.dart';
import '../../../ui/widgets/main_screen/timeline_content.dart';
class ChartTimelineCard extends StatelessWidget {
const ChartTimelineCard({super.key});
@override
Widget build(BuildContext context) {
final int daysCount = 14;
late Future<TimelineData> futureTimeline = ScrobblesApi.fetchTimeline(daysCount);
return FutureBuilder<TimelineData>(
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.primary,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: ChartTimelineCardContent(
daysCount: daysCount,
chartData: snapshot.hasData
? TimelineData.fromJson(jsonDecode(snapshot.data.toString()))
: TimelineData.createEmpty(),
isLoading: !snapshot.hasData,
),
),
);
},
);
}
}
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import '../../../config/app_colors.dart';
import '../../../models/timeline.dart';
import '../../../utils/color_extensions.dart';
class ChartTimelineCounts extends StatelessWidget {
final TimelineData chartData;
const ChartTimelineCounts({super.key, required this.chartData});
@override
Widget build(BuildContext context) {
return Container(
height: 150.0,
child: LayoutBuilder(builder: (context, constraints) {
final double maxWidth = constraints.maxWidth;
final int barsCount = this.chartData.data.keys.length;
final double barWidth = 0.7 * (maxWidth / barsCount);
return BarChart(
BarChartData(
barGroups: getDataCounts(barWidth),
backgroundColor: Theme.of(context).colorScheme.onBackground,
borderData: getBorderData(),
gridData: getGridData(),
titlesData: getTitlesData(),
barTouchData: getBarTouchData(),
maxY: getNextRoundNumber(getMaxCountsValue(), 50),
),
);
}),
);
}
double getMaxCountsValue() {
double maxValue = 0;
this.chartData.data.keys.forEach((key) {
TimelineDataValue? value = this.chartData.data[key];
if (value != null) {
double counts = value.counts.toDouble();
if (counts > maxValue) {
maxValue = counts;
}
}
});
return maxValue;
}
double getNextRoundNumber(double number, int scale) {
return scale * ((number ~/ scale).toInt() + 1);
}
List<BarChartGroupData> getDataCounts(double barWidth) {
List<BarChartGroupData> data = [];
this.chartData.data.keys.forEach((key) {
TimelineDataValue? value = this.chartData.data[key];
if (value != null) {
final int date = DateTime.parse(key).millisecondsSinceEpoch;
final double counts = value.counts.toDouble();
data.add(BarChartGroupData(
x: date,
barRods: [
BarChartRodData(
toY: counts,
color: AppColors.contentColorOrange,
width: barWidth,
borderRadius: BorderRadius.all(Radius.zero),
borderSide: BorderSide(
color: AppColors.contentColorOrange.darken(20),
),
),
],
));
}
});
return data;
}
FlBorderData getBorderData() {
return FlBorderData(
show: true,
border: Border.all(
color: AppColors.borderColor,
width: 2,
),
);
}
FlGridData getGridData() {
return const FlGridData(
show: true,
);
}
FlTitlesData getTitlesData() {
const AxisTitles none = const AxisTitles(
sideTitles: SideTitles(showTitles: false),
);
final AxisTitles verticalTitles = AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
getTitlesWidget: getVerticalTitlesWidget,
),
);
final AxisTitles horizontalTitles = AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 20,
getTitlesWidget: getHorizontalTitlesWidget,
),
);
return FlTitlesData(
show: true,
bottomTitles: horizontalTitles,
leftTitles: none,
topTitles: none,
rightTitles: verticalTitles,
);
}
Widget getVerticalTitlesWidget(double value, TitleMeta meta) {
return SideTitleWidget(
axisSide: meta.axisSide,
space: 4,
child: Text(
value.toInt().toString(),
style: const TextStyle(
color: AppColors.mainTextColor1,
fontSize: 12,
),
),
);
}
Widget getHorizontalTitlesWidget(double value, TitleMeta meta) {
final DateFormat formatter = DateFormat('dd/MM');
final DateTime date = DateTime.fromMillisecondsSinceEpoch(value.toInt());
final String text = formatter.format(date);
return SideTitleWidget(
axisSide: meta.axisSide,
space: 4,
child: RotationTransition(
turns: new AlwaysStoppedAnimation(-30 / 360),
child: Text(
text,
style: const TextStyle(
color: AppColors.mainTextColor1,
fontSize: 11,
),
),
),
);
}
BarTouchData getBarTouchData() {
return BarTouchData(
enabled: true,
touchTooltipData: BarTouchTooltipData(
tooltipBgColor: Colors.transparent,
tooltipPadding: EdgeInsets.zero,
tooltipMargin: 2,
getTooltipItem: (
BarChartGroupData group,
int groupIndex,
BarChartRodData rod,
int rodIndex,
) {
return BarTooltipItem(
rod.toY.round().toString(),
const TextStyle(
color: AppColors.mainTextColor2,
fontWeight: FontWeight.bold,
fontSize: 10,
),
);
},
),
);
}
}
import 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import '../../../models/timeline.dart';
import 'timeline_chart_counts.dart';
class ChartTimelineCardContent extends StatelessWidget {
final int daysCount;
final TimelineData chartData;
final bool isLoading;
const ChartTimelineCardContent(
{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(
'timeline_title',
style: textTheme.titleLarge!.apply(fontWeightDelta: 2),
).tr(
namedArgs: {
'daysCount': this.daysCount.toString(),
},
),
const SizedBox(height: 8),
this.isLoading
? Text(placeholder)
: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ChartTimelineCounts(
chartData: TimelineData.fromJson(jsonDecode(this.chartData.toString())),
),
],
),
],
);
}
}
import 'dart:ui';
extension ColorExtension on Color {
Color darken([int percent = 40]) {
assert(1 <= percent && percent <= 100);
final value = 1 - percent / 100;
return Color.fromARGB(
alpha,
(red * value).round(),
(green * value).round(),
(blue * value).round(),
);
}
Color lighten([int percent = 40]) {
assert(1 <= percent && percent <= 100);
final value = percent / 100;
return Color.fromARGB(
alpha,
(red + ((255 - red) * value)).round(),
(green + ((255 - green) * value)).round(),
(blue + ((255 - blue) * value)).round(),
);
}
Color avg(Color other) {
final red = (this.red + other.red) ~/ 2;
final green = (this.green + other.green) ~/ 2;
final blue = (this.blue + other.blue) ~/ 2;
final alpha = (this.alpha + other.alpha) ~/ 2;
return Color.fromARGB(alpha, red, green, blue);
}
}
......@@ -57,6 +57,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.0.2"
equatable:
dependency: transitive
description:
name: equatable
sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2
url: "https://pub.dev"
source: hosted
version: "2.0.5"
ffi:
dependency: transitive
description:
......@@ -73,6 +81,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.0"
fl_chart:
dependency: "direct main"
description:
name: fl_chart
sha256: "6b9eb2b3017241d05c482c01f668dd05cc909ec9a0114fdd49acd958ff2432fa"
url: "https://pub.dev"
source: hosted
version: "0.64.0"
flutter:
dependency: "direct main"
description: flutter
......
......@@ -3,7 +3,7 @@ description: Display scrobbles data and charts
publish_to: 'none'
version: 0.0.6+6
version: 0.0.7+7
environment:
sdk: '^3.0.0'
......@@ -14,6 +14,7 @@ dependencies:
easy_localization: ^3.0.1
http: ^1.1.0
fl_chart: ^0.64.0
flutter:
uses-material-design: false
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment