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 'package:http/http.dart' as http;
import '../models/counts_by_day.dart';
import '../models/counts_by_hour.dart';
import '../models/statistics.dart';
import '../models/timeline.dart';
import 'package:scrobbles/models/counts_by_day.dart';
import 'package:scrobbles/models/counts_by_hour.dart';
import 'package:scrobbles/models/discoveries.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 {
static String baseUrl = 'https://scrobble.harrault.fr';
static Future<StatisticsData> fetchStatistics() async {
final response = await http.get(Uri.parse(baseUrl + '/stats'));
static Future<StatisticsGlobalData> fetchGlobalStatistics() async {
final String url = baseUrl + '/stats';
print('fetching ' + url);
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
return 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 {
throw Exception('Failed to get data from API.');
}
......@@ -21,9 +40,11 @@ class ScrobblesApi {
static Future<TimelineData> fetchTimeline(int daysCount) async {
final String url = baseUrl + '/data/' + daysCount.toString() + '/timeline';
print('fetching ' + url);
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
print('ok - fetched ' + url);
return TimelineData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
throw Exception('Failed to get data from API.');
......@@ -32,9 +53,11 @@ class ScrobblesApi {
static Future<CountsByDayData> fetchCountsByDay(int daysCount) async {
final String url = baseUrl + '/data/' + daysCount.toString() + '/counts-by-day';
print('fetching ' + url);
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
print('ok - fetched ' + url);
return CountsByDayData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
throw Exception('Failed to get data from API.');
......@@ -43,12 +66,40 @@ class ScrobblesApi {
static Future<CountsByHourData> fetchCountsByHour(int daysCount) async {
final String url = baseUrl + '/data/' + daysCount.toString() + '/counts-by-hour';
print('fetching ' + url);
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
print('ok - fetched ' + url);
return CountsByHourData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
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 '../widgets/header.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';
import 'package:scrobbles/ui/widgets/cards/discoveries.dart';
class MainScreen extends StatelessWidget {
const MainScreen({super.key});
class ScreenDiscoveries extends StatelessWidget {
const ScreenDiscoveries({super.key});
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.background,
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.symmetric(horizontal: 4),
physics: const BouncingScrollPhysics(),
children: <Widget>[
const Header(text: 'app_name'),
const StatisticsCard(),
const SizedBox(height: 8),
const ChartTimelineCard(),
const SizedBox(height: 8),
const ChartCountsByDayCard(),
const SizedBox(height: 8),
const ChartCountsByHourCard(),
const CardDiscoveries(),
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:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import '../../../config/app_colors.dart';
import '../../../models/timeline.dart';
import '../../../utils/color_extensions.dart';
import 'package:scrobbles/config/app_colors.dart';
import 'package:scrobbles/utils/color_extensions.dart';
class ChartTimelineCounts extends StatelessWidget {
final TimelineData chartData;
class CustomBarChart extends StatelessWidget {
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
Widget build(BuildContext context) {
return Container(
height: 150.0,
child: LayoutBuilder(builder: (context, constraints) {
final double maxWidth = constraints.maxWidth;
final int barsCount = this.chartData.data.keys.length;
final double barWidth = 0.7 * (maxWidth / barsCount);
return BarChart(
BarChartData(
barGroups: getDataCounts(barWidth),
backgroundColor: Theme.of(context).colorScheme.onBackground,
borderData: getBorderData(),
gridData: getGridData(),
titlesData: getTitlesData(),
barTouchData: getBarTouchData(),
maxY: getNextRoundNumber(getMaxCountsValue(), 50),
),
);
}),
child: SizedBox(
height: this.chartHeight,
),
);
}
BarChart getBarChart({barWidth, backgroundColor}) {
return BarChart(
BarChartData(
barGroups: getDataCounts(barWidth),
backgroundColor: backgroundColor,
borderData: getBorderData(),
gridData: getGridData(),
titlesData: getTitlesData(),
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 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;
return 0.0;
}
double getNextRoundNumber(double number, int scale) {
double getNextRoundNumber(double number, double scale) {
return scale * ((number ~/ scale).toInt() + 1);
}
List<BarChartGroupData> getDataCounts(double barWidth) {
List<BarChartGroupData> data = [];
this.chartData.data.keys.forEach((key) {
TimelineDataValue? value = this.chartData.data[key];
if (value != null) {
final int date = DateTime.parse(key).millisecondsSinceEpoch;
final double counts = value.counts.toDouble();
data.add(BarChartGroupData(
x: date,
barRods: [
BarChartRodData(
toY: counts,
color: AppColors.contentColorOrange,
width: barWidth,
borderRadius: BorderRadius.all(Radius.zero),
borderSide: BorderSide(
color: AppColors.contentColorOrange.darken(20),
),
),
],
));
}
});
BarChartGroupData getBarItem(
{required int x,
required List<double> values,
required List<Color> barColors,
required double barWidth}) {
List<BarChartRodData> barRods = [];
for (int i = 0; i < values.length; i++) {
double value = values[i];
Color barColor = barColors[i];
final gradient = this.getGradient(barColor, value, this.getMaxCountsValue());
final borderColor = barColor.darken(20);
barRods.add(
BarChartRodData(
toY: value,
color: barColor,
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() {
......@@ -97,6 +115,8 @@ class ChartTimelineCounts extends StatelessWidget {
FlGridData getGridData() {
return const FlGridData(
show: true,
drawHorizontalLine: true,
drawVerticalLine: false,
);
}
......@@ -108,10 +128,21 @@ class ChartTimelineCounts extends StatelessWidget {
final AxisTitles verticalTitles = AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
reservedSize: 35,
getTitlesWidget: getVerticalTitlesWidget,
interval: this.verticalTicksInterval,
),
);
final AxisTitles verticalSpacer = AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 35,
getTitlesWidget: getVerticalTitlesSpacerWidget,
interval: this.verticalTicksInterval,
),
);
final AxisTitles horizontalTitles = AxisTitles(
sideTitles: SideTitles(
showTitles: true,
......@@ -123,26 +154,37 @@ class ChartTimelineCounts extends StatelessWidget {
return FlTitlesData(
show: true,
bottomTitles: horizontalTitles,
leftTitles: none,
leftTitles: verticalTitles,
topTitles: none,
rightTitles: verticalTitles,
rightTitles: verticalSpacer,
);
}
Widget getVerticalTitlesWidget(double value, TitleMeta meta) {
String suffix =
this.verticalAxisTitleSuffix != '' ? ' ' + this.verticalAxisTitleSuffix : '';
return SideTitleWidget(
axisSide: meta.axisSide,
space: 4,
child: Text(
value.toInt().toString(),
style: const TextStyle(
value.toInt().toString() + suffix,
style: TextStyle(
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) {
final DateFormat formatter = DateFormat('dd/MM');
......@@ -152,42 +194,19 @@ class ChartTimelineCounts extends StatelessWidget {
return SideTitleWidget(
axisSide: meta.axisSide,
space: 4,
child: RotationTransition(
turns: new AlwaysStoppedAnimation(-30 / 360),
child: Text(
text,
style: const TextStyle(
color: AppColors.mainTextColor1,
fontSize: 11,
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,
),
),
),
),
);
}
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:flutter/material.dart';
import '../../../config/app_colors.dart';
import '../../../models/timeline.dart';
import 'package:scrobbles/config/app_colors.dart';
class ChartTimelineEclecticism extends StatelessWidget {
final TimelineData chartData;
class CustomLineChart extends StatelessWidget {
CustomLineChart({super.key});
const ChartTimelineEclecticism({super.key, required this.chartData});
final Widget placeholder = Text('⏳');
final double chartHeight = 150.0;
final double titleFontSize = 10;
@override
Widget build(BuildContext context) {
return Container(
height: 100.0,
child: LineChart(
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),
child: SizedBox(
height: this.chartHeight,
),
);
}
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() {
return FlBorderData(
show: true,
border: Border.all(
color: AppColors.borderColor,
width: 2,
),
show: false,
);
}
FlGridData getGridData() {
return const FlGridData(
show: true,
show: false,
);
}
......@@ -80,11 +39,20 @@ class ChartTimelineEclecticism extends StatelessWidget {
final AxisTitles verticalTitles = AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
reservedSize: 35,
getTitlesWidget: getVerticalTitlesWidget,
interval: 25,
),
);
final AxisTitles verticalSpacer = AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 35,
getTitlesWidget: getVerticalTitlesSpacerWidget,
),
);
final AxisTitles horizontalTitles = AxisTitles(
sideTitles: SideTitles(
showTitles: true,
......@@ -96,7 +64,7 @@ class ChartTimelineEclecticism extends StatelessWidget {
return FlTitlesData(
show: true,
bottomTitles: horizontalTitles,
leftTitles: none,
leftTitles: verticalSpacer,
topTitles: none,
rightTitles: verticalTitles,
);
......@@ -107,58 +75,24 @@ class ChartTimelineEclecticism extends StatelessWidget {
axisSide: meta.axisSide,
space: 4,
child: Text(
value.toInt().toString(),
style: const TextStyle(
value.toInt().toString() + ' %',
style: TextStyle(
color: AppColors.mainTextColor1,
fontSize: 12,
fontSize: this.titleFontSize,
),
),
);
}
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);
Widget getVerticalTitlesSpacerWidget(double value, TitleMeta meta) {
return SideTitleWidget(
axisSide: meta.axisSide,
space: 4,
child: RotationTransition(
turns: new AlwaysStoppedAnimation(-30 / 360),
child: Text(
text,
style: const TextStyle(
color: AppColors.mainTextColor1,
fontSize: 11,
),
),
),
child: Text(''),
);
}
LineTouchData getLineTouchDataEclecticism() {
return LineTouchData(
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();
},
),
);
Widget getHorizontalTitlesWidget(double value, TitleMeta meta) {
return Text('');
}
}
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;
},
);
},
);
}
}