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

Merge branch '65-normalize-game-architecture' into 'master'

Resolve "Normalize game architecture"

Closes #65

See merge request !61
parents 2fb921a6 4b567d90
No related branches found
No related tags found
1 merge request!61Resolve "Normalize game architecture"
Pipeline #5748 passed
Showing
with 369 additions and 110 deletions
File moved
File moved
import 'dart:convert';
import 'package:scrobbles/models/artists.dart';
import 'package:scrobbles/models/data/artists.dart';
class NewArtistData {
final DateTime? firstPlayed;
......
import 'dart:convert';
import 'package:scrobbles/models/track.dart';
import 'package:scrobbles/models/data/track.dart';
class NewTrackData {
final DateTime? firstPlayed;
......
File moved
File moved
import 'dart:convert';
import 'package:scrobbles/models/artists.dart';
import 'package:scrobbles/models/data/artists.dart';
class Track {
final int id;
......
import 'dart:convert';
import 'package:http/http.dart' as http;
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/heatmap.dart';
import 'package:scrobbles/models/new_artists.dart';
import 'package:scrobbles/models/new_tracks.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';
import 'package:scrobbles/models/data/counts_by_day.dart';
import 'package:scrobbles/models/data/counts_by_hour.dart';
import 'package:scrobbles/models/data/discoveries.dart';
import 'package:scrobbles/models/data/heatmap.dart';
import 'package:scrobbles/models/data/new_artists.dart';
import 'package:scrobbles/models/data/new_tracks.dart';
import 'package:scrobbles/models/data/statistics_global.dart';
import 'package:scrobbles/models/data/statistics_recent.dart';
import 'package:scrobbles/models/data/timeline.dart';
import 'package:scrobbles/models/data/topartists.dart';
class ScrobblesApi {
static String baseUrl = 'https://scrobble.harrault.fr';
static Future<StatisticsGlobalData> fetchGlobalStatistics() async {
static Future<StatisticsGlobalData?> fetchGlobalStatistics() async {
final String url = '$baseUrl/stats';
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
return StatisticsGlobalData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
throw Exception('Failed to get data from API.');
final Map<String, dynamic> rawData = jsonDecode(response.body) as Map<String, dynamic>;
return StatisticsGlobalData.fromJson(rawData);
}
return null;
}
static Future<StatisticsRecentData> fetchRecentStatistics(int daysCount) async {
static Future<StatisticsRecentData?> fetchRecentStatistics(int daysCount) async {
final String url = '$baseUrl/$daysCount/stats';
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
return StatisticsRecentData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
throw Exception('Failed to get data from API.');
final Map<String, dynamic> rawData = jsonDecode(response.body) as Map<String, dynamic>;
return StatisticsRecentData.fromJson(rawData);
}
return null;
}
static Future<TimelineData> fetchTimeline(int daysCount) async {
static Future<TimelineData?> fetchTimeline(int daysCount) async {
final String url = '$baseUrl/data/$daysCount/timeline';
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
return TimelineData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
throw Exception('Failed to get data from API.');
final Map<String, dynamic> rawData = jsonDecode(response.body) as Map<String, dynamic>;
return TimelineData.fromJson(rawData);
}
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 response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
return CountsByDayData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
throw Exception('Failed to get data from API.');
final Map<String, dynamic> rawData = jsonDecode(response.body) as Map<String, dynamic>;
return CountsByDayData.fromJson(rawData);
}
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 response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
return CountsByHourData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
throw Exception('Failed to get data from API.');
final Map<String, dynamic> rawData = jsonDecode(response.body) as Map<String, dynamic>;
return CountsByHourData.fromJson(rawData);
}
return null;
}
static Future<DiscoveriesData> fetchDiscoveries(int daysCount) async {
static Future<DiscoveriesData?> fetchDiscoveries(int daysCount) async {
final String url = '$baseUrl/data/$daysCount/news';
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
return DiscoveriesData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
throw Exception('Failed to get data from API.');
final Map<String, dynamic> rawData = jsonDecode(response.body) as Map<String, dynamic>;
return DiscoveriesData.fromJson(rawData);
}
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 response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
return TopArtistsData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
throw Exception('Failed to get data from API.');
final Map<String, dynamic> rawData = jsonDecode(response.body) as Map<String, dynamic>;
return TopArtistsData.fromJson(rawData);
}
return null;
}
static Future<HeatmapData> fetchHeatmap(int daysCount) async {
static Future<HeatmapData?> fetchHeatmap(int daysCount) async {
final String url = '$baseUrl/data/$daysCount/heatmap';
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
return HeatmapData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
throw Exception('Failed to get data from API.');
final Map<String, dynamic> rawData = jsonDecode(response.body) as Map<String, dynamic>;
return HeatmapData.fromJson(rawData);
}
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 response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
return NewArtistsData.fromJson(jsonDecode(response.body) as List<dynamic>);
} else {
throw Exception('Failed to get data from API.');
final List<dynamic> rawData = jsonDecode(response.body) as List<dynamic>;
return NewArtistsData.fromJson(rawData);
}
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 response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
return NewTracksData.fromJson(jsonDecode(response.body) as List<dynamic>);
} else {
throw Exception('Failed to get data from API.');
final List<dynamic> rawData = jsonDecode(response.body) as List<dynamic>;
return NewTracksData.fromJson(rawData);
}
return null;
}
}
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
class AppTitle extends StatelessWidget {
const AppTitle({super.key, required this.text});
class AppHeader extends StatelessWidget {
const AppHeader({super.key, required this.text});
final String text;
......@@ -11,37 +11,22 @@ class AppTitle extends StatelessWidget {
return Text(
tr(text),
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 {
const AppTitle1({super.key, required this.text});
class AppTitle extends StatelessWidget {
const AppTitle({super.key, required this.text});
final String text;
@override
Widget build(BuildContext context) {
return Text(
text,
tr(text),
textAlign: TextAlign.start,
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';
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';
import 'package:scrobbles/config/activity_page.dart';
import 'package:scrobbles/cubit/nav_cubit_pages.dart';
class BottomNavBar extends StatelessWidget {
const BottomNavBar({super.key, required this.swipeController});
......@@ -17,14 +17,26 @@ class BottomNavBar extends StatelessWidget {
margin: const EdgeInsets.all(0),
elevation: 4,
shadowColor: Theme.of(context).colorScheme.shadow,
color: Theme.of(context).colorScheme.surfaceVariant,
color: Theme.of(context).colorScheme.surfaceContainerHighest,
shape: const ContinuousRectangleBorder(),
child: BlocBuilder<BottomNavCubit, int>(
builder: (BuildContext context, int state) {
child: BlocBuilder<NavCubitPage, int>(
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(
currentIndex: state,
currentIndex: pageIndex,
onTap: (int index) {
context.read<BottomNavCubit>().updateIndex(index);
context.read<NavCubitPage>().updateIndex(index);
swipeController.move(index);
},
type: BottomNavigationBarType.fixed,
......@@ -32,24 +44,7 @@ class BottomNavBar extends StatelessWidget {
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'),
),
],
items: items,
);
},
),
......
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_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_recent.dart';
import 'package:scrobbles/ui/widgets/cards/timeline.dart';
import 'package:scrobbles/ui/widgets/cards/top_artists.dart';
class ScreenHome extends StatelessWidget {
final Function() notifyParent;
const ScreenHome({super.key, required this.notifyParent});
class PageHome extends StatelessWidget {
const PageHome({super.key});
@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),
CardStatisticsGlobal(),
SizedBox(height: 6),
CardStatisticsRecent(),
SizedBox(height: 6),
CardTimeline(),
SizedBox(height: 6),
CardTopArtists(),
SizedBox(height: 36),
],
),
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),
CardStatisticsGlobal(),
SizedBox(height: 6),
CardStatisticsRecent(),
SizedBox(height: 6),
CardTimeline(),
SizedBox(height: 6),
CardTopArtists(),
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.
Finish editing this message first!
Please register or to comment