Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • android/org.benoitharrault.scrobbles
1 result
Show changes
Showing
with 1051 additions and 735 deletions
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:scrobbles/config/app_colors.dart';
import 'package:scrobbles/models/counts_by_day.dart';
import 'package:scrobbles/ui/widgets/abstracts/custom_bar_chart.dart';
class ChartCountsByDay extends CustomBarChart {
final CountsByDayData chartData;
final bool isLoading;
ChartCountsByDay({super.key, required this.chartData, required this.isLoading});
final double verticalTicksInterval = 5;
final String verticalAxisTitleSuffix = '%';
@override
Widget build(BuildContext context) {
if (this.chartData.data.keys.length == 0) {
return SizedBox(
height: this.chartHeight,
);
}
return Container(
height: this.chartHeight,
child: this.isLoading
? this.placeholder
: LayoutBuilder(
builder: (context, constraints) {
final double maxWidth = constraints.maxWidth;
final int barsCount = this.chartData.data.keys.length;
return getBarChart(
barWidth: this.getBarWidth(maxWidth, barsCount),
backgroundColor: Theme.of(context).colorScheme.onSurface,
);
},
),
);
}
double getMaxCountsValue() {
double maxValue = 0;
this.chartData.data.keys.forEach((key) {
double? counts = this.chartData.data[key];
if (counts != null) {
if (counts > maxValue) {
maxValue = counts;
}
}
});
return maxValue;
}
Color getColorFromIndex(int index) {
Color barColor = AppColors.contentColorBlack;
switch (index) {
case 1:
barColor = Color.fromARGB(255, 255, 99, 132);
break;
case 2:
barColor = Color.fromARGB(255, 255, 159, 64);
break;
case 3:
barColor = Color.fromARGB(255, 255, 205, 86);
break;
case 4:
barColor = Color.fromARGB(255, 75, 192, 192);
break;
case 5:
barColor = Color.fromARGB(255, 54, 162, 235);
break;
case 6:
barColor = Color.fromARGB(255, 153, 102, 255);
break;
case 7:
barColor = Color.fromARGB(255, 201, 203, 207);
break;
default:
}
return barColor;
}
List<BarChartGroupData> getDataCounts(double barWidth) {
List<BarChartGroupData> data = [];
this.chartData.data.keys.forEach((day) {
final double? counts = this.chartData.data[day];
if (counts != null) {
final Color barColor = this.getColorFromIndex(day);
data.add(this.getBarItem(
x: day,
values: [counts],
barColors: [barColor],
barWidth: barWidth,
));
}
});
return data;
}
Widget getHorizontalTitlesWidget(double value, TitleMeta meta) {
final int day = value.toInt();
String dayShortName = '';
switch (day) {
case 1:
dayShortName = 'MON';
break;
case 2:
dayShortName = 'TUE';
break;
case 3:
dayShortName = 'WED';
break;
case 4:
dayShortName = 'THU';
break;
case 5:
dayShortName = 'FRI';
break;
case 6:
dayShortName = 'SAT';
break;
case 7:
dayShortName = 'SUN';
break;
default:
}
return SideTitleWidget(
axisSide: meta.axisSide,
space: 2,
child: Text(
tr(dayShortName),
style: const TextStyle(
color: AppColors.mainTextColor1,
fontSize: 11,
),
),
);
}
}
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:scrobbles/config/app_colors.dart';
import 'package:scrobbles/models/counts_by_hour.dart';
import 'package:scrobbles/ui/widgets/abstracts/custom_bar_chart.dart';
import 'package:scrobbles/utils/color_extensions.dart';
class ChartCountsByHour extends CustomBarChart {
final CountsByHourData chartData;
final bool isLoading;
ChartCountsByHour({super.key, required this.chartData, required this.isLoading});
final double verticalTicksInterval = 5;
final String verticalAxisTitleSuffix = '%';
@override
Widget build(BuildContext context) {
if (this.chartData.data.keys.length == 0) {
return SizedBox(
height: this.chartHeight,
);
}
return Container(
height: this.chartHeight,
child: this.isLoading
? this.placeholder
: LayoutBuilder(
builder: (context, constraints) {
final double maxWidth = constraints.maxWidth;
final int barsCount = this.chartData.data.keys.length;
return getBarChart(
barWidth: this.getBarWidth(maxWidth, barsCount),
backgroundColor: Theme.of(context).colorScheme.onSurface,
);
},
),
);
}
double getMaxCountsValue() {
double maxValue = 0;
this.chartData.data.keys.forEach((key) {
double? counts = this.chartData.data[key];
if (counts != null) {
if (counts > maxValue) {
maxValue = counts;
}
}
});
return maxValue;
}
List<BarChartGroupData> getDataCounts(double barWidth) {
List<BarChartGroupData> data = [];
final barColor = AppColors.contentColorCyan.darken(30);
this.chartData.data.keys.forEach((day) {
final double? counts = this.chartData.data[day];
if (counts != null) {
data.add(getBarItem(
x: day,
values: [counts],
barColors: [barColor],
barWidth: barWidth,
));
}
});
return data;
}
Widget getHorizontalTitlesWidget(double value, TitleMeta meta) {
String text = '';
if (value % 4 == 0 || value == 23) {
text = value.toInt().toString().padLeft(2, '0');
}
return SideTitleWidget(
axisSide: meta.axisSide,
space: 2,
child: Text(
text,
style: const TextStyle(
color: AppColors.mainTextColor1,
fontSize: 11,
),
),
);
}
}
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:scrobbles/config/app_colors.dart';
import 'package:scrobbles/models/discoveries.dart';
import 'package:scrobbles/ui/widgets/abstracts/custom_bar_chart.dart';
import 'package:scrobbles/utils/color_extensions.dart';
class ChartDiscoveriesArtists extends CustomBarChart {
final DiscoveriesData chartData;
final bool isLoading;
ChartDiscoveriesArtists({super.key, required this.chartData, required this.isLoading});
@override
Widget build(BuildContext context) {
if (this.chartData.data.keys.length == 0) {
return SizedBox(
height: this.chartHeight,
);
}
return Container(
height: this.chartHeight,
child: this.isLoading
? this.placeholder
: LayoutBuilder(
builder: (context, constraints) {
final double maxWidth = constraints.maxWidth;
final int barsCount = this.chartData.data.keys.length;
return getBarChart(
barWidth: this.getBarWidth(maxWidth, barsCount),
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();
if (newArtistsCount > maxValue) {
maxValue = newArtistsCount;
}
}
});
return maxValue;
}
List<BarChartGroupData> getDataCounts(double barWidth) {
List<BarChartGroupData> data = [];
final newArtistsBarColor = AppColors.contentColorGreen.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(),
],
barColors: [
newArtistsBarColor,
],
barWidth: barWidth,
));
}
});
return data;
}
}
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:scrobbles/config/app_colors.dart';
import 'package:scrobbles/models/discoveries.dart';
import 'package:scrobbles/ui/widgets/abstracts/custom_bar_chart.dart';
import 'package:scrobbles/utils/color_extensions.dart';
class ChartDiscoveriesTracks extends CustomBarChart {
final DiscoveriesData chartData;
final bool isLoading;
ChartDiscoveriesTracks({super.key, required this.chartData, required this.isLoading});
@override
Widget build(BuildContext context) {
if (this.chartData.data.keys.length == 0) {
return SizedBox(
height: this.chartHeight,
);
}
return Container(
height: this.chartHeight,
child: this.isLoading
? this.placeholder
: LayoutBuilder(
builder: (context, constraints) {
final double maxWidth = constraints.maxWidth;
final int barsCount = this.chartData.data.keys.length;
return getBarChart(
barWidth: this.getBarWidth(maxWidth, barsCount),
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 newTracksCount = value.newTracksCount.toDouble();
if (newTracksCount > maxValue) {
maxValue = newTracksCount;
}
}
});
return maxValue;
}
List<BarChartGroupData> getDataCounts(double barWidth) {
List<BarChartGroupData> data = [];
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.newTracksCount.toDouble(),
],
barColors: [
newTracksBarColor,
],
barWidth: barWidth,
));
}
});
return data;
}
}
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:scrobbles/config/app_colors.dart';
import 'package:scrobbles/models/timeline.dart';
import 'package:scrobbles/ui/widgets/abstracts/custom_bar_chart.dart';
class ChartTimelineCounts extends CustomBarChart {
final TimelineData chartData;
final bool isLoading;
ChartTimelineCounts({super.key, required this.chartData, required this.isLoading});
final double verticalTicksInterval = 50;
@override
Widget build(BuildContext context) {
if (this.chartData.data.keys.length == 0) {
return SizedBox(
height: this.chartHeight,
);
}
return Container(
height: this.chartHeight,
child: this.isLoading
? this.placeholder
: LayoutBuilder(
builder: (context, constraints) {
final double maxWidth = constraints.maxWidth;
final int barsCount = this.chartData.data.keys.length;
return getBarChart(
barWidth: this.getBarWidth(maxWidth, barsCount),
backgroundColor: Theme.of(context).colorScheme.onSurface,
);
},
),
);
}
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;
}
List<BarChartGroupData> getDataCounts(double barWidth) {
List<BarChartGroupData> data = [];
final barColor = AppColors.contentColorOrange;
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(this.getBarItem(
x: date,
values: [counts],
barColors: [barColor],
barWidth: barWidth,
));
}
});
return data;
}
}
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:scrobbles/config/app_colors.dart';
import 'package:scrobbles/models/timeline.dart';
import 'package:scrobbles/ui/widgets/abstracts/custom_line_chart.dart';
import 'package:scrobbles/utils/color_extensions.dart';
class ChartTimelineEclecticism extends CustomLineChart {
final TimelineData chartData;
final bool isLoading;
ChartTimelineEclecticism({super.key, required this.chartData, required this.isLoading});
@override
Widget build(BuildContext context) {
final horizontalScale = getHorizontalScale();
if (this.chartData.data.keys.length == 0) {
return SizedBox(
height: this.chartHeight,
);
}
return Container(
height: this.chartHeight,
child: this.isLoading
? this.placeholder
: LineChart(
LineChartData(
lineBarsData: getDataEclecticism(),
borderData: getBorderData(),
gridData: getGridData(),
titlesData: getTitlesData(),
lineTouchData: const LineTouchData(enabled: false),
minX: horizontalScale['min'],
maxX: horizontalScale['max'],
maxY: 100,
minY: 0,
),
duration: const Duration(milliseconds: 250),
),
);
}
Map<String, double> getHorizontalScale() {
// Left/right margins: 12h, in milliseconds
int margin = 12 * 60 * 60 * 1000;
double minDateAsDouble = double.maxFinite;
double maxDateAsDouble = -double.maxFinite;
this.chartData.data.keys.forEach((element) {
TimelineDataValue? value = this.chartData.data[element];
if (value != null) {
final double date = DateTime.parse(element).millisecondsSinceEpoch.toDouble();
if (date < minDateAsDouble) {
minDateAsDouble = date;
}
if (date > maxDateAsDouble) {
maxDateAsDouble = date;
}
}
});
return {
'min': minDateAsDouble - margin,
'max': maxDateAsDouble + margin,
};
}
List<LineChartBarData> getDataEclecticism() {
List<FlSpot> spots = [];
this.chartData.data.keys.forEach((element) {
TimelineDataValue? value = this.chartData.data[element];
if (value != null) {
final double date = DateTime.parse(element).millisecondsSinceEpoch.toDouble();
final double eclecticism = value.eclecticism.toDouble();
spots.add(FlSpot(date, eclecticism));
}
});
final baseColor = AppColors.contentColorCyan;
final borderColor = baseColor.darken(20);
return [
LineChartBarData(
isCurved: true,
color: borderColor,
barWidth: 2,
isStrokeCapRound: false,
dotData: const FlDotData(show: false),
spots: spots,
),
];
}
}
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:scrobbles/config/app_colors.dart';
import 'package:scrobbles/models/topartists.dart';
import 'package:scrobbles/utils/color_extensions.dart';
class ChartTopArtists extends StatelessWidget {
final TopArtistsData chartData;
final bool isLoading;
ChartTopArtists({super.key, required this.chartData, required this.isLoading});
final Widget placeholder = Text('⏳');
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 2.1,
child: this.isLoading
? this.placeholder
: Row(
children: <Widget>[
Expanded(
child: AspectRatio(
aspectRatio: 1,
child: PieChart(
PieChartData(
sections: getPieChartData(),
sectionsSpace: 2,
centerSpaceRadius: 40,
startDegreeOffset: -45,
pieTouchData: PieTouchData(enabled: false),
borderData: FlBorderData(show: false),
),
),
),
),
buildLegendWidget(),
],
),
);
}
Color getColorIndex(int index) {
const List<int> hexValues = [
0x8dd3c7,
0xffffb3,
0xbebada,
0xfb8072,
0x80b1d3,
0xfdb462,
0xb3de69,
0xfccde5,
0xd9d9d9,
0xbc80bd,
0xccebc5,
0xffed6f,
];
return Color(hexValues[index % hexValues.length] + 0xff000000);
}
List<PieChartSectionData> getPieChartData() {
List<PieChartSectionData> items = [];
final radius = 40.0;
final fontSize = 11.0;
const shadows = [Shadow(color: Colors.black, blurRadius: 2)];
int index = 0;
this.chartData.topArtists.forEach((element) {
Color baseColor = getColorIndex(index++);
final PieChartSectionData item = PieChartSectionData(
value: element.count.toDouble(),
title: element.artistName,
color: baseColor.darken(20),
borderSide: BorderSide(
color: baseColor.darken(40),
width: 1,
),
radius: radius,
titleStyle: TextStyle(
fontSize: fontSize,
color: AppColors.mainTextColor1,
shadows: shadows,
),
);
items.add(item);
});
return items;
}
Widget buildLegendWidget() {
const double itemSize = 12;
List<Widget> items = [];
int index = 0;
this.chartData.topArtists.forEach((element) {
Color baseColor = getColorIndex(index++);
final Widget item = Row(
children: <Widget>[
Container(
width: itemSize,
height: itemSize,
decoration: BoxDecoration(
shape: BoxShape.rectangle,
color: baseColor.darken(20),
border: Border.all(
color: baseColor.darken(40),
width: 1,
),
),
),
const SizedBox(
width: 4,
),
Text(
element.artistName + ' (' + element.count.toString() + ')',
style: TextStyle(
fontSize: itemSize - 2,
fontWeight: FontWeight.bold,
),
)
],
);
items.add(item);
});
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: items,
);
}
}
import 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import '../../../models/counts_by_day.dart';
import '../../../ui/widgets/main_screen/counts_by_day_chart.dart';
import 'package:scrobbles/models/statistics_global.dart';
class ChartCountsByDayCardContent extends StatelessWidget {
final int daysCount;
final CountsByDayData chartData;
class ContentStatisticsGlobal extends StatelessWidget {
final StatisticsGlobalData statistics;
final bool isLoading;
const ChartCountsByDayCardContent(
{super.key, required this.daysCount, required this.chartData, required this.isLoading});
const ContentStatisticsGlobal(
{super.key, required this.statistics, required this.isLoading});
@override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).primaryTextTheme;
final String placeholder = '⏳';
const String placeholder = '⏳';
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'counts_by_day',
style: textTheme.titleLarge!.apply(fontWeightDelta: 2),
'statistics_total_scrobbles_count',
style: textTheme.bodyMedium,
).tr(
namedArgs: {
'count': this.isLoading
? placeholder
: (this.statistics.totalCount != null
? this.statistics.totalCount.toString()
: ''),
},
),
Text(
'statistics_last_scrobble',
style: textTheme.bodyMedium,
).tr(
namedArgs: {
'daysCount': this.daysCount.toString(),
'datetime': this.isLoading
? placeholder
: (this.statistics.lastScrobble != null
? DateFormat().format(this.statistics.lastScrobble ?? DateTime.now())
: ''),
},
),
const SizedBox(height: 8),
this.isLoading
? Text(placeholder)
: CountsByDayCardContentChart(
chartData: CountsByDayData.fromJson(jsonDecode(this.chartData.toString())),
),
],
);
}
......
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import '../../../models/statistics.dart';
import 'package:scrobbles/models/statistics_recent.dart';
class StatisticsContent extends StatelessWidget {
final StatisticsData statistics;
class ContentStatisticsRecent extends StatelessWidget {
final StatisticsRecentData statistics;
final bool isLoading;
const StatisticsContent({super.key, required this.statistics, required this.isLoading});
const ContentStatisticsRecent(
{super.key, required this.statistics, required this.isLoading});
@override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).primaryTextTheme;
final String placeholder = '⏳';
const 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),
},
),
Text(
'statistics_selected_period',
style: textTheme.bodyMedium,
).tr(
namedArgs: {
'daysCount':
this.isLoading ? placeholder : this.statistics.selectedPeriod.toString(),
},
),
Text(
'statistics_recent_scrobbles_count',
(this.statistics.recentCount != null &&
this.statistics.firstPlayedArtistsCount != null &&
this.statistics.firstPlayedTracksCount != null)
? 'statistics_recent_scrobbles_count_and_discoveries'
: '',
style: textTheme.bodyMedium,
).tr(
namedArgs: {
'count': this.isLoading ? placeholder : this.statistics.recentCount.toString(),
},
),
Text(
'statistics_discoveries',
style: textTheme.bodyMedium,
).tr(
namedArgs: {
'artistsCount': this.isLoading
? placeholder
: this.statistics.firstPlayedArtistsCount.toString(),
......
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
class Header extends StatelessWidget {
const Header({super.key, required this.text});
class AppHeader extends StatelessWidget {
const AppHeader({super.key, required this.text});
final String text;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 2, right: 2, top: 48, bottom: 24),
child: Text(
tr(text),
textAlign: TextAlign.start,
style: Theme.of(context).textTheme.headlineMedium!.apply(fontWeightDelta: 2),
),
return Text(
tr(text),
textAlign: TextAlign.start,
style: Theme.of(context).textTheme.headlineMedium!.apply(fontWeightDelta: 2),
);
}
}
import 'dart:convert';
import 'package:flutter/material.dart';
import '../../../models/counts_by_day.dart';
import '../../../network/scrobbles_api.dart';
import '../../../ui/widgets/error.dart';
import '../../../ui/widgets/main_screen/counts_by_day_content.dart';
class ChartCountsByDayCard extends StatelessWidget {
const ChartCountsByDayCard({super.key});
@override
Widget build(BuildContext context) {
final int daysCount = 21;
late Future<CountsByDayData> futureCountsByDay = ScrobblesApi.fetchCountsByDay(daysCount);
return FutureBuilder<CountsByDayData>(
future: futureCountsByDay,
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: ChartCountsByDayCardContent(
daysCount: daysCount,
chartData: snapshot.hasData
? CountsByDayData.fromJson(jsonDecode(snapshot.data.toString()))
: CountsByDayData.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/counts_by_day.dart';
import '../../../utils/color_extensions.dart';
class CountsByDayCardContentChart extends StatelessWidget {
final CountsByDayData chartData;
const CountsByDayCardContentChart({super.key, required this.chartData});
@override
Widget build(BuildContext context) {
return Container(
height: 100.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(), 10),
),
);
}),
);
}
double getMaxCountsValue() {
double maxValue = 0;
this.chartData.data.keys.forEach((key) {
double? counts = this.chartData.data[key];
if (counts != null) {
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((day) {
final double? counts = this.chartData.data[day];
if (counts != null) {
Color barColor = AppColors.contentColorBlack;
switch (day) {
case 1:
barColor = Color.fromARGB(255, 255, 99, 132);
break;
case 2:
barColor = Color.fromARGB(255, 255, 159, 64);
break;
case 3:
barColor = Color.fromARGB(255, 255, 205, 86);
break;
case 4:
barColor = Color.fromARGB(255, 75, 192, 192);
break;
case 5:
barColor = Color.fromARGB(255, 54, 162, 235);
break;
case 6:
barColor = Color.fromARGB(255, 153, 102, 255);
break;
case 7:
barColor = Color.fromARGB(255, 201, 203, 207);
break;
default:
}
data.add(BarChartGroupData(
x: day,
barRods: [
BarChartRodData(
toY: counts,
color: barColor,
width: barWidth,
borderRadius: BorderRadius.all(Radius.zero),
borderSide: BorderSide(
color: barColor.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,
interval: 10,
),
);
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 int day = value.toInt();
String dayShortName = '';
switch (day) {
case 1:
dayShortName = 'MON';
break;
case 2:
dayShortName = 'TUE';
break;
case 3:
dayShortName = 'WED';
break;
case 4:
dayShortName = 'THU';
break;
case 5:
dayShortName = 'FRI';
break;
case 6:
dayShortName = 'SAT';
break;
case 7:
dayShortName = 'SUN';
break;
default:
}
final String text = tr(dayShortName);
return SideTitleWidget(
axisSide: meta.axisSide,
space: 2,
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:flutter/material.dart';
import '../../../models/counts_by_hour.dart';
import '../../../network/scrobbles_api.dart';
import '../../../ui/widgets/error.dart';
import '../../../ui/widgets/main_screen/counts_by_hour_content.dart';
class ChartCountsByHourCard extends StatelessWidget {
const ChartCountsByHourCard({super.key});
@override
Widget build(BuildContext context) {
final int daysCount = 21;
late Future<CountsByHourData> futureCountsByHour =
ScrobblesApi.fetchCountsByHour(daysCount);
return FutureBuilder<CountsByHourData>(
future: futureCountsByHour,
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: ChartCountsByHourCardContent(
daysCount: daysCount,
chartData: snapshot.hasData
? CountsByHourData.fromJson(jsonDecode(snapshot.data.toString()))
: CountsByHourData.createEmpty(),
isLoading: !snapshot.hasData,
),
),
);
},
);
}
}
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import '../../../config/app_colors.dart';
import '../../../models/counts_by_hour.dart';
import '../../../utils/color_extensions.dart';
class CountsByHourCardContentChart extends StatelessWidget {
final CountsByHourData chartData;
const CountsByHourCardContentChart({super.key, required this.chartData});
@override
Widget build(BuildContext context) {
return Container(
height: 100.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(), 5),
),
);
}),
);
}
double getMaxCountsValue() {
double maxValue = 0;
this.chartData.data.keys.forEach((key) {
double? counts = this.chartData.data[key];
if (counts != null) {
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((day) {
final double? counts = this.chartData.data[day];
if (counts != null) {
final Color barColor = AppColors.contentColorCyan.darken(30);
data.add(BarChartGroupData(
x: day,
barRods: [
BarChartRodData(
toY: counts,
color: barColor,
width: barWidth,
borderRadius: BorderRadius.all(Radius.zero),
borderSide: BorderSide(
color: barColor.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,
drawHorizontalLine: true,
drawVerticalLine: false,
);
}
FlTitlesData getTitlesData() {
const AxisTitles none = const AxisTitles(
sideTitles: SideTitles(showTitles: false),
);
final AxisTitles verticalTitles = AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
getTitlesWidget: getVerticalTitlesWidget,
interval: 5,
),
);
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) {
String text = '';
if (value % 4 == 0 || value == 23) {
text = value.toInt().toString().padLeft(2, '0');
}
return SideTitleWidget(
axisSide: meta.axisSide,
space: 2,
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/counts_by_hour.dart';
import '../../../ui/widgets/main_screen/counts_by_hour_chart.dart';
class ChartCountsByHourCardContent extends StatelessWidget {
final int daysCount;
final CountsByHourData chartData;
final bool isLoading;
const ChartCountsByHourCardContent(
{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(
'counts_by_hour',
style: textTheme.titleLarge!.apply(fontWeightDelta: 2),
).tr(
namedArgs: {
'daysCount': this.daysCount.toString(),
},
),
const SizedBox(height: 8),
this.isLoading
? Text(placeholder)
: CountsByHourCardContentChart(
chartData: CountsByHourData.fromJson(jsonDecode(this.chartData.toString())),
),
],
);
}
}
import 'dart:convert';
import 'package:flutter/material.dart';
import '../../../models/statistics.dart';
import '../../../network/scrobbles_api.dart';
import '../../../ui/widgets/error.dart';
import '../../../ui/widgets/main_screen/statistics_content.dart';
class StatisticsCard extends StatelessWidget {
const StatisticsCard({super.key});
@override
Widget build(BuildContext context) {
late Future<StatisticsData> futureAlbum = ScrobblesApi.fetchStatistics();
return FutureBuilder<StatisticsData>(
future: futureAlbum,
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: StatisticsContent(
statistics: snapshot.hasData
? StatisticsData.fromJson(jsonDecode(snapshot.data.toString()))
: StatisticsData.createEmpty(),
isLoading: !snapshot.hasData,
),
),
);
},
);
}
}
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 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import '../../../models/timeline.dart';
import 'timeline_chart_counts.dart';
import 'timeline_chart_eclecticism.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())),
),
const SizedBox(height: 8),
ChartTimelineEclecticism(
chartData: TimelineData.fromJson(jsonDecode(this.chartData.toString())),
),
],
),
],
);
}
}
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:unicons/unicons.dart';
import 'package:scrobbles/cubit/settings_cubit.dart';
class SettingsForm extends StatefulWidget {
const SettingsForm({super.key});
@override
State<SettingsForm> createState() => _SettingsFormState();
}
class _SettingsFormState extends State<SettingsForm> {
final usernameController = TextEditingController();
final securityTokenController = TextEditingController();
@override
void dispose() {
usernameController.dispose();
securityTokenController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
SettingsCubit settings = BlocProvider.of<SettingsCubit>(context);
usernameController.text = settings.getUsername();
securityTokenController.text = settings.getSecurityToken();
void saveSettings() {
BlocProvider.of<SettingsCubit>(context).setValues(
username: usernameController.text,
securityToken: securityTokenController.text,
);
}
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Text('settings_label_username').tr(),
TextFormField(
controller: usernameController,
decoration: InputDecoration(
border: UnderlineInputBorder(),
),
),
SizedBox(height: 16),
Text('settings_label_security_token').tr(),
TextFormField(
controller: securityTokenController,
decoration: InputDecoration(
border: UnderlineInputBorder(),
),
),
SizedBox(height: 20),
ElevatedButton(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(UniconsLine.save),
SizedBox(width: 8),
Text('settings_button_save').tr(),
],
),
onPressed: () => saveSettings(),
),
],
);
}
}
......@@ -17,6 +17,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.11.0"
bloc:
dependency: transitive
description:
name: bloc
sha256: "3820f15f502372d979121de1f6b97bfcf1630ebff8fe1d52fb2b0bfa49be5b49"
url: "https://pub.dev"
source: hosted
version: "8.1.2"
characters:
dependency: transitive
description:
......@@ -37,10 +45,18 @@ packages:
dependency: transitive
description:
name: collection
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.dev"
source: hosted
version: "1.17.2"
version: "1.18.0"
crypto:
dependency: transitive
description:
name: crypto
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
url: "https://pub.dev"
source: hosted
version: "3.0.3"
easy_localization:
dependency: "direct main"
description:
......@@ -58,7 +74,7 @@ packages:
source: hosted
version: "0.0.2"
equatable:
dependency: transitive
dependency: "direct main"
description:
name: equatable
sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2
......@@ -85,25 +101,49 @@ packages:
dependency: "direct main"
description:
name: fl_chart
sha256: "6b9eb2b3017241d05c482c01f668dd05cc909ec9a0114fdd49acd958ff2432fa"
sha256: "5a74434cc83bf64346efb562f1a06eefaf1bcb530dc3d96a104f631a1eff8d79"
url: "https://pub.dev"
source: hosted
version: "0.64.0"
version: "0.65.0"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_bloc:
dependency: "direct main"
description:
name: flutter_bloc
sha256: e74efb89ee6945bcbce74a5b3a5a3376b088e5f21f55c263fc38cbdc6237faae
url: "https://pub.dev"
source: hosted
version: "8.1.3"
flutter_localizations:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
flutter_swipe:
dependency: "direct main"
description:
name: flutter_swipe
sha256: dc6541bac3a0545ce15a3fa15913f6250532062960bf6b0ad4562d02f14a8545
url: "https://pub.dev"
source: hosted
version: "1.0.1"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
hive:
dependency: transitive
description:
name: hive
sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941"
url: "https://pub.dev"
source: hosted
version: "2.2.3"
http:
dependency: "direct main"
description:
......@@ -120,6 +160,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.2"
hydrated_bloc:
dependency: "direct main"
description:
name: hydrated_bloc
sha256: "24994e61f64904d911683cce1a31dc4ef611619da5253f1de2b7b8fc6f79a118"
url: "https://pub.dev"
source: hosted
version: "9.1.2"
intl:
dependency: transitive
description:
......@@ -128,6 +176,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.18.1"
ionicons:
dependency: "direct main"
description:
name: ionicons
sha256: "5496bc65a16115ecf05b15b78f494ee4a8869504357668f0a11d689e970523cf"
url: "https://pub.dev"
source: hosted
version: "0.2.2"
material_color_utilities:
dependency: transitive
description:
......@@ -140,10 +196,18 @@ packages:
dependency: transitive
description:
name: meta
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
url: "https://pub.dev"
source: hosted
version: "1.10.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
version: "1.0.0"
path:
dependency: transitive
description:
......@@ -152,6 +216,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.8.3"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa
url: "https://pub.dev"
source: hosted
version: "2.1.1"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
path_provider_linux:
dependency: transitive
description:
......@@ -188,10 +276,18 @@ packages:
dependency: transitive
description:
name: plugin_platform_interface
sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d
sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8
url: "https://pub.dev"
source: hosted
version: "2.1.7"
provider:
dependency: transitive
description:
name: provider
sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096"
url: "https://pub.dev"
source: hosted
version: "2.1.6"
version: "6.1.1"
shared_preferences:
dependency: transitive
description:
......@@ -236,10 +332,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_web
sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf
sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
version: "2.2.2"
shared_preferences_windows:
dependency: transitive
description:
......@@ -269,6 +365,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.0"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
term_glyph:
dependency: transitive
description:
......@@ -285,6 +389,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.2"
unicons:
dependency: "direct main"
description:
name: unicons
sha256: dbfcf93ff4d4ea19b324113857e358e4882115ab85db04417a4ba1c72b17a670
url: "https://pub.dev"
source: hosted
version: "2.1.1"
vector_math:
dependency: transitive
description:
......@@ -297,18 +409,18 @@ packages:
dependency: transitive
description:
name: web
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
url: "https://pub.dev"
source: hosted
version: "0.1.4-beta"
version: "0.3.0"
win32:
dependency: transitive
description:
name: win32
sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3"
sha256: "7c99c0e1e2fa190b48d25c81ca5e42036d5cac81430ef249027d97b0935c553f"
url: "https://pub.dev"
source: hosted
version: "5.0.9"
version: "5.1.0"
xdg_directories:
dependency: transitive
description:
......@@ -318,5 +430,5 @@ packages:
source: hosted
version: "1.0.3"
sdks:
dart: ">=3.1.0-185.0.dev <4.0.0"
flutter: ">=3.7.0"
dart: ">=3.2.0 <4.0.0"
flutter: ">=3.16.0"