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

Merge branch '74-improve-game-architecture' into 'master'

Resolve "Improve game architecture"

Closes #74

See merge request !75
parents 934c31d2 4f928dc7
No related branches found
No related tags found
1 merge request!75Resolve "Improve game architecture"
Pipeline #5624 passed
Showing
with 472 additions and 84 deletions
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
app.versionName=0.1.38
app.versionCode=62
app.versionName=0.1.39
app.versionCode=63
File added
File added
File added
File added
{
"app_name": "Word guess",
"bottom_nav_home": "Game",
"bottom_nav_settings": "Settings",
"bottom_nav_about": "About",
"settings_title": "Settings",
"settings_label_theme": "Theme mode",
"about_title": "About",
"about_content": "Word guess game.",
"about_version": "Version: {version}"
}
{
"app_name": "Trouve le mot",
"bottom_nav_home": "Jeu",
"bottom_nav_settings": "Réglages",
"bottom_nav_about": "Infos",
"settings_title": "Réglages",
"settings_label_theme": "Thème de couleurs",
"about_title": "Informations",
"about_content": "Trouve le mot.",
"about_version": "Version : {version}"
}
Improve game conception/architecture.
Amélioration de la conception/architecture du jeu.
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:unicons/unicons.dart';
import 'package:wordguessing/ui/screens/about_page.dart';
import 'package:wordguessing/ui/screens/game_page.dart';
import 'package:wordguessing/ui/screens/settings_page.dart';
class MenuItem {
final String code;
final Icon icon;
final Widget page;
const MenuItem({
required this.code,
required this.icon,
required this.page,
});
}
class Menu {
static List<MenuItem> items = [
const MenuItem(
code: 'bottom_nav_home',
icon: Icon(UniconsLine.home),
page: GamePage(),
),
const MenuItem(
code: 'bottom_nav_settings',
icon: Icon(UniconsLine.setting),
page: SettingsPage(),
),
const MenuItem(
code: 'bottom_nav_about',
icon: Icon(UniconsLine.info_circle),
page: AboutPage(),
),
];
static Widget getPageWidget(int pageIndex) {
return Menu.items.elementAt(pageIndex).page;
}
static List<BottomNavigationBarItem> getMenuItems() {
return Menu.items
.map((MenuItem item) => BottomNavigationBarItem(
icon: item.icon,
label: tr(item.code),
))
.toList();
}
static int itemsCount = Menu.items.length;
}
import 'package:flutter/material.dart';
/// Colors from Tailwind CSS (v3.0) - June 2022
///
/// https://tailwindcss.com/docs/customizing-colors
const int _primaryColor = 0xFF6366F1;
const MaterialColor primarySwatch = MaterialColor(_primaryColor, <int, Color>{
50: Color(0xFFEEF2FF), // indigo-50
100: Color(0xFFE0E7FF), // indigo-100
200: Color(0xFFC7D2FE), // indigo-200
300: Color(0xFFA5B4FC), // indigo-300
400: Color(0xFF818CF8), // indigo-400
500: Color(_primaryColor), // indigo-500
600: Color(0xFF4F46E5), // indigo-600
700: Color(0xFF4338CA), // indigo-700
800: Color(0xFF3730A3), // indigo-800
900: Color(0xFF312E81), // indigo-900
});
const int _textColor = 0xFF64748B;
const MaterialColor textSwatch = MaterialColor(_textColor, <int, Color>{
50: Color(0xFFF8FAFC), // slate-50
100: Color(0xFFF1F5F9), // slate-100
200: Color(0xFFE2E8F0), // slate-200
300: Color(0xFFCBD5E1), // slate-300
400: Color(0xFF94A3B8), // slate-400
500: Color(_textColor), // slate-500
600: Color(0xFF475569), // slate-600
700: Color(0xFF334155), // slate-700
800: Color(0xFF1E293B), // slate-800
900: Color(0xFF0F172A), // slate-900
});
const Color errorColor = Color(0xFFDC2626); // red-600
final ColorScheme lightColorScheme = ColorScheme.light(
primary: primarySwatch.shade500,
secondary: primarySwatch.shade500,
onSecondary: Colors.white,
error: errorColor,
background: textSwatch.shade200,
onBackground: textSwatch.shade500,
onSurface: textSwatch.shade500,
surface: textSwatch.shade50,
surfaceVariant: Colors.white,
shadow: textSwatch.shade900.withOpacity(.1),
);
final ColorScheme darkColorScheme = ColorScheme.dark(
primary: primarySwatch.shade500,
secondary: primarySwatch.shade500,
onSecondary: Colors.white,
error: errorColor,
background: const Color(0xFF171724),
onBackground: textSwatch.shade400,
onSurface: textSwatch.shade300,
surface: const Color(0xFF262630),
surfaceVariant: const Color(0xFF282832),
shadow: textSwatch.shade900.withOpacity(.2),
);
final ThemeData lightTheme = ThemeData(
colorScheme: lightColorScheme,
fontFamily: 'Nunito',
textTheme: TextTheme(
displayLarge: TextStyle(
color: textSwatch.shade700,
fontFamily: 'Nunito',
),
displayMedium: TextStyle(
color: textSwatch.shade600,
fontFamily: 'Nunito',
),
displaySmall: TextStyle(
color: textSwatch.shade500,
fontFamily: 'Nunito',
),
headlineLarge: TextStyle(
color: textSwatch.shade700,
fontFamily: 'Nunito',
),
headlineMedium: TextStyle(
color: textSwatch.shade600,
fontFamily: 'Nunito',
),
headlineSmall: TextStyle(
color: textSwatch.shade500,
fontFamily: 'Nunito',
),
titleLarge: TextStyle(
color: textSwatch.shade700,
fontFamily: 'Nunito',
),
titleMedium: TextStyle(
color: textSwatch.shade600,
fontFamily: 'Nunito',
),
titleSmall: TextStyle(
color: textSwatch.shade500,
fontFamily: 'Nunito',
),
bodyLarge: TextStyle(
color: textSwatch.shade700,
fontFamily: 'Nunito',
),
bodyMedium: TextStyle(
color: textSwatch.shade600,
fontFamily: 'Nunito',
),
bodySmall: TextStyle(
color: textSwatch.shade500,
fontFamily: 'Nunito',
),
labelLarge: TextStyle(
color: textSwatch.shade700,
fontFamily: 'Nunito',
),
labelMedium: TextStyle(
color: textSwatch.shade600,
fontFamily: 'Nunito',
),
labelSmall: TextStyle(
color: textSwatch.shade500,
fontFamily: 'Nunito',
),
),
);
final ThemeData darkTheme = lightTheme.copyWith(
colorScheme: darkColorScheme,
textTheme: TextTheme(
displayLarge: TextStyle(
color: textSwatch.shade200,
fontFamily: 'Nunito',
),
displayMedium: TextStyle(
color: textSwatch.shade300,
fontFamily: 'Nunito',
),
displaySmall: TextStyle(
color: textSwatch.shade400,
fontFamily: 'Nunito',
),
headlineLarge: TextStyle(
color: textSwatch.shade200,
fontFamily: 'Nunito',
),
headlineMedium: TextStyle(
color: textSwatch.shade300,
fontFamily: 'Nunito',
),
headlineSmall: TextStyle(
color: textSwatch.shade400,
fontFamily: 'Nunito',
),
titleLarge: TextStyle(
color: textSwatch.shade200,
fontFamily: 'Nunito',
),
titleMedium: TextStyle(
color: textSwatch.shade300,
fontFamily: 'Nunito',
),
titleSmall: TextStyle(
color: textSwatch.shade400,
fontFamily: 'Nunito',
),
bodyLarge: TextStyle(
color: textSwatch.shade200,
fontFamily: 'Nunito',
),
bodyMedium: TextStyle(
color: textSwatch.shade300,
fontFamily: 'Nunito',
),
bodySmall: TextStyle(
color: textSwatch.shade400,
fontFamily: 'Nunito',
),
labelLarge: TextStyle(
color: textSwatch.shade200,
fontFamily: 'Nunito',
),
labelMedium: TextStyle(
color: textSwatch.shade300,
fontFamily: 'Nunito',
),
labelSmall: TextStyle(
color: textSwatch.shade400,
fontFamily: 'Nunito',
),
),
);
final ThemeData appTheme = darkTheme;
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:wordguessing/config/menu.dart';
class BottomNavCubit extends HydratedCubit<int> {
BottomNavCubit() : super(0);
void updateIndex(int index) {
if (isIndexAllowed(index)) {
emit(index);
} else {
goToHomePage();
}
}
bool isIndexAllowed(int index) {
return (index >= 0) && (index < Menu.itemsCount);
}
void goToHomePage() => emit(0);
@override
int fromJson(Map<String, dynamic> json) {
return 0;
}
@override
Map<String, dynamic>? toJson(int state) {
return <String, int>{'pageIndex': state};
}
}
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
part 'theme_state.dart';
class ThemeCubit extends HydratedCubit<ThemeModeState> {
ThemeCubit() : super(const ThemeModeState());
void getTheme(ThemeModeState state) {
emit(state);
}
@override
ThemeModeState? fromJson(Map<String, dynamic> json) {
switch (json['themeMode']) {
case 'ThemeMode.dark':
return const ThemeModeState(themeMode: ThemeMode.dark);
case 'ThemeMode.light':
return const ThemeModeState(themeMode: ThemeMode.light);
case 'ThemeMode.system':
default:
return const ThemeModeState(themeMode: ThemeMode.system);
}
}
@override
Map<String, String>? toJson(ThemeModeState state) {
return <String, String>{'themeMode': state.themeMode.toString()};
}
}
part of 'theme_cubit.dart';
@immutable
class ThemeModeState extends Equatable {
const ThemeModeState({
this.themeMode,
});
final ThemeMode? themeMode;
@override
List<Object?> get props => <Object?>[
themeMode,
];
}
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hive/hive.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:wordguessing/config/theme.dart';
import 'package:wordguessing/cubit/bottom_nav_cubit.dart';
import 'package:wordguessing/cubit/theme_cubit.dart';
import 'package:wordguessing/provider/data.dart';
import 'package:wordguessing/screens/game_pick_image.dart';
import 'package:wordguessing/screens/game_pick_word.dart';
import 'package:wordguessing/screens/home.dart';
import 'package:wordguessing/utils/tools.dart';
import 'package:wordguessing/ui/skeleton.dart';
void main() {
void main() async {
/// Initialize packages
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp])
.then((value) => runApp(const MyApp()));
await EasyLocalization.ensureInitialized();
final Directory tmpDir = await getTemporaryDirectory();
Hive.init(tmpDir.toString());
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: tmpDir,
);
runApp(
EasyLocalization(
path: 'assets/translations',
supportedLocales: const <Locale>[
Locale('en'),
Locale('fr'),
],
fallbackLocale: const Locale('en'),
useFallbackTranslations: true,
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
......@@ -19,31 +43,34 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<BottomNavCubit>(create: (context) => BottomNavCubit()),
BlocProvider<ThemeCubit>(create: (context) => ThemeCubit()),
],
child: BlocBuilder<ThemeCubit, ThemeModeState>(
builder: (BuildContext context, ThemeModeState state) {
return ChangeNotifierProvider(
create: (BuildContext context) => Data(),
child: Consumer<Data>(builder: (context, data, child) {
child: Consumer<Data>(
builder: (context, data, child) {
return MaterialApp(
title: 'Jeux de mots et lettres',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const Home(),
onGenerateRoute: (settings) {
switch (settings.name) {
case '/game-pick-word':
return MaterialPageRoute(builder: (context) => const GamePickWordPage());
case '/game-pick-image':
return MaterialPageRoute(
builder: (context) => const GamePickImagePage(),
);
home: const SkeletonScreen(),
default:
printlog("Unknown menu entry");
}
// Theme stuff
theme: lightTheme,
darkTheme: darkTheme,
themeMode: state.themeMode,
return null;
// Localization stuff
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
locale: context.locale,
debugShowCheckedModeBanner: false,
);
},
),
);
}),
);
......
......@@ -2,7 +2,11 @@ import 'package:flutter/foundation.dart';
import 'package:wordguessing/models/word.dart';
enum GameType { pickWord, pickImage }
class Data extends ChangeNotifier {
GameType? _gameType;
// Language
String _lang = '';
......@@ -18,6 +22,18 @@ class Data extends ChangeNotifier {
int _goodAnswers = 0;
int _wrongAnswers = 0;
GameType? get gameType => _gameType;
void updateGameType(GameType gameType) {
_gameType = gameType;
notifyListeners();
}
void resetGameType() {
_gameType = null;
notifyListeners();
}
String get lang => _lang;
void updateLang(String value) {
......@@ -55,6 +71,7 @@ class Data extends ChangeNotifier {
}
void resetGame() {
resetGameType();
updateLang('');
updateQuestionsCount(0);
updateGoodAnswers(0);
......
......@@ -3,13 +3,12 @@ import 'package:provider/provider.dart';
import 'package:wordguessing/provider/data.dart';
class Game extends StatelessWidget {
const Game({super.key});
class GameAbstract extends StatelessWidget {
const GameAbstract({super.key});
final int countWords = 4;
Future<void> startGame(Data myProvider, String lang) async {
myProvider.resetGame();
myProvider.updateLang(lang);
await nextWord(myProvider);
}
......@@ -137,27 +136,6 @@ class Game extends StatelessWidget {
Widget buildPage(BuildContext context) {
final Data myProvider = Provider.of<Data>(context);
final Scaffold pageContent = Scaffold(
appBar: AppBar(
elevation: 0,
actions: <Widget>[
IconButton(
icon: const Icon(Icons.loop),
onPressed: () => myProvider.resetGame(),
),
],
),
backgroundColor: Colors.blue,
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: buildPageContent(myProvider),
),
),
);
return SizedBox.expand(
child: FittedBox(
fit: BoxFit.contain,
......@@ -165,7 +143,14 @@ class Game extends StatelessWidget {
child: SizedBox(
height: (MediaQuery.of(context).size.height),
width: (MediaQuery.of(context).size.width),
child: pageContent,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: buildPageContent(myProvider),
),
),
),
),
);
......
......@@ -2,10 +2,10 @@ import 'package:flutter/material.dart';
import 'package:wordguessing/models/word.dart';
import 'package:wordguessing/provider/data.dart';
import 'package:wordguessing/screens/game.dart';
import 'package:wordguessing/ui/games/abstract_game.dart';
import 'package:wordguessing/utils/random_pick_words.dart';
class GamePickImagePage extends Game {
class GamePickImagePage extends GameAbstract {
const GamePickImagePage({super.key});
@override
......
......@@ -2,10 +2,10 @@ import 'package:flutter/material.dart';
import 'package:wordguessing/models/word.dart';
import 'package:wordguessing/provider/data.dart';
import 'package:wordguessing/screens/game.dart';
import 'package:wordguessing/ui/games/abstract_game.dart';
import 'package:wordguessing/utils/random_pick_words.dart';
class GamePickWordPage extends Game {
class GamePickWordPage extends GameAbstract {
const GamePickWordPage({super.key});
@override
......
......@@ -3,16 +3,14 @@ import 'package:provider/provider.dart';
import 'package:wordguessing/provider/data.dart';
class Home extends StatelessWidget {
const Home({super.key});
static const String id = 'home';
class GameSelector extends StatelessWidget {
const GameSelector({super.key});
@override
Widget build(BuildContext context) {
final Data myProvider = Provider.of<Data>(context);
Container buildMenuItemContainer(String code, Color color) {
Widget buildMenuItemContainer(String code, Color color) {
const double imageSize = 150;
final String imageAsset = 'assets/menu/$code.png';
......@@ -35,18 +33,21 @@ class Home extends StatelessWidget {
),
onTap: () {
myProvider.resetGame();
Navigator.pushNamed(
context,
'/$code',
);
switch (code) {
case 'game-pick-word':
myProvider.updateGameType(GameType.pickWord);
break;
case 'game-pick-image':
myProvider.updateGameType(GameType.pickImage);
break;
default:
}
},
),
);
}
return Scaffold(
backgroundColor: Colors.blue,
body: Center(
Widget content = Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
......@@ -56,7 +57,8 @@ class Home extends StatelessWidget {
buildMenuItemContainer('game-pick-image', Colors.yellow),
],
),
),
);
return content;
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment