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 1263 additions and 16 deletions
import 'dart:convert';
class TopArtistsDataValue {
final String artistName;
final int count;
const TopArtistsDataValue({required this.artistName, required this.count});
factory TopArtistsDataValue.fromJson(Map<String, dynamic>? json) {
return TopArtistsDataValue(
artistName: json?['artistName'] as String,
count: json?['count'] as int,
);
}
}
class TopArtistsData {
final List<TopArtistsDataValue> topArtists;
const TopArtistsData({
required this.topArtists,
});
factory TopArtistsData.fromJson(Map<String, dynamic>? json) {
List<TopArtistsDataValue> topArtists = [];
json?['top-artists'].forEach((element) {
TopArtistsDataValue value = TopArtistsDataValue(
artistName: element['artistName'] as String,
count: element['count'] as int,
);
topArtists.add(value);
});
return TopArtistsData(
topArtists: topArtists,
);
}
Map<String, Object?>? toJson() {
List<Map<String, Object>> listArtists = [];
this.topArtists.forEach((TopArtistsDataValue? item) {
listArtists.add({
'artistName': item != null ? item.artistName : '',
'count': item != null ? item.count : 0,
});
});
return {
'top-artists': listArtists,
};
}
String toString() {
return jsonEncode(this.toJson());
}
}
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import '../models/counts_by_day.dart'; import 'package:scrobbles/models/counts_by_day.dart';
import '../models/counts_by_hour.dart'; import 'package:scrobbles/models/counts_by_hour.dart';
import '../models/statistics.dart'; import 'package:scrobbles/models/discoveries.dart';
import '../models/timeline.dart'; import 'package:scrobbles/models/statistics_global.dart';
import 'package:scrobbles/models/statistics_recent.dart';
import 'package:scrobbles/models/timeline.dart';
import 'package:scrobbles/models/topartists.dart';
class ScrobblesApi { class ScrobblesApi {
static String baseUrl = 'https://scrobble.harrault.fr'; static String baseUrl = 'https://scrobble.harrault.fr';
static Future<StatisticsData> fetchStatistics() async { static Future<StatisticsGlobalData> fetchGlobalStatistics() async {
final response = await http.get(Uri.parse(baseUrl + '/stats')); final String url = baseUrl + '/stats';
print('fetching ' + url);
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) { if (response.statusCode == 200) {
return StatisticsData.fromJson(jsonDecode(response.body) as Map<String, dynamic>); print('ok - fetched ' + url);
return StatisticsGlobalData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
throw Exception('Failed to get data from API.');
}
}
static Future<StatisticsRecentData> fetchRecentStatistics(int daysCount) async {
final String url = baseUrl + '/' + daysCount.toString() + '/stats';
print('fetching ' + url);
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
print('ok - fetched ' + url);
return StatisticsRecentData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else { } else {
throw Exception('Failed to get data from API.'); throw Exception('Failed to get data from API.');
} }
...@@ -21,9 +40,11 @@ class ScrobblesApi { ...@@ -21,9 +40,11 @@ class ScrobblesApi {
static Future<TimelineData> fetchTimeline(int daysCount) async { static Future<TimelineData> fetchTimeline(int daysCount) async {
final String url = baseUrl + '/data/' + daysCount.toString() + '/timeline'; final String url = baseUrl + '/data/' + daysCount.toString() + '/timeline';
print('fetching ' + url);
final response = await http.get(Uri.parse(url)); final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) { if (response.statusCode == 200) {
print('ok - fetched ' + url);
return TimelineData.fromJson(jsonDecode(response.body) as Map<String, dynamic>); return TimelineData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else { } else {
throw Exception('Failed to get data from API.'); throw Exception('Failed to get data from API.');
...@@ -32,9 +53,11 @@ class ScrobblesApi { ...@@ -32,9 +53,11 @@ class ScrobblesApi {
static Future<CountsByDayData> fetchCountsByDay(int daysCount) async { static Future<CountsByDayData> fetchCountsByDay(int daysCount) async {
final String url = baseUrl + '/data/' + daysCount.toString() + '/counts-by-day'; final String url = baseUrl + '/data/' + daysCount.toString() + '/counts-by-day';
print('fetching ' + url);
final response = await http.get(Uri.parse(url)); final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) { if (response.statusCode == 200) {
print('ok - fetched ' + url);
return CountsByDayData.fromJson(jsonDecode(response.body) as Map<String, dynamic>); return CountsByDayData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else { } else {
throw Exception('Failed to get data from API.'); throw Exception('Failed to get data from API.');
...@@ -43,12 +66,40 @@ class ScrobblesApi { ...@@ -43,12 +66,40 @@ class ScrobblesApi {
static Future<CountsByHourData> fetchCountsByHour(int daysCount) async { static Future<CountsByHourData> fetchCountsByHour(int daysCount) async {
final String url = baseUrl + '/data/' + daysCount.toString() + '/counts-by-hour'; final String url = baseUrl + '/data/' + daysCount.toString() + '/counts-by-hour';
print('fetching ' + url);
final response = await http.get(Uri.parse(url)); final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) { if (response.statusCode == 200) {
print('ok - fetched ' + url);
return CountsByHourData.fromJson(jsonDecode(response.body) as Map<String, dynamic>); return CountsByHourData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else { } else {
throw Exception('Failed to get data from API.'); throw Exception('Failed to get data from API.');
} }
} }
static Future<DiscoveriesData> fetchDiscoveries(int daysCount) async {
final String url = baseUrl + '/data/' + daysCount.toString() + '/news';
print('fetching ' + url);
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
print('ok - fetched ' + url);
return DiscoveriesData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
throw Exception('Failed to get data from API.');
}
}
static Future<TopArtistsData> fetchTopArtists(int daysCount) async {
final String url = baseUrl + '/data/' + daysCount.toString() + '/top-artists';
print('fetching ' + url);
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
print('ok - fetched ' + url);
return TopArtistsData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
throw Exception('Failed to get data from API.');
}
}
} }
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../widgets/header.dart'; import 'package:scrobbles/ui/widgets/cards/discoveries.dart';
import '../widgets/main_screen/counts_by_day_card.dart';
import '../widgets/main_screen/counts_by_hour_card.dart';
import '../widgets/main_screen/statistics_card.dart';
import '../widgets/main_screen/timeline_card.dart';
class MainScreen extends StatelessWidget { class ScreenDiscoveries extends StatelessWidget {
const MainScreen({super.key}); const ScreenDiscoveries({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return Material(
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.background,
child: ListView( child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 4),
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
children: <Widget>[ children: <Widget>[
const Header(text: 'app_name'),
const StatisticsCard(),
const SizedBox(height: 8), const SizedBox(height: 8),
const ChartTimelineCard(), const CardDiscoveries(),
const SizedBox(height: 8),
const ChartCountsByDayCard(),
const SizedBox(height: 8),
const ChartCountsByHourCard(),
const SizedBox(height: 36), const SizedBox(height: 36),
], ],
), ),
......
import 'package:flutter/material.dart';
import 'package:scrobbles/ui/widgets/cards/statistics_global.dart';
import 'package:scrobbles/ui/widgets/cards/statistics_recent.dart';
import 'package:scrobbles/ui/widgets/cards/timeline.dart';
import 'package:scrobbles/ui/widgets/cards/top_artists.dart';
class ScreenHome extends StatelessWidget {
const ScreenHome({super.key});
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.background,
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 4),
physics: const BouncingScrollPhysics(),
children: <Widget>[
const SizedBox(height: 8),
const CardStatisticsGlobal(),
const SizedBox(height: 6),
const CardStatisticsRecent(),
const SizedBox(height: 6),
const CardTimeline(),
const SizedBox(height: 6),
const CardTopArtists(),
const SizedBox(height: 36),
],
),
);
}
}
import 'package:flutter/material.dart';
import 'package:scrobbles/ui/widgets/header_app.dart';
import 'package:scrobbles/ui/widgets/settings_form.dart';
class ScreenSettings extends StatelessWidget {
const ScreenSettings({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
SizedBox(height: 8),
AppHeader(text: 'settings_title'),
SizedBox(height: 8),
SettingsForm(),
],
);
}
}
import 'package:flutter/material.dart';
import 'main_screen.dart';
class SkeletonScreen extends StatelessWidget {
const SkeletonScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
body: MainScreen(),
backgroundColor: Theme.of(context).colorScheme.background,
);
}
}
import 'package:flutter/material.dart';
import 'package:scrobbles/ui/widgets/cards/counts_by_day.dart';
import 'package:scrobbles/ui/widgets/cards/counts_by_hour.dart';
class ScreenStatistics extends StatelessWidget {
const ScreenStatistics({super.key});
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.background,
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 4),
physics: const BouncingScrollPhysics(),
children: <Widget>[
const SizedBox(height: 8),
const CardCountsByDay(),
const SizedBox(height: 6),
const CardCountsByHour(),
const SizedBox(height: 36),
],
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_swipe/flutter_swipe.dart';
import 'package:scrobbles/cubit/bottom_nav_cubit.dart';
import 'package:scrobbles/ui/screens/discoveries.dart';
import 'package:scrobbles/ui/screens/home.dart';
import 'package:scrobbles/ui/screens/settings.dart';
import 'package:scrobbles/ui/screens/statistics.dart';
import 'package:scrobbles/ui/widgets/app_bar.dart';
import 'package:scrobbles/ui/widgets/bottom_nav_bar.dart';
class SkeletonScreen extends StatefulWidget {
const SkeletonScreen({super.key});
@override
State<SkeletonScreen> createState() => _SkeletonScreenState();
}
class _SkeletonScreenState extends State<SkeletonScreen> {
@override
Widget build(BuildContext context) {
const List<Widget> pageNavigation = <Widget>[
const ScreenHome(),
const ScreenDiscoveries(),
const ScreenStatistics(),
const ScreenSettings(),
];
return Scaffold(
appBar: StandardAppBar(notifyParent: refresh),
extendBodyBehindAppBar: false,
body: Swiper(
itemCount: BlocProvider.of<BottomNavCubit>(context).pagesCount,
itemBuilder: (BuildContext context, int index) {
return pageNavigation.elementAt(index);
},
pagination: SwiperPagination(
builder: SwiperCustomPagination(
builder: (BuildContext context, SwiperPluginConfig config) {
return BottomNavBar(swipeController: config.controller);
},
),
),
onIndexChanged: (newPageIndex) {
BlocProvider.of<BottomNavCubit>(context).updateIndex(newPageIndex);
},
outer: true,
loop: false,
),
backgroundColor: Theme.of(context).colorScheme.background,
);
}
refresh() {
setState(() {});
}
}
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import '../../../config/app_colors.dart'; import 'package:scrobbles/config/app_colors.dart';
import '../../../models/timeline.dart'; import 'package:scrobbles/utils/color_extensions.dart';
import '../../../utils/color_extensions.dart';
class ChartTimelineCounts extends StatelessWidget { class CustomBarChart extends StatelessWidget {
final TimelineData chartData; CustomBarChart({super.key});
const ChartTimelineCounts({super.key, required this.chartData}); final Widget placeholder = Text('⏳');
final double chartHeight = 150.0;
final double verticalTicksInterval = 10;
final String verticalAxisTitleSuffix = '';
final double titleFontSize = 10;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
height: 150.0, child: SizedBox(
child: LayoutBuilder(builder: (context, constraints) { height: this.chartHeight,
final double maxWidth = constraints.maxWidth; ),
final int barsCount = this.chartData.data.keys.length; );
final double barWidth = 0.7 * (maxWidth / barsCount); }
return BarChart( BarChart getBarChart({barWidth, backgroundColor}) {
BarChartData( return BarChart(
barGroups: getDataCounts(barWidth), BarChartData(
backgroundColor: Theme.of(context).colorScheme.onBackground, barGroups: getDataCounts(barWidth),
borderData: getBorderData(), backgroundColor: backgroundColor,
gridData: getGridData(), borderData: getBorderData(),
titlesData: getTitlesData(), gridData: getGridData(),
barTouchData: getBarTouchData(), titlesData: getTitlesData(),
maxY: getNextRoundNumber(getMaxCountsValue(), 50), barTouchData: BarTouchData(enabled: false),
), maxY: getNextRoundNumber(getMaxCountsValue(), this.verticalTicksInterval),
); ),
}),
); );
} }
double getBarWidth(double containerWidth, int barsCount) {
return 0.65 * (containerWidth / barsCount);
}
List<BarChartGroupData> getDataCounts(double barWidth) {
return [];
}
double getMaxCountsValue() { double getMaxCountsValue() {
double maxValue = 0; return 0.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) { double getNextRoundNumber(double number, double scale) {
return scale * ((number ~/ scale).toInt() + 1); return scale * ((number ~/ scale).toInt() + 1);
} }
List<BarChartGroupData> getDataCounts(double barWidth) { BarChartGroupData getBarItem(
List<BarChartGroupData> data = []; {required int x,
required List<double> values,
this.chartData.data.keys.forEach((key) { required List<Color> barColors,
TimelineDataValue? value = this.chartData.data[key]; required double barWidth}) {
if (value != null) { List<BarChartRodData> barRods = [];
final int date = DateTime.parse(key).millisecondsSinceEpoch;
final double counts = value.counts.toDouble(); for (int i = 0; i < values.length; i++) {
double value = values[i];
data.add(BarChartGroupData( Color barColor = barColors[i];
x: date,
barRods: [ final gradient = this.getGradient(barColor, value, this.getMaxCountsValue());
BarChartRodData( final borderColor = barColor.darken(20);
toY: counts,
color: AppColors.contentColorOrange, barRods.add(
width: barWidth, BarChartRodData(
borderRadius: BorderRadius.all(Radius.zero), toY: value,
borderSide: BorderSide( color: barColor,
color: AppColors.contentColorOrange.darken(20), gradient: gradient,
), width: barWidth,
), borderRadius: BorderRadius.all(Radius.zero),
], borderSide: BorderSide(
)); color: borderColor,
} ),
}); ),
);
}
return BarChartGroupData(
x: x,
barRods: barRods,
);
}
return data; LinearGradient getGradient(Color baseColor, double value, double maxValue) {
double alignmentTopValue = value != 0.0 ? -2 * maxValue / value + 1 : 0;
return LinearGradient(
begin: Alignment(-1, alignmentTopValue),
end: Alignment(1, 1),
colors: <Color>[
baseColor.lighten(30),
baseColor,
baseColor.darken(30),
],
tileMode: TileMode.mirror,
);
} }
FlBorderData getBorderData() { FlBorderData getBorderData() {
...@@ -97,6 +115,8 @@ class ChartTimelineCounts extends StatelessWidget { ...@@ -97,6 +115,8 @@ class ChartTimelineCounts extends StatelessWidget {
FlGridData getGridData() { FlGridData getGridData() {
return const FlGridData( return const FlGridData(
show: true, show: true,
drawHorizontalLine: true,
drawVerticalLine: false,
); );
} }
...@@ -108,10 +128,21 @@ class ChartTimelineCounts extends StatelessWidget { ...@@ -108,10 +128,21 @@ class ChartTimelineCounts extends StatelessWidget {
final AxisTitles verticalTitles = AxisTitles( final AxisTitles verticalTitles = AxisTitles(
sideTitles: SideTitles( sideTitles: SideTitles(
showTitles: true, showTitles: true,
reservedSize: 30, reservedSize: 35,
getTitlesWidget: getVerticalTitlesWidget, getTitlesWidget: getVerticalTitlesWidget,
interval: this.verticalTicksInterval,
),
);
final AxisTitles verticalSpacer = AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 35,
getTitlesWidget: getVerticalTitlesSpacerWidget,
interval: this.verticalTicksInterval,
), ),
); );
final AxisTitles horizontalTitles = AxisTitles( final AxisTitles horizontalTitles = AxisTitles(
sideTitles: SideTitles( sideTitles: SideTitles(
showTitles: true, showTitles: true,
...@@ -123,26 +154,37 @@ class ChartTimelineCounts extends StatelessWidget { ...@@ -123,26 +154,37 @@ class ChartTimelineCounts extends StatelessWidget {
return FlTitlesData( return FlTitlesData(
show: true, show: true,
bottomTitles: horizontalTitles, bottomTitles: horizontalTitles,
leftTitles: none, leftTitles: verticalTitles,
topTitles: none, topTitles: none,
rightTitles: verticalTitles, rightTitles: verticalSpacer,
); );
} }
Widget getVerticalTitlesWidget(double value, TitleMeta meta) { Widget getVerticalTitlesWidget(double value, TitleMeta meta) {
String suffix =
this.verticalAxisTitleSuffix != '' ? ' ' + this.verticalAxisTitleSuffix : '';
return SideTitleWidget( return SideTitleWidget(
axisSide: meta.axisSide, axisSide: meta.axisSide,
space: 4, space: 4,
child: Text( child: Text(
value.toInt().toString(), value.toInt().toString() + suffix,
style: const TextStyle( style: TextStyle(
color: AppColors.mainTextColor1, color: AppColors.mainTextColor1,
fontSize: 12, fontSize: this.titleFontSize,
), ),
), ),
); );
} }
Widget getVerticalTitlesSpacerWidget(double value, TitleMeta meta) {
return SideTitleWidget(
axisSide: meta.axisSide,
space: 4,
child: Text(''),
);
}
Widget getHorizontalTitlesWidget(double value, TitleMeta meta) { Widget getHorizontalTitlesWidget(double value, TitleMeta meta) {
final DateFormat formatter = DateFormat('dd/MM'); final DateFormat formatter = DateFormat('dd/MM');
...@@ -152,42 +194,19 @@ class ChartTimelineCounts extends StatelessWidget { ...@@ -152,42 +194,19 @@ class ChartTimelineCounts extends StatelessWidget {
return SideTitleWidget( return SideTitleWidget(
axisSide: meta.axisSide, axisSide: meta.axisSide,
space: 4, space: 4,
child: RotationTransition( child: Padding(
turns: new AlwaysStoppedAnimation(-30 / 360), padding: EdgeInsets.only(right: 10),
child: Text( child: RotationTransition(
text, turns: new AlwaysStoppedAnimation(-30 / 360),
style: const TextStyle( child: Text(
color: AppColors.mainTextColor1, text,
fontSize: 11, style: TextStyle(
color: AppColors.mainTextColor1,
fontSize: this.titleFontSize,
),
), ),
), ),
), ),
); );
} }
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 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import '../../../config/app_colors.dart'; import 'package:scrobbles/config/app_colors.dart';
import '../../../models/timeline.dart';
class ChartTimelineEclecticism extends StatelessWidget { class CustomLineChart extends StatelessWidget {
final TimelineData chartData; CustomLineChart({super.key});
const ChartTimelineEclecticism({super.key, required this.chartData}); final Widget placeholder = Text('⏳');
final double chartHeight = 150.0;
final double titleFontSize = 10;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
height: 100.0, child: SizedBox(
child: LineChart( height: this.chartHeight,
LineChartData(
lineBarsData: getDataEclecticism(),
backgroundColor: Theme.of(context).colorScheme.onBackground,
borderData: getBorderData(),
gridData: getGridData(),
titlesData: getTitlesData(),
lineTouchData: getLineTouchDataEclecticism(),
maxY: 100,
minY: 0,
),
duration: const Duration(milliseconds: 250),
), ),
); );
} }
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));
}
});
return [
LineChartBarData(
isCurved: true,
color: AppColors.contentColorCyan,
barWidth: 3,
isStrokeCapRound: false,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(show: true),
spots: spots,
),
];
}
FlBorderData getBorderData() { FlBorderData getBorderData() {
return FlBorderData( return FlBorderData(
show: true, show: false,
border: Border.all(
color: AppColors.borderColor,
width: 2,
),
); );
} }
FlGridData getGridData() { FlGridData getGridData() {
return const FlGridData( return const FlGridData(
show: true, show: false,
); );
} }
...@@ -80,11 +39,20 @@ class ChartTimelineEclecticism extends StatelessWidget { ...@@ -80,11 +39,20 @@ class ChartTimelineEclecticism extends StatelessWidget {
final AxisTitles verticalTitles = AxisTitles( final AxisTitles verticalTitles = AxisTitles(
sideTitles: SideTitles( sideTitles: SideTitles(
showTitles: true, showTitles: true,
reservedSize: 30, reservedSize: 35,
getTitlesWidget: getVerticalTitlesWidget, getTitlesWidget: getVerticalTitlesWidget,
interval: 25, interval: 25,
), ),
); );
final AxisTitles verticalSpacer = AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 35,
getTitlesWidget: getVerticalTitlesSpacerWidget,
),
);
final AxisTitles horizontalTitles = AxisTitles( final AxisTitles horizontalTitles = AxisTitles(
sideTitles: SideTitles( sideTitles: SideTitles(
showTitles: true, showTitles: true,
...@@ -96,7 +64,7 @@ class ChartTimelineEclecticism extends StatelessWidget { ...@@ -96,7 +64,7 @@ class ChartTimelineEclecticism extends StatelessWidget {
return FlTitlesData( return FlTitlesData(
show: true, show: true,
bottomTitles: horizontalTitles, bottomTitles: horizontalTitles,
leftTitles: none, leftTitles: verticalSpacer,
topTitles: none, topTitles: none,
rightTitles: verticalTitles, rightTitles: verticalTitles,
); );
...@@ -107,58 +75,24 @@ class ChartTimelineEclecticism extends StatelessWidget { ...@@ -107,58 +75,24 @@ class ChartTimelineEclecticism extends StatelessWidget {
axisSide: meta.axisSide, axisSide: meta.axisSide,
space: 4, space: 4,
child: Text( child: Text(
value.toInt().toString(), value.toInt().toString() + ' %',
style: const TextStyle( style: TextStyle(
color: AppColors.mainTextColor1, color: AppColors.mainTextColor1,
fontSize: 12, fontSize: this.titleFontSize,
), ),
), ),
); );
} }
Widget getHorizontalTitlesWidget(double value, TitleMeta meta) { Widget getVerticalTitlesSpacerWidget(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( return SideTitleWidget(
axisSide: meta.axisSide, axisSide: meta.axisSide,
space: 4, space: 4,
child: RotationTransition( child: Text(''),
turns: new AlwaysStoppedAnimation(-30 / 360),
child: Text(
text,
style: const TextStyle(
color: AppColors.mainTextColor1,
fontSize: 11,
),
),
),
); );
} }
LineTouchData getLineTouchDataEclecticism() { Widget getHorizontalTitlesWidget(double value, TitleMeta meta) {
return LineTouchData( return Text('');
handleBuiltInTouches: true,
touchTooltipData: LineTouchTooltipData(
tooltipBgColor: Colors.transparent,
tooltipPadding: EdgeInsets.all(8),
tooltipMargin: 2,
getTooltipItems: (List<LineBarSpot> touchedSpots) {
return touchedSpots.map((LineBarSpot touchedSpot) {
final textStyle = TextStyle(
color: AppColors.mainTextColor2,
fontWeight: FontWeight.bold,
fontSize: 10,
);
return LineTooltipItem(
touchedSpot.y.toString(),
textStyle,
);
}).toList();
},
),
);
} }
} }
import 'package:flutter/material.dart';
import 'package:unicons/unicons.dart';
import 'package:scrobbles/ui/widgets/header_app.dart';
class StandardAppBar extends StatelessWidget implements PreferredSizeWidget {
final Function() notifyParent;
const StandardAppBar({super.key, required this.notifyParent});
@override
Widget build(BuildContext context) {
return AppBar(
title: const AppHeader(text: 'app_name'),
actions: [
IconButton(
onPressed: () {
this.notifyParent();
},
icon: const Icon(UniconsSolid.refresh),
),
],
);
}
@override
Size get preferredSize => const Size.fromHeight(50);
}
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_swipe/flutter_swipe.dart';
import 'package:ionicons/ionicons.dart';
import 'package:scrobbles/cubit/bottom_nav_cubit.dart';
class BottomNavBar extends StatelessWidget {
const BottomNavBar({super.key, required this.swipeController});
final SwiperController swipeController;
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(
top: 1,
right: 0,
left: 0,
),
elevation: 4,
shadowColor: Theme.of(context).colorScheme.shadow,
color: Theme.of(context).colorScheme.surfaceVariant,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: BlocBuilder<BottomNavCubit, int>(
builder: (BuildContext context, int state) {
return BottomNavigationBar(
currentIndex: state,
onTap: (int index) {
context.read<BottomNavCubit>().updateIndex(index);
swipeController.move(index);
},
type: BottomNavigationBarType.fixed,
elevation: 0,
backgroundColor: Colors.transparent,
selectedItemColor: Theme.of(context).colorScheme.primary,
unselectedItemColor: Theme.of(context).textTheme.bodySmall!.color,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: const Icon(Ionicons.home_outline),
label: tr('bottom_nav_home'),
),
BottomNavigationBarItem(
icon: const Icon(Ionicons.star_outline),
label: tr('bottom_nav_discoveries'),
),
BottomNavigationBarItem(
icon: const Icon(Ionicons.bar_chart_outline),
label: tr('bottom_nav_repartition'),
),
BottomNavigationBarItem(
icon: const Icon(Ionicons.settings_outline),
label: tr('bottom_nav_settings'),
),
],
);
},
),
);
}
}
import 'package:flutter/material.dart';
class CardContent extends StatelessWidget {
const CardContent({
super.key,
required this.title,
required this.color,
required this.loader,
required this.content,
});
final String title;
final Color color;
final Widget loader;
final Widget content;
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
shadowColor: Theme.of(context).colorScheme.shadow,
color: this.color,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(8),
),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
this.title,
style:
Theme.of(context).primaryTextTheme.titleLarge!.apply(fontWeightDelta: 2),
),
this.loader,
],
),
const SizedBox(height: 8),
this.content,
],
),
),
);
}
}
import 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:scrobbles/config/settings.dart';
import 'package:scrobbles/cubit/data_counts_by_day_cubit.dart';
import 'package:scrobbles/models/counts_by_day.dart';
import 'package:scrobbles/network/scrobbles.dart';
import 'package:scrobbles/ui/widgets/card_content.dart';
import 'package:scrobbles/ui/widgets/charts/counts_by_day.dart';
import 'package:scrobbles/ui/widgets/error.dart';
class CardCountsByDay extends StatelessWidget {
const CardCountsByDay({super.key});
@override
Widget build(BuildContext context) {
final int daysCount = Settings.countsByDayDaysCount;
return BlocBuilder<DataCountsByDayCubit, DataCountsByDayState>(
builder: (BuildContext context, DataCountsByDayState state) {
return CardContent(
color: Theme.of(context).colorScheme.surface,
title: 'counts_by_day'.tr(
namedArgs: {
'daysCount': daysCount.toString(),
},
),
loader: updateCountsByDay(Settings.countsByDayDaysCount),
content: ChartCountsByDay(
chartData: CountsByDayData.fromJson(jsonDecode(state.countsByDay.toString())),
isLoading: false,
),
);
},
);
}
Widget updateCountsByDay(int daysCount) {
final Widget loading = const Text('⏳');
final Widget done = const Text('');
late Future<CountsByDayData> futureCountsByDay = ScrobblesApi.fetchCountsByDay(daysCount);
return BlocBuilder<DataCountsByDayCubit, DataCountsByDayState>(
builder: (BuildContext context, DataCountsByDayState state) {
return FutureBuilder<CountsByDayData>(
future: futureCountsByDay,
builder: (context, snapshot) {
if (snapshot.hasError) {
return ShowErrorWidget(message: '${snapshot.error}');
}
BlocProvider.of<DataCountsByDayCubit>(context).update(snapshot.data);
return !snapshot.hasData ? loading : done;
},
);
},
);
}
}
import 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:scrobbles/config/settings.dart';
import 'package:scrobbles/cubit/data_counts_by_hour_cubit.dart';
import 'package:scrobbles/models/counts_by_hour.dart';
import 'package:scrobbles/network/scrobbles.dart';
import 'package:scrobbles/ui/widgets/card_content.dart';
import 'package:scrobbles/ui/widgets/charts/counts_by_hour.dart';
import 'package:scrobbles/ui/widgets/error.dart';
class CardCountsByHour extends StatelessWidget {
const CardCountsByHour({super.key});
@override
Widget build(BuildContext context) {
final int daysCount = Settings.countsByHourDaysCount;
return BlocBuilder<DataCountsByHourCubit, DataCountsByHourState>(
builder: (BuildContext context, DataCountsByHourState state) {
return CardContent(
color: Theme.of(context).colorScheme.surface,
title: 'counts_by_hour'.tr(
namedArgs: {
'daysCount': daysCount.toString(),
},
),
loader: updateCountsByHour(Settings.countsByHourDaysCount),
content: ChartCountsByHour(
chartData: CountsByHourData.fromJson(jsonDecode(state.countsByHour.toString())),
isLoading: false,
),
);
},
);
}
Widget updateCountsByHour(int daysCount) {
final Widget loading = const Text('⏳');
final Widget done = const Text('');
late Future<CountsByHourData> futureCountsByHour =
ScrobblesApi.fetchCountsByHour(daysCount);
return BlocBuilder<DataCountsByHourCubit, DataCountsByHourState>(
builder: (BuildContext context, DataCountsByHourState state) {
return FutureBuilder<CountsByHourData>(
future: futureCountsByHour,
builder: (context, snapshot) {
if (snapshot.hasError) {
return ShowErrorWidget(message: '${snapshot.error}');
}
BlocProvider.of<DataCountsByHourCubit>(context).update(snapshot.data);
return !snapshot.hasData ? loading : done;
},
);
},
);
}
}
import 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:scrobbles/config/settings.dart';
import 'package:scrobbles/cubit/data_discoveries_cubit.dart';
import 'package:scrobbles/models/discoveries.dart';
import 'package:scrobbles/network/scrobbles.dart';
import 'package:scrobbles/ui/widgets/card_content.dart';
import 'package:scrobbles/ui/widgets/charts/discoveries_artists.dart';
import 'package:scrobbles/ui/widgets/charts/discoveries_tracks.dart';
import 'package:scrobbles/ui/widgets/error.dart';
class CardDiscoveries extends StatelessWidget {
const CardDiscoveries({super.key});
@override
Widget build(BuildContext context) {
final int daysCount = Settings.discoveriesDaysCount;
return BlocBuilder<DataDiscoveriesCubit, DataDiscoveriesState>(
builder: (BuildContext context, DataDiscoveriesState state) {
final TextTheme textTheme = Theme.of(context).primaryTextTheme;
return CardContent(
color: Theme.of(context).colorScheme.surface,
title: 'discoveries_title'.tr(
namedArgs: {
'daysCount': daysCount.toString(),
},
),
loader: updateDiscoveries(Settings.discoveriesDaysCount),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'discoveries_artists_title',
style: textTheme.titleMedium!.apply(fontWeightDelta: 2),
).tr(),
const SizedBox(height: 8),
ChartDiscoveriesArtists(
chartData: DiscoveriesData.fromJson(jsonDecode(state.discoveries.toString())),
isLoading: false,
),
const SizedBox(height: 8),
Text(
'discoveries_tracks_title',
style: textTheme.titleMedium!.apply(fontWeightDelta: 2),
).tr(),
const SizedBox(height: 8),
ChartDiscoveriesTracks(
chartData: DiscoveriesData.fromJson(jsonDecode(state.discoveries.toString())),
isLoading: false,
),
],
),
);
},
);
}
Widget updateDiscoveries(int daysCount) {
final Widget loading = const Text('⏳');
final Widget done = const Text('');
late Future<DiscoveriesData> futureDiscoveries = ScrobblesApi.fetchDiscoveries(daysCount);
return BlocBuilder<DataDiscoveriesCubit, DataDiscoveriesState>(
builder: (BuildContext context, DataDiscoveriesState state) {
return FutureBuilder<DiscoveriesData>(
future: futureDiscoveries,
builder: (context, snapshot) {
if (snapshot.hasError) {
return ShowErrorWidget(message: '${snapshot.error}');
}
BlocProvider.of<DataDiscoveriesCubit>(context).update(snapshot.data);
return !snapshot.hasData ? loading : done;
},
);
},
);
}
}
import 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:scrobbles/cubit/data_statistics_global_cubit.dart';
import 'package:scrobbles/models/statistics_global.dart';
import 'package:scrobbles/network/scrobbles.dart';
import 'package:scrobbles/ui/widgets/card_content.dart';
import 'package:scrobbles/ui/widgets/content/statistics_global.dart';
import 'package:scrobbles/ui/widgets/error.dart';
class CardStatisticsGlobal extends StatelessWidget {
const CardStatisticsGlobal({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<DataStatisticsGlobalCubit, DataStatisticsGlobalState>(
builder: (BuildContext context, DataStatisticsGlobalState state) {
return CardContent(
color: Theme.of(context).colorScheme.primary,
title: 'global_statistics'.tr(),
loader: updateStatisticsGlobal(),
content: ContentStatisticsGlobal(
statistics:
StatisticsGlobalData.fromJson(jsonDecode(state.statisticsGlobal.toString())),
isLoading: false,
),
);
},
);
}
Widget updateStatisticsGlobal() {
final Widget loading = const Text('⏳');
final Widget done = const Text('');
late Future<StatisticsGlobalData> futureStatisticsGlobal =
ScrobblesApi.fetchGlobalStatistics();
return BlocBuilder<DataStatisticsGlobalCubit, DataStatisticsGlobalState>(
builder: (BuildContext context, DataStatisticsGlobalState dataState) {
return FutureBuilder<StatisticsGlobalData>(
future: futureStatisticsGlobal,
builder: (context, snapshot) {
if (snapshot.hasError) {
return ShowErrorWidget(message: '${snapshot.error}');
}
BlocProvider.of<DataStatisticsGlobalCubit>(context).update(snapshot.data);
return !snapshot.hasData ? loading : done;
},
);
},
);
}
}
import 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:scrobbles/config/settings.dart';
import 'package:scrobbles/cubit/data_statistics_recent_cubit.dart';
import 'package:scrobbles/models/statistics_recent.dart';
import 'package:scrobbles/network/scrobbles.dart';
import 'package:scrobbles/ui/widgets/card_content.dart';
import 'package:scrobbles/ui/widgets/content/statistics_recent.dart';
import 'package:scrobbles/ui/widgets/error.dart';
class CardStatisticsRecent extends StatelessWidget {
const CardStatisticsRecent({super.key});
@override
Widget build(BuildContext context) {
final int daysCount = Settings.statisticsRecentDaysCount;
return BlocBuilder<DataStatisticsRecentCubit, DataStatisticsRecentState>(
builder: (BuildContext context, DataStatisticsRecentState dataState) {
return CardContent(
color: Theme.of(context).colorScheme.primary,
title: 'recent_statistics'.tr(
namedArgs: {
'daysCount': daysCount.toString(),
},
),
loader: updateStatisticsRecent(Settings.statisticsRecentDaysCount),
content: ContentStatisticsRecent(
statistics: StatisticsRecentData.fromJson(
jsonDecode(dataState.statisticsRecent.toString())),
isLoading: false,
),
);
},
);
}
Widget updateStatisticsRecent(int daysCount) {
final Widget loading = const Text('⏳');
final Widget done = const Text('');
late Future<StatisticsRecentData> futureStatisticsRecent =
ScrobblesApi.fetchRecentStatistics(daysCount);
return BlocBuilder<DataStatisticsRecentCubit, DataStatisticsRecentState>(
builder: (BuildContext context, DataStatisticsRecentState state) {
return FutureBuilder<StatisticsRecentData>(
future: futureStatisticsRecent,
builder: (context, snapshot) {
if (snapshot.hasError) {
return ShowErrorWidget(message: '${snapshot.error}');
}
BlocProvider.of<DataStatisticsRecentCubit>(context).update(snapshot.data);
return !snapshot.hasData ? loading : done;
},
);
},
);
}
}
import 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:scrobbles/config/settings.dart';
import 'package:scrobbles/cubit/data_timeline_cubit.dart';
import 'package:scrobbles/models/timeline.dart';
import 'package:scrobbles/network/scrobbles.dart';
import 'package:scrobbles/ui/widgets/card_content.dart';
import 'package:scrobbles/ui/widgets/charts/timeline_counts.dart';
import 'package:scrobbles/ui/widgets/charts/timeline_eclecticism.dart';
import 'package:scrobbles/ui/widgets/error.dart';
class CardTimeline extends StatelessWidget {
const CardTimeline({super.key});
@override
Widget build(BuildContext context) {
final int daysCount = Settings.timelineDaysCount;
return BlocBuilder<DataTimelineCubit, DataTimelineState>(
builder: (BuildContext context, DataTimelineState state) {
return CardContent(
color: Theme.of(context).colorScheme.surface,
title: 'timeline_title'.tr(
namedArgs: {
'daysCount': daysCount.toString(),
},
),
loader: updateTimeline(Settings.timelineDaysCount),
content: Stack(
children: [
ChartTimelineCounts(
chartData: TimelineData.fromJson(jsonDecode(state.timeline.toString())),
isLoading: false,
),
ChartTimelineEclecticism(
chartData: TimelineData.fromJson(jsonDecode(state.timeline.toString())),
isLoading: false,
),
],
),
);
},
);
}
Widget updateTimeline(int daysCount) {
final Widget loading = const Text('⏳');
final Widget done = const Text('');
late Future<TimelineData> futureTimeline = ScrobblesApi.fetchTimeline(daysCount);
return BlocBuilder<DataTimelineCubit, DataTimelineState>(
builder: (BuildContext context, DataTimelineState dataTimelineState) {
return FutureBuilder<TimelineData>(
future: futureTimeline,
builder: (context, snapshot) {
if (snapshot.hasError) {
return ShowErrorWidget(message: '${snapshot.error}');
}
BlocProvider.of<DataTimelineCubit>(context).update(snapshot.data);
return !snapshot.hasData ? loading : done;
},
);
},
);
}
}
import 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:scrobbles/config/settings.dart';
import 'package:scrobbles/cubit/data_top_artists_cubit.dart';
import 'package:scrobbles/models/topartists.dart';
import 'package:scrobbles/network/scrobbles.dart';
import 'package:scrobbles/ui/widgets/card_content.dart';
import 'package:scrobbles/ui/widgets/charts/top_artists.dart';
import 'package:scrobbles/ui/widgets/error.dart';
class CardTopArtists extends StatelessWidget {
const CardTopArtists({super.key});
@override
Widget build(BuildContext context) {
final int daysCount = Settings.topArtistsDaysCount;
return BlocBuilder<DataTopArtistsCubit, DataTopArtistsState>(
builder: (BuildContext context, DataTopArtistsState state) {
return CardContent(
color: Theme.of(context).colorScheme.surface,
title: 'top_artists_title'.tr(
namedArgs: {
'daysCount': daysCount.toString(),
},
),
loader: updateTopArtists(Settings.topArtistsDaysCount),
content: ChartTopArtists(
chartData: TopArtistsData.fromJson(jsonDecode(state.topArtists.toString())),
isLoading: false,
),
);
},
);
}
Widget updateTopArtists(int daysCount) {
final Widget loading = const Text('⏳');
final Widget done = const Text('');
late Future<TopArtistsData> futureTopArtists = ScrobblesApi.fetchTopArtists(daysCount);
return BlocBuilder<DataTopArtistsCubit, DataTopArtistsState>(
builder: (BuildContext context, DataTopArtistsState state) {
return FutureBuilder<TopArtistsData>(
future: futureTopArtists,
builder: (context, snapshot) {
if (snapshot.hasError) {
return ShowErrorWidget(message: '${snapshot.error}');
}
BlocProvider.of<DataTopArtistsCubit>(context).update(snapshot.data);
return !snapshot.hasData ? loading : done;
},
);
},
);
}
}