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

Normalize app architecture

parent 2fb921a6
No related branches found
No related tags found
1 merge request!61Resolve "Normalize game architecture"
Pipeline #5730 passed
Showing
with 369 additions and 110 deletions
File moved
File moved
import 'dart:convert'; import 'dart:convert';
import 'package:scrobbles/models/artists.dart'; import 'package:scrobbles/models/data/artists.dart';
class NewArtistData { class NewArtistData {
final DateTime? firstPlayed; final DateTime? firstPlayed;
......
import 'dart:convert'; import 'dart:convert';
import 'package:scrobbles/models/track.dart'; import 'package:scrobbles/models/data/track.dart';
class NewTrackData { class NewTrackData {
final DateTime? firstPlayed; final DateTime? firstPlayed;
......
File moved
File moved
import 'dart:convert'; import 'dart:convert';
import 'package:scrobbles/models/artists.dart'; import 'package:scrobbles/models/data/artists.dart';
class Track { class Track {
final int id; final int id;
......
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:scrobbles/models/counts_by_day.dart'; import 'package:scrobbles/models/data/counts_by_day.dart';
import 'package:scrobbles/models/counts_by_hour.dart'; import 'package:scrobbles/models/data/counts_by_hour.dart';
import 'package:scrobbles/models/discoveries.dart'; import 'package:scrobbles/models/data/discoveries.dart';
import 'package:scrobbles/models/heatmap.dart'; import 'package:scrobbles/models/data/heatmap.dart';
import 'package:scrobbles/models/new_artists.dart'; import 'package:scrobbles/models/data/new_artists.dart';
import 'package:scrobbles/models/new_tracks.dart'; import 'package:scrobbles/models/data/new_tracks.dart';
import 'package:scrobbles/models/statistics_global.dart'; import 'package:scrobbles/models/data/statistics_global.dart';
import 'package:scrobbles/models/statistics_recent.dart'; import 'package:scrobbles/models/data/statistics_recent.dart';
import 'package:scrobbles/models/timeline.dart'; import 'package:scrobbles/models/data/timeline.dart';
import 'package:scrobbles/models/topartists.dart'; import 'package:scrobbles/models/data/topartists.dart';
class ScrobblesApi { class ScrobblesApi {
static String baseUrl = 'https://scrobble.harrault.fr'; static String baseUrl = 'https://scrobble.harrault.fr';
static Future<StatisticsGlobalData> fetchGlobalStatistics() async { static Future<StatisticsGlobalData?> fetchGlobalStatistics() async {
final String url = '$baseUrl/stats'; final String url = '$baseUrl/stats';
final response = await http.get(Uri.parse(url)); final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) { if (response.statusCode == 200) {
return StatisticsGlobalData.fromJson(jsonDecode(response.body) as Map<String, dynamic>); final Map<String, dynamic> rawData = jsonDecode(response.body) as Map<String, dynamic>;
} else { return StatisticsGlobalData.fromJson(rawData);
throw Exception('Failed to get data from API.');
} }
return null;
} }
static Future<StatisticsRecentData> fetchRecentStatistics(int daysCount) async { static Future<StatisticsRecentData?> fetchRecentStatistics(int daysCount) async {
final String url = '$baseUrl/$daysCount/stats'; final String url = '$baseUrl/$daysCount/stats';
final response = await http.get(Uri.parse(url)); final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) { if (response.statusCode == 200) {
return StatisticsRecentData.fromJson(jsonDecode(response.body) as Map<String, dynamic>); final Map<String, dynamic> rawData = jsonDecode(response.body) as Map<String, dynamic>;
} else { return StatisticsRecentData.fromJson(rawData);
throw Exception('Failed to get data from API.');
} }
return null;
} }
static Future<TimelineData> fetchTimeline(int daysCount) async { static Future<TimelineData?> fetchTimeline(int daysCount) async {
final String url = '$baseUrl/data/$daysCount/timeline'; final String url = '$baseUrl/data/$daysCount/timeline';
final response = await http.get(Uri.parse(url)); final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) { if (response.statusCode == 200) {
return TimelineData.fromJson(jsonDecode(response.body) as Map<String, dynamic>); final Map<String, dynamic> rawData = jsonDecode(response.body) as Map<String, dynamic>;
} else { return TimelineData.fromJson(rawData);
throw Exception('Failed to get data from API.');
} }
return null;
} }
static Future<CountsByDayData> fetchCountsByDay(int daysCount) async { static Future<CountsByDayData?> fetchCountsByDay(int daysCount) async {
final String url = '$baseUrl/data/$daysCount/counts-by-day'; final String url = '$baseUrl/data/$daysCount/counts-by-day';
final response = await http.get(Uri.parse(url)); final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) { if (response.statusCode == 200) {
return CountsByDayData.fromJson(jsonDecode(response.body) as Map<String, dynamic>); final Map<String, dynamic> rawData = jsonDecode(response.body) as Map<String, dynamic>;
} else { return CountsByDayData.fromJson(rawData);
throw Exception('Failed to get data from API.');
} }
return null;
} }
static Future<CountsByHourData> fetchCountsByHour(int daysCount) async { static Future<CountsByHourData?> fetchCountsByHour(int daysCount) async {
final String url = '$baseUrl/data/$daysCount/counts-by-hour'; final String url = '$baseUrl/data/$daysCount/counts-by-hour';
final response = await http.get(Uri.parse(url)); final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) { if (response.statusCode == 200) {
return CountsByHourData.fromJson(jsonDecode(response.body) as Map<String, dynamic>); final Map<String, dynamic> rawData = jsonDecode(response.body) as Map<String, dynamic>;
} else { return CountsByHourData.fromJson(rawData);
throw Exception('Failed to get data from API.');
} }
return null;
} }
static Future<DiscoveriesData> fetchDiscoveries(int daysCount) async { static Future<DiscoveriesData?> fetchDiscoveries(int daysCount) async {
final String url = '$baseUrl/data/$daysCount/news'; final String url = '$baseUrl/data/$daysCount/news';
final response = await http.get(Uri.parse(url)); final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) { if (response.statusCode == 200) {
return DiscoveriesData.fromJson(jsonDecode(response.body) as Map<String, dynamic>); final Map<String, dynamic> rawData = jsonDecode(response.body) as Map<String, dynamic>;
} else { return DiscoveriesData.fromJson(rawData);
throw Exception('Failed to get data from API.');
} }
return null;
} }
static Future<TopArtistsData> fetchTopArtists(int daysCount) async { static Future<TopArtistsData?> fetchTopArtists(int daysCount) async {
final String url = '$baseUrl/data/$daysCount/top-artists'; final String url = '$baseUrl/data/$daysCount/top-artists';
final response = await http.get(Uri.parse(url)); final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) { if (response.statusCode == 200) {
return TopArtistsData.fromJson(jsonDecode(response.body) as Map<String, dynamic>); final Map<String, dynamic> rawData = jsonDecode(response.body) as Map<String, dynamic>;
} else { return TopArtistsData.fromJson(rawData);
throw Exception('Failed to get data from API.');
} }
return null;
} }
static Future<HeatmapData> fetchHeatmap(int daysCount) async { static Future<HeatmapData?> fetchHeatmap(int daysCount) async {
final String url = '$baseUrl/data/$daysCount/heatmap'; final String url = '$baseUrl/data/$daysCount/heatmap';
final response = await http.get(Uri.parse(url)); final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) { if (response.statusCode == 200) {
return HeatmapData.fromJson(jsonDecode(response.body) as Map<String, dynamic>); final Map<String, dynamic> rawData = jsonDecode(response.body) as Map<String, dynamic>;
} else { return HeatmapData.fromJson(rawData);
throw Exception('Failed to get data from API.');
} }
return null;
} }
static Future<NewArtistsData> fetchNewArtists(int count) async { static Future<NewArtistsData?> fetchNewArtists(int count) async {
final String url = '$baseUrl/data/discoveries/artists/$count'; final String url = '$baseUrl/data/discoveries/artists/$count';
final response = await http.get(Uri.parse(url)); final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) { if (response.statusCode == 200) {
return NewArtistsData.fromJson(jsonDecode(response.body) as List<dynamic>); final List<dynamic> rawData = jsonDecode(response.body) as List<dynamic>;
} else { return NewArtistsData.fromJson(rawData);
throw Exception('Failed to get data from API.');
} }
return null;
} }
static Future<NewTracksData> fetchNewTracks(int count) async { static Future<NewTracksData?> fetchNewTracks(int count) async {
final String url = '$baseUrl/data/discoveries/tracks/$count'; final String url = '$baseUrl/data/discoveries/tracks/$count';
final response = await http.get(Uri.parse(url)); final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) { if (response.statusCode == 200) {
return NewTracksData.fromJson(jsonDecode(response.body) as List<dynamic>); final List<dynamic> rawData = jsonDecode(response.body) as List<dynamic>;
} else { return NewTracksData.fromJson(rawData);
throw Exception('Failed to get data from API.');
} }
return null;
} }
} }
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 AppTitle extends StatelessWidget { class AppHeader extends StatelessWidget {
const AppTitle({super.key, required this.text}); const AppHeader({super.key, required this.text});
final String text; final String text;
...@@ -11,37 +11,22 @@ class AppTitle extends StatelessWidget { ...@@ -11,37 +11,22 @@ class AppTitle extends StatelessWidget {
return Text( return Text(
tr(text), tr(text),
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: Theme.of(context).textTheme.headlineLarge!.apply(fontWeightDelta: 2), style: Theme.of(context).textTheme.headlineMedium!.apply(fontWeightDelta: 2),
); );
} }
} }
class AppTitle1 extends StatelessWidget { class AppTitle extends StatelessWidget {
const AppTitle1({super.key, required this.text}); const AppTitle({super.key, required this.text});
final String text; final String text;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Text( return Text(
text, tr(text),
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: Theme.of(context).textTheme.titleLarge!.apply(fontWeightDelta: 2), style: Theme.of(context).textTheme.titleLarge!.apply(fontWeightDelta: 2),
); );
} }
} }
class AppTitle2 extends StatelessWidget {
const AppTitle2({super.key, required this.text});
final String text;
@override
Widget build(BuildContext context) {
return Text(
text,
textAlign: TextAlign.start,
style: Theme.of(context).textTheme.titleMedium!.apply(fontWeightDelta: 2),
);
}
}
...@@ -2,9 +2,9 @@ import 'package:easy_localization/easy_localization.dart'; ...@@ -2,9 +2,9 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_swipe/flutter_swipe.dart'; import 'package:flutter_swipe/flutter_swipe.dart';
import 'package:ionicons/ionicons.dart';
import 'package:scrobbles/cubit/bottom_nav_cubit.dart'; import 'package:scrobbles/config/activity_page.dart';
import 'package:scrobbles/cubit/nav_cubit_pages.dart';
class BottomNavBar extends StatelessWidget { class BottomNavBar extends StatelessWidget {
const BottomNavBar({super.key, required this.swipeController}); const BottomNavBar({super.key, required this.swipeController});
...@@ -17,14 +17,26 @@ class BottomNavBar extends StatelessWidget { ...@@ -17,14 +17,26 @@ class BottomNavBar extends StatelessWidget {
margin: const EdgeInsets.all(0), margin: const EdgeInsets.all(0),
elevation: 4, elevation: 4,
shadowColor: Theme.of(context).colorScheme.shadow, shadowColor: Theme.of(context).colorScheme.shadow,
color: Theme.of(context).colorScheme.surfaceVariant, color: Theme.of(context).colorScheme.surfaceContainerHighest,
shape: const ContinuousRectangleBorder(), shape: const ContinuousRectangleBorder(),
child: BlocBuilder<BottomNavCubit, int>( child: BlocBuilder<NavCubitPage, int>(
builder: (BuildContext context, int state) { builder: (BuildContext context, int pageIndex) {
final List<ActivityPageItem> pageItems = [
ActivityPage.pageHome,
ActivityPage.pageDiscoveries,
ActivityPage.pageStatistics,
];
final List<BottomNavigationBarItem> items = pageItems.map((ActivityPageItem item) {
return BottomNavigationBarItem(
icon: item.icon,
label: tr(item.code),
);
}).toList();
return BottomNavigationBar( return BottomNavigationBar(
currentIndex: state, currentIndex: pageIndex,
onTap: (int index) { onTap: (int index) {
context.read<BottomNavCubit>().updateIndex(index); context.read<NavCubitPage>().updateIndex(index);
swipeController.move(index); swipeController.move(index);
}, },
type: BottomNavigationBarType.fixed, type: BottomNavigationBarType.fixed,
...@@ -32,24 +44,7 @@ class BottomNavBar extends StatelessWidget { ...@@ -32,24 +44,7 @@ class BottomNavBar extends StatelessWidget {
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
selectedItemColor: Theme.of(context).colorScheme.primary, selectedItemColor: Theme.of(context).colorScheme.primary,
unselectedItemColor: Theme.of(context).textTheme.bodySmall!.color, unselectedItemColor: Theme.of(context).textTheme.bodySmall!.color,
items: <BottomNavigationBarItem>[ items: items,
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';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:scrobbles/cubit/activity_cubit.dart';
import 'package:unicons/unicons.dart';
import 'package:scrobbles/cubit/nav_cubit_pages.dart';
import 'package:scrobbles/config/screen.dart';
import 'package:scrobbles/cubit/nav_cubit_screens.dart';
import 'package:scrobbles/ui/helpers/app_titles.dart';
class GlobalAppBar extends StatelessWidget implements PreferredSizeWidget {
const GlobalAppBar({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<NavCubitScreen, int>(
builder: (BuildContext context, int screenIndex) {
final List<Widget> menuActions = [];
if (screenIndex == Screen.indexActivity) {
// go to Settings page
menuActions.add(IconButton(
onPressed: () {
context.read<NavCubitScreen>().goToSettingsPage();
},
icon: Screen.screenSettings.icon,
));
// go to About page
menuActions.add(IconButton(
onPressed: () {
context.read<NavCubitScreen>().goToAboutPage();
},
icon: Screen.screenAbout.icon,
));
// refresh data
menuActions.add(IconButton(
onPressed: () {
BlocProvider.of<ActivityCubit>(context).refresh(context);
},
icon: const Icon(UniconsSolid.refresh),
));
} else {
// back to Home page
menuActions.add(IconButton(
onPressed: () {
context.read<NavCubitScreen>().goToActivityPage();
context.read<NavCubitPage>().goToHomePage();
},
icon: Screen.screenActivity.icon,
));
}
return AppBar(
title: const AppHeader(text: 'app_name'),
actions: menuActions,
);
},
);
}
@override
Size get preferredSize => const Size.fromHeight(50);
}
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:scrobbles/cubit/activity_cubit.dart';
import 'package:scrobbles/ui/widgets/cards/discoveries.dart';
import 'package:scrobbles/ui/widgets/cards/new_artists.dart';
import 'package:scrobbles/ui/widgets/cards/new_tracks.dart';
class PageDiscoveries extends StatelessWidget {
const PageDiscoveries({super.key});
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: () async {
BlocProvider.of<ActivityCubit>(context).refresh(context);
},
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 4),
physics: const BouncingScrollPhysics(),
children: const <Widget>[
SizedBox(height: 8),
CardDiscoveries(),
SizedBox(height: 6),
CardNewArtists(),
SizedBox(height: 6),
CardNewTracks(),
SizedBox(height: 36),
],
),
);
}
}
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:scrobbles/cubit/activity_cubit.dart';
import 'package:scrobbles/ui/widgets/cards/statistics_global.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/statistics_recent.dart';
import 'package:scrobbles/ui/widgets/cards/timeline.dart'; import 'package:scrobbles/ui/widgets/cards/timeline.dart';
import 'package:scrobbles/ui/widgets/cards/top_artists.dart'; import 'package:scrobbles/ui/widgets/cards/top_artists.dart';
class ScreenHome extends StatelessWidget { class PageHome extends StatelessWidget {
final Function() notifyParent; const PageHome({super.key});
const ScreenHome({super.key, required this.notifyParent});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return RefreshIndicator(
color: Theme.of(context).colorScheme.background,
child: RefreshIndicator(
onRefresh: () async { onRefresh: () async {
notifyParent(); BlocProvider.of<ActivityCubit>(context).refresh(context);
}, },
child: ListView( child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 4), padding: const EdgeInsets.symmetric(horizontal: 4),
...@@ -33,7 +31,6 @@ class ScreenHome extends StatelessWidget { ...@@ -33,7 +31,6 @@ class ScreenHome extends StatelessWidget {
SizedBox(height: 36), SizedBox(height: 36),
], ],
), ),
),
); );
} }
} }
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:scrobbles/cubit/activity_cubit.dart';
import 'package:scrobbles/ui/widgets/cards/counts_by_day.dart';
import 'package:scrobbles/ui/widgets/cards/counts_by_hour.dart';
import 'package:scrobbles/ui/widgets/cards/heatmap.dart';
class PageStatistics extends StatelessWidget {
const PageStatistics({super.key});
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: () async {
BlocProvider.of<ActivityCubit>(context).refresh(context);
},
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 4),
physics: const BouncingScrollPhysics(),
children: const <Widget>[
SizedBox(height: 8),
CardHeatmap(),
SizedBox(height: 6),
CardCountsByDay(),
SizedBox(height: 6),
CardCountsByHour(),
SizedBox(height: 36),
],
),
);
}
}
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:scrobbles/ui/helpers/app_titles.dart';
class ScreenAbout extends StatelessWidget {
const ScreenAbout({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
const SizedBox(height: 8),
const AppTitle(text: 'about_title'),
const Text('about_content').tr(),
FutureBuilder<PackageInfo>(
future: PackageInfo.fromPlatform(),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.done:
return const Text('about_version').tr(
namedArgs: {
'version': snapshot.data!.version,
},
);
default:
return const SizedBox();
}
},
),
],
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_swipe/flutter_swipe.dart';
import 'package:scrobbles/config/activity_page.dart';
import 'package:scrobbles/config/screen.dart';
import 'package:scrobbles/cubit/nav_cubit_pages.dart';
import 'package:scrobbles/ui/nav/bottom_nav_bar.dart';
class ScreenActivity extends StatelessWidget {
const ScreenActivity({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<NavCubitPage, int>(
builder: (BuildContext context, int pageIndex) {
return Swiper(
itemCount: Screen.itemsCount,
itemBuilder: (BuildContext context, int pageIndex) {
return ActivityPage.getPageWidget(pageIndex);
},
pagination: SwiperPagination(
margin: const EdgeInsets.all(0),
builder: SwiperCustomPagination(
builder: (BuildContext context, SwiperPluginConfig config) {
return BottomNavBar(swipeController: config.controller);
},
),
),
onIndexChanged: (newPageIndex) {
BlocProvider.of<NavCubitPage>(context).updateIndex(newPageIndex);
},
outer: true,
loop: false,
);
},
);
}
}
import 'package:flutter/material.dart';
import 'package:scrobbles/ui/widgets/cards/discoveries.dart';
import 'package:scrobbles/ui/widgets/cards/new_artists.dart';
import 'package:scrobbles/ui/widgets/cards/new_tracks.dart';
class ScreenDiscoveries extends StatelessWidget {
final Function() notifyParent;
const ScreenDiscoveries({super.key, required this.notifyParent});
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.background,
child: RefreshIndicator(
onRefresh: () async {
notifyParent();
},
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 4),
physics: const BouncingScrollPhysics(),
children: const <Widget>[
SizedBox(height: 8),
CardDiscoveries(),
SizedBox(height: 6),
CardNewArtists(),
SizedBox(height: 6),
CardNewTracks(),
SizedBox(height: 36),
],
),
),
);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment