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 789 additions and 500 deletions
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;
const ChartDiscoveriesTracks({super.key, required this.chartData});
@override
Widget build(BuildContext context) {
if (this.chartData.data.keys.length == 0) {
return SizedBox(
height: this.chartHeight,
);
}
return Container(
height: this.chartHeight,
child: 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:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
import '../../../config/app_colors.dart'; import 'package:scrobbles/config/app_colors.dart';
import '../../../models/timeline.dart'; import 'package:scrobbles/models/timeline.dart';
import '../../../ui/widgets/charts/custom_bar_chart.dart'; import 'package:scrobbles/ui/widgets/abstracts/custom_bar_chart.dart';
class ChartTimelineCounts extends CustomBarChart { class ChartTimelineCounts extends CustomBarChart {
final TimelineData chartData; final TimelineData chartData;
ChartTimelineCounts({super.key, required this.chartData}); const ChartTimelineCounts({super.key, required this.chartData});
final double chartHeight = 120.0;
final double verticalTicksInterval = 50; final double verticalTicksInterval = 50;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (this.chartData.data.keys.length == 0) {
return SizedBox(
height: this.chartHeight,
);
}
return Container( return Container(
height: this.chartHeight, height: this.chartHeight,
child: LayoutBuilder(builder: (context, constraints) { child: LayoutBuilder(
return getBarChart( builder: (context, constraints) {
barWidth: this.getBarWidth(constraints.maxWidth, this.chartData.data.keys.length), final double maxWidth = constraints.maxWidth;
backgroundColor: Theme.of(context).colorScheme.onSurface, final int barsCount = this.chartData.data.keys.length;
);
}), return getBarChart(
barWidth: this.getBarWidth(maxWidth, barsCount),
backgroundColor: Theme.of(context).colorScheme.onSurface,
);
},
),
); );
} }
......
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
import '../../../config/app_colors.dart'; import 'package:scrobbles/config/app_colors.dart';
import '../../../models/timeline.dart'; import 'package:scrobbles/models/timeline.dart';
import '../../../ui/widgets/charts/custom_line_chart.dart'; import 'package:scrobbles/ui/widgets/abstracts/custom_line_chart.dart';
import '../../../utils/color_extensions.dart'; import 'package:scrobbles/utils/color_extensions.dart';
class ChartTimelineEclecticism extends CustomLineChart { class ChartTimelineEclecticism extends CustomLineChart {
final TimelineData chartData; final TimelineData chartData;
ChartTimelineEclecticism({super.key, required this.chartData}); const ChartTimelineEclecticism({super.key, required this.chartData});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final horizontalScale = getHorizontalScale(); final horizontalScale = getHorizontalScale();
if (this.chartData.data.keys.length == 0) {
return SizedBox(
height: this.chartHeight,
);
}
return Container( return Container(
height: this.chartHeight, height: this.chartHeight,
child: LineChart( child: LineChart(
......
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;
const ChartTopArtists({super.key, required this.chartData});
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 2.1,
child: 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 '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/topartists.dart';
import 'package:scrobbles/ui/widgets/abstracts/custom_line_chart.dart';
import 'package:scrobbles/utils/color_extensions.dart';
class ChartTopArtistsStream extends CustomLineChart {
final TopArtistsData chartData;
const ChartTopArtistsStream({super.key, required this.chartData});
final double verticalTicksInterval = 10;
@override
Widget build(BuildContext context) {
final horizontalScale = getHorizontalScale();
if (this.chartData.topArtistsStream.keys.isEmpty) {
return SizedBox(
height: this.chartHeight,
);
}
return Container(
height: this.chartHeight,
child: LineChart(
LineChartData(
lineBarsData: getDataStreamLine(),
betweenBarsData: getBetweenBarsData(),
borderData: getBorderData(),
gridData: getGridData(),
titlesData: getTitlesData(),
lineTouchData: const LineTouchData(enabled: false),
minX: horizontalScale['min'],
maxX: horizontalScale['max'],
maxY: getNextRoundNumber(getMaxVerticalValue(), this.verticalTicksInterval),
minY: 0,
),
duration: const Duration(milliseconds: 250),
),
);
}
double getNextRoundNumber(double number, double scale) {
return scale * ((number ~/ scale).toInt() + 1);
}
FlGridData getGridData() {
return const FlGridData(
show: true,
drawHorizontalLine: true,
drawVerticalLine: false,
);
}
double getMaxVerticalValue() {
double maxValue = 0;
this.chartData.topArtistsStream.keys.forEach((dateAsString) {
double totalValue = 0.0;
List<TopArtistsStreamDataValue> artists =
this.chartData.topArtistsStream[dateAsString] ?? [];
artists.forEach((artist) {
final double value = artist.value;
totalValue = totalValue + value;
});
if (totalValue > maxValue) {
maxValue = totalValue;
}
});
return maxValue;
}
Map<String, double> getHorizontalScale() {
double minDateAsDouble = double.maxFinite;
double maxDateAsDouble = -double.maxFinite;
this.chartData.topArtistsStream.keys.forEach((dateAsString) {
final double date = DateTime.parse(dateAsString).millisecondsSinceEpoch.toDouble();
if (date < minDateAsDouble) {
minDateAsDouble = date;
}
if (date > maxDateAsDouble) {
maxDateAsDouble = date;
}
});
return {
'min': minDateAsDouble,
'max': maxDateAsDouble,
};
}
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<LineChartBarData> getDataStreamLine() {
int artistsCount =
this.chartData.topArtistsStream[this.chartData.topArtistsStream.keys.first]?.length ??
0;
List<LineChartBarData> lines = [];
LineChartBarData getZeroHorizontalLine() {
final baseColor = getColorIndex(0);
final borderColor = baseColor.darken(20);
List<FlSpot> spots = [];
this.chartData.topArtistsStream.keys.forEach((dateAsString) {
final double date = DateTime.parse(dateAsString).millisecondsSinceEpoch.toDouble();
spots.add(FlSpot(date, 0));
});
return LineChartBarData(
color: borderColor,
dotData: const FlDotData(show: false),
spots: spots,
);
}
// First horizontal "zero" line
lines.add(getZeroHorizontalLine());
LineChartBarData getLinesFromIndex(int index) {
final baseColor = getColorIndex(index);
final borderColor = baseColor.darken(20);
List<FlSpot> spots = [];
this.chartData.topArtistsStream.keys.forEach((dateAsString) {
final double date = DateTime.parse(dateAsString).millisecondsSinceEpoch.toDouble();
List<TopArtistsStreamDataValue> artists =
this.chartData.topArtistsStream[dateAsString] ?? [];
double value = 0;
for (int i = 0; i <= index; i++) {
value = value + artists[i].value;
}
spots.add(FlSpot(date, value));
});
return LineChartBarData(
isCurved: true,
curveSmoothness: 0.25,
color: borderColor,
dotData: const FlDotData(show: false),
spots: spots,
);
}
Iterable<int>.generate(artistsCount)
.toList()
.map((index) => lines.add(getLinesFromIndex(index)))
.toList();
return lines;
}
List<BetweenBarsData> getBetweenBarsData() {
int artistsCount =
this.chartData.topArtistsStream[this.chartData.topArtistsStream.keys.first]?.length ??
0;
return Iterable<int>.generate(artistsCount)
.toList()
.map((index) => BetweenBarsData(
fromIndex: index,
toIndex: index + 1,
color: getColorIndex(index),
))
.toList();
}
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: Padding(
padding: EdgeInsets.only(right: 10),
child: RotationTransition(
turns: new AlwaysStoppedAnimation(-30 / 360),
child: Text(
text,
style: TextStyle(
color: AppColors.mainTextColor1,
fontSize: this.titleFontSize,
),
),
),
),
);
}
}
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_global.dart'; import 'package:scrobbles/models/statistics_global.dart';
class StatisticsGlobalContent extends StatelessWidget { class ContentStatisticsGlobal extends StatelessWidget {
final StatisticsGlobalData statistics; final StatisticsGlobalData statistics;
final bool isLoading;
const StatisticsGlobalContent( const ContentStatisticsGlobal({super.key, required this.statistics});
{super.key, required this.statistics, required this.isLoading});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).primaryTextTheme; final TextTheme textTheme = Theme.of(context).primaryTextTheme;
const String placeholder = '⏳';
final String placeholder = '⏳';
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Text(
'global_statistics',
style: textTheme.titleLarge!.apply(fontWeightDelta: 2),
).tr(),
Text( Text(
'statistics_total_scrobbles_count', 'statistics_total_scrobbles_count',
style: textTheme.bodyMedium, style: textTheme.bodyMedium,
).tr( ).tr(
namedArgs: { namedArgs: {
'count': this.isLoading ? placeholder : this.statistics.totalCount.toString(), 'count': this.statistics.totalCount != null
? this.statistics.totalCount.toString()
: placeholder,
}, },
), ),
Text( Text(
...@@ -37,9 +32,9 @@ class StatisticsGlobalContent extends StatelessWidget { ...@@ -37,9 +32,9 @@ class StatisticsGlobalContent extends StatelessWidget {
style: textTheme.bodyMedium, style: textTheme.bodyMedium,
).tr( ).tr(
namedArgs: { namedArgs: {
'datetime': this.isLoading 'datetime': this.statistics.lastScrobble != null
? placeholder ? DateFormat().format(this.statistics.lastScrobble ?? DateTime.now())
: DateFormat().format(this.statistics.lastScrobble), : placeholder,
}, },
), ),
], ],
......
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_recent.dart'; import 'package:scrobbles/models/statistics_recent.dart';
class StatisticsRecentContent extends StatelessWidget { class ContentStatisticsRecent extends StatelessWidget {
final StatisticsRecentData statistics; final StatisticsRecentData statistics;
final bool isLoading;
const StatisticsRecentContent( const ContentStatisticsRecent({super.key, required this.statistics});
{super.key, required this.statistics, required this.isLoading});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).primaryTextTheme; final TextTheme textTheme = Theme.of(context).primaryTextTheme;
final String placeholder = '⏳';
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Text( Text(
'recent_statistics', (this.statistics.recentCount != null &&
style: textTheme.titleLarge!.apply(fontWeightDelta: 2), this.statistics.firstPlayedArtistsCount != null &&
).tr(), this.statistics.firstPlayedTracksCount != null)
Text( ? 'statistics_recent_scrobbles_count_and_discoveries'
'statistics_selected_period', : '',
style: textTheme.bodyMedium!.apply(fontWeightDelta: 2),
).tr(
namedArgs: {
'daysCount':
this.isLoading ? placeholder : this.statistics.selectedPeriod.toString(),
},
),
Text(
'statistics_recent_scrobbles_count_and_discoveries',
style: textTheme.bodyMedium, style: textTheme.bodyMedium,
).tr( ).tr(
namedArgs: { namedArgs: {
'count': this.isLoading ? placeholder : this.statistics.recentCount.toString(), 'count': this.statistics.recentCount.toString(),
'artistsCount': this.isLoading 'artistsCount': this.statistics.firstPlayedArtistsCount.toString(),
? placeholder 'tracksCount': this.statistics.firstPlayedTracksCount.toString(),
: this.statistics.firstPlayedArtistsCount.toString(),
'tracksCount': this.isLoading
? placeholder
: this.statistics.firstPlayedTracksCount.toString(),
}, },
), ),
], ],
......
...@@ -8,6 +8,8 @@ class ShowErrorWidget extends StatelessWidget { ...@@ -8,6 +8,8 @@ class ShowErrorWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
print(message);
return Text( return Text(
'⚠️ ' + tr(message), '⚠️ ' + tr(message),
textAlign: TextAlign.start, textAlign: TextAlign.start,
......
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class Header extends StatelessWidget { class AppHeader extends StatelessWidget {
const Header({super.key, required this.text}); const AppHeader({super.key, required this.text});
final String text; final String text;
......
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.surface,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(8),
),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ChartCountsByDayCardContent(
daysCount: daysCount,
chartData: snapshot.hasData
? CountsByDayData.fromJson(jsonDecode(snapshot.data.toString()))
: CountsByDayData.createEmpty(),
isLoading: !snapshot.hasData,
),
),
);
},
);
}
}
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';
class ChartCountsByDayCardContent extends StatelessWidget {
final int daysCount;
final CountsByDayData chartData;
final bool isLoading;
const ChartCountsByDayCardContent(
{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_day',
style: textTheme.titleLarge!.apply(fontWeightDelta: 2),
).tr(
namedArgs: {
'daysCount': this.daysCount.toString(),
},
),
const SizedBox(height: 8),
this.isLoading
? Text(placeholder)
: CountsByDayCardContentChart(
chartData: CountsByDayData.fromJson(jsonDecode(this.chartData.toString())),
),
],
);
}
}
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.surface,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(8),
),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ChartCountsByHourCardContent(
daysCount: daysCount,
chartData: snapshot.hasData
? CountsByHourData.fromJson(jsonDecode(snapshot.data.toString()))
: CountsByHourData.createEmpty(),
isLoading: !snapshot.hasData,
),
),
);
},
);
}
}
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/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(8),
),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ChartDiscoveriesCardContent(
daysCount: daysCount,
chartData: snapshot.hasData
? DiscoveriesData.fromJson(jsonDecode(snapshot.data.toString()))
: DiscoveriesData.createEmpty(),
isLoading: !snapshot.hasData,
),
),
);
},
);
}
}
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())),
),
],
);
}
}
import 'dart:convert';
import 'package:flutter/material.dart';
import '../../../models/statistics_global.dart';
import '../../../network/scrobbles_api.dart';
import '../../../ui/widgets/error.dart';
import 'statistics_global_content.dart';
class StatisticsGlobalCard extends StatelessWidget {
const StatisticsGlobalCard({super.key});
@override
Widget build(BuildContext context) {
late Future<StatisticsGlobalData> future = ScrobblesApi.fetchGlobalStatistics();
return FutureBuilder<StatisticsGlobalData>(
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: StatisticsGlobalContent(
statistics: snapshot.hasData
? StatisticsGlobalData.fromJson(jsonDecode(snapshot.data.toString()))
: StatisticsGlobalData.createEmpty(),
isLoading: !snapshot.hasData,
),
),
);
},
);
}
}
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 '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.surface,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(8),
),
),
child: Padding(
padding: const EdgeInsets.all(8.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)
: Stack(
children: [
ChartTimelineCounts(
chartData: TimelineData.fromJson(jsonDecode(this.chartData.toString())),
),
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/config/default_settings.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();
int countsByDayDaysCount = DefaultSettings.defaultCountsByDayDaysCount;
int countsByHourDaysCount = DefaultSettings.defaultCountsByHourDaysCount;
int discoveriesDaysCount = DefaultSettings.defaultDiscoveriesDaysCount;
int statisticsRecentDaysCount = DefaultSettings.defaultStatisticsRecentDaysCount;
int timelineDaysCount = DefaultSettings.defaultTimelineDaysCount;
int topArtistsDaysCount = DefaultSettings.defaultTopArtistsDaysCount;
List<bool> _selectedCountsByDayDaysCount = [];
List<bool> _selectedCountsByHourDaysCount = [];
List<bool> _selectedDiscoveriesDaysCount = [];
List<bool> _selectedStatisticsRecentDaysCount = [];
List<bool> _selectedTimelineDaysCount = [];
List<bool> _selectedTopArtistsDaysCount = [];
@override
void didChangeDependencies() {
SettingsCubit settings = BlocProvider.of<SettingsCubit>(context);
usernameController.text = settings.getUsername();
securityTokenController.text = settings.getSecurityToken();
countsByDayDaysCount = settings.getCountsByDayDaysCount();
countsByHourDaysCount = settings.getCountsByHourDaysCount();
discoveriesDaysCount = settings.getDiscoveriesDaysCount();
statisticsRecentDaysCount = settings.getStatisticsRecentDaysCount();
timelineDaysCount = settings.getTimelineDaysCount();
topArtistsDaysCount = settings.getTopArtistsDaysCount();
_selectedCountsByDayDaysCount =
DefaultSettings.allowedValues.map((e) => (e == countsByDayDaysCount)).toList();
_selectedCountsByHourDaysCount =
DefaultSettings.allowedValues.map((e) => (e == countsByHourDaysCount)).toList();
_selectedDiscoveriesDaysCount =
DefaultSettings.allowedValues.map((e) => (e == discoveriesDaysCount)).toList();
_selectedStatisticsRecentDaysCount =
DefaultSettings.allowedValues.map((e) => (e == statisticsRecentDaysCount)).toList();
_selectedTimelineDaysCount =
DefaultSettings.allowedValues.map((e) => (e == timelineDaysCount)).toList();
_selectedTopArtistsDaysCount =
DefaultSettings.allowedValues.map((e) => (e == topArtistsDaysCount)).toList();
super.didChangeDependencies();
}
@override
void dispose() {
usernameController.dispose();
securityTokenController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
void saveSettings() {
BlocProvider.of<SettingsCubit>(context).setValues(
username: usernameController.text,
securityToken: securityTokenController.text,
countsByDayDaysCount: countsByDayDaysCount,
countsByHourDaysCount: countsByHourDaysCount,
discoveriesDaysCount: discoveriesDaysCount,
statisticsRecentDaysCount: statisticsRecentDaysCount,
timelineDaysCount: timelineDaysCount,
topArtistsDaysCount: topArtistsDaysCount,
);
}
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
// Username
// Text('settings_label_username').tr(),
// TextFormField(
// controller: usernameController,
// decoration: InputDecoration(
// border: UnderlineInputBorder(),
// ),
// ),
// SizedBox(height: 16),
// Security token
// Text('settings_label_security_token').tr(),
// TextFormField(
// controller: securityTokenController,
// decoration: InputDecoration(
// border: UnderlineInputBorder(),
// ),
// ),
// SizedBox(height: 20),
// Counts by day
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('settings_label_counts_by_day_days_count').tr(),
ToggleButtons(
onPressed: (int index) {
setState(() {
countsByDayDaysCount = DefaultSettings.allowedValues[index];
for (int i = 0; i < _selectedCountsByDayDaysCount.length; i++) {
_selectedCountsByDayDaysCount[i] = i == index;
}
});
},
borderRadius: const BorderRadius.all(Radius.circular(8)),
constraints: const BoxConstraints(minHeight: 30.0, minWidth: 30.0),
isSelected: _selectedCountsByDayDaysCount,
children: DefaultSettings.allowedValues.map((e) => Text(e.toString())).toList(),
),
],
),
// Counts by hour
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('settings_label_counts_by_hour_days_count').tr(),
ToggleButtons(
onPressed: (int index) {
setState(() {
countsByHourDaysCount = DefaultSettings.allowedValues[index];
for (int i = 0; i < _selectedCountsByHourDaysCount.length; i++) {
_selectedCountsByHourDaysCount[i] = i == index;
}
});
},
borderRadius: const BorderRadius.all(Radius.circular(8)),
constraints: const BoxConstraints(minHeight: 30.0, minWidth: 30.0),
isSelected: _selectedCountsByHourDaysCount,
children: DefaultSettings.allowedValues.map((e) => Text(e.toString())).toList(),
),
],
),
// Discoveries
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('settings_label_discoveries_days_count').tr(),
ToggleButtons(
onPressed: (int index) {
setState(() {
discoveriesDaysCount = DefaultSettings.allowedValues[index];
for (int i = 0; i < _selectedDiscoveriesDaysCount.length; i++) {
_selectedDiscoveriesDaysCount[i] = i == index;
}
});
},
borderRadius: const BorderRadius.all(Radius.circular(8)),
constraints: const BoxConstraints(minHeight: 30.0, minWidth: 30.0),
isSelected: _selectedDiscoveriesDaysCount,
children: DefaultSettings.allowedValues.map((e) => Text(e.toString())).toList(),
),
],
),
// Statistics (recent)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('settings_label_statistics_recent_days_count').tr(),
ToggleButtons(
onPressed: (int index) {
setState(() {
statisticsRecentDaysCount = DefaultSettings.allowedValues[index];
for (int i = 0; i < _selectedStatisticsRecentDaysCount.length; i++) {
_selectedStatisticsRecentDaysCount[i] = i == index;
}
});
},
borderRadius: const BorderRadius.all(Radius.circular(8)),
constraints: const BoxConstraints(minHeight: 30.0, minWidth: 30.0),
isSelected: _selectedStatisticsRecentDaysCount,
children: DefaultSettings.allowedValues.map((e) => Text(e.toString())).toList(),
),
],
),
// Timeline
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('settings_label_timeline_days_count').tr(),
ToggleButtons(
onPressed: (int index) {
setState(() {
timelineDaysCount = DefaultSettings.allowedValues[index];
for (int i = 0; i < _selectedTimelineDaysCount.length; i++) {
_selectedTimelineDaysCount[i] = i == index;
}
});
},
borderRadius: const BorderRadius.all(Radius.circular(8)),
constraints: const BoxConstraints(minHeight: 30.0, minWidth: 30.0),
isSelected: _selectedTimelineDaysCount,
children: DefaultSettings.allowedValues.map((e) => Text(e.toString())).toList(),
),
],
),
// Top Artists
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('settings_label_top_artists_days_count').tr(),
ToggleButtons(
onPressed: (int index) {
setState(() {
topArtistsDaysCount = DefaultSettings.allowedValues[index];
for (int i = 0; i < _selectedTopArtistsDaysCount.length; i++) {
_selectedTopArtistsDaysCount[i] = i == index;
}
});
},
borderRadius: const BorderRadius.all(Radius.circular(8)),
constraints: const BoxConstraints(minHeight: 30.0, minWidth: 30.0),
isSelected: _selectedTopArtistsDaysCount,
children: DefaultSettings.allowedValues.map((e) => Text(e.toString())).toList(),
),
],
),
// Save
ElevatedButton(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(UniconsLine.save),
SizedBox(width: 8),
Text('settings_button_save').tr(),
],
),
onPressed: () => saveSettings(),
),
],
);
}
}