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

Merge branch '79-add-settings-page' into 'master'

Resolve "Add "settings" page"

Closes #79

See merge request !74
parents 45fe7ba0 85639c5c
No related branches found
No related tags found
1 merge request!74Resolve "Add "settings" page"
Pipeline #5618 passed
Showing
with 405 additions and 120 deletions
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
app.versionName=0.1.20
app.versionCode=69
app.versionName=0.1.21
app.versionCode=70
{
"app_name": "Sudoku"
"app_name": "Sudoku",
"bottom_nav_game": "Game",
"bottom_nav_settings": "Settings",
"settings_title": "Settings",
"settings_label_theme": "Theme mode"
}
{
"app_name": "Sudoku"
"app_name": "Sudoku",
"bottom_nav_game": "Jeu",
"bottom_nav_settings": "Réglages",
"settings_title": "Réglages",
"settings_label_theme": "Thème de couleurs"
}
Add "settings" page with dark/light theme selector.
Ajout d'une page de réglages avec sélection du thème clair ou sombre.
import 'package:flutter/material.dart';
import 'package:unicons/unicons.dart';
import 'package:sudoku/ui/screens/game_page.dart';
import 'package:sudoku/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 const indexGame = 0;
static const menuItemGame = MenuItem(
code: 'bottom_nav_game',
icon: Icon(UniconsLine.home),
page: GamePage(),
);
static const indexSettings = 1;
static const menuItemSettings = MenuItem(
code: 'bottom_nav_settings',
icon: Icon(UniconsLine.setting),
page: SettingsPage(),
);
static Map<int, MenuItem> items = {
indexGame: menuItemGame,
indexSettings: menuItemSettings,
};
static bool isIndexAllowed(int pageIndex) {
return items.keys.contains(pageIndex);
}
static Widget getPageWidget(int pageIndex) {
return items[pageIndex]?.page ?? menuItemGame.page;
}
static int itemsCount = Menu.items.length;
}
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:sudoku/config/menu.dart';
class NavCubit extends HydratedCubit<int> {
NavCubit() : super(0);
void updateIndex(int index) {
if (Menu.isIndexAllowed(index)) {
emit(index);
} else {
goToGamePage();
}
}
void goToGamePage() {
emit(Menu.indexGame);
}
void switchToSettingsPage() {
if (state != Menu.indexSettings) {
emit(Menu.indexSettings);
} else {
goToGamePage();
}
}
@override
int fromJson(Map<String, dynamic> json) {
return Menu.indexGame;
}
@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,
];
}
......@@ -6,13 +6,14 @@ 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:overlay_support/overlay_support.dart';
import 'package:sudoku/config/default_global_settings.dart';
import 'package:sudoku/config/theme.dart';
import 'package:sudoku/cubit/game_cubit.dart';
import 'package:sudoku/cubit/nav_cubit.dart';
import 'package:sudoku/cubit/settings_game_cubit.dart';
import 'package:sudoku/cubit/settings_global_cubit.dart';
import 'package:sudoku/cubit/theme_cubit.dart';
import 'package:sudoku/ui/skeleton.dart';
void main() async {
......@@ -51,22 +52,30 @@ class MyApp extends StatelessWidget {
return MultiBlocProvider(
providers: [
BlocProvider<NavCubit>(create: (context) => NavCubit()),
BlocProvider<ThemeCubit>(create: (context) => ThemeCubit()),
BlocProvider<GameCubit>(create: (context) => GameCubit()),
BlocProvider<GlobalSettingsCubit>(create: (context) => GlobalSettingsCubit()),
BlocProvider<GameSettingsCubit>(create: (context) => GameSettingsCubit()),
],
child: OverlaySupport(
child: MaterialApp(
title: 'Sudoku',
theme: appTheme,
home: const SkeletonScreen(),
child: BlocBuilder<ThemeCubit, ThemeModeState>(
builder: (BuildContext context, ThemeModeState state) {
return MaterialApp(
title: 'Sudoku',
home: const SkeletonScreen(),
// Localization stuff
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
locale: context.locale,
debugShowCheckedModeBanner: false,
),
// Theme stuff
theme: lightTheme,
darkTheme: darkTheme,
themeMode: state.themeMode,
// Localization stuff
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
locale: context.locale,
debugShowCheckedModeBanner: false,
);
},
),
);
}
......
......@@ -15,7 +15,7 @@ class BoardLayout extends StatelessWidget {
builder: (BuildContext context, GameState gameState) {
final Game game = gameState.game;
const Color borderColor = Colors.black;
final Color borderColor = Theme.of(context).colorScheme.onBackground;
return Container(
margin: const EdgeInsets.all(2),
......
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:sudoku/cubit/game_cubit.dart';
import 'package:sudoku/ui/widgets/game.dart';
import 'package:sudoku/ui/widgets/parameters.dart';
class GamePage extends StatelessWidget {
const GamePage({super.key});
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.background,
child: BlocBuilder<GameCubit, GameState>(
builder: (BuildContext context, GameState gameState) {
return gameState.game.isRunning
? const GameWidget()
: Parameters(
canResume: gameState.game.isStarted && !gameState.game.isFinished,
);
},
),
);
}
}
import 'package:flutter/material.dart';
import 'package:sudoku/ui/widgets/header_app.dart';
import 'package:sudoku/ui/widgets/settings/settings_form.dart';
class SettingsPage extends StatelessWidget {
const SettingsPage({super.key});
@override
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: 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 'package:flutter_bloc/flutter_bloc.dart';
import 'package:sudoku/cubit/game_cubit.dart';
import 'package:sudoku/ui/screens/screen_game.dart';
import 'package:sudoku/ui/screens/screen_parameters.dart';
import 'package:sudoku/config/menu.dart';
import 'package:sudoku/cubit/nav_cubit.dart';
import 'package:sudoku/ui/widgets/global_app_bar.dart';
class SkeletonScreen extends StatelessWidget {
......@@ -14,22 +13,10 @@ class SkeletonScreen extends StatelessWidget {
return Scaffold(
appBar: const GlobalAppBar(),
extendBodyBehindAppBar: false,
body: Material(
color: Theme.of(context).colorScheme.background,
child: Container(
margin: const EdgeInsets.only(
top: 8.0,
),
child: BlocBuilder<GameCubit, GameState>(
builder: (BuildContext context, GameState gameState) {
return gameState.game.isRunning
? const ScreenGame()
: ScreenParameters(
canResume: gameState.game.isStarted && !gameState.game.isFinished,
);
},
),
),
body: BlocBuilder<NavCubit, int>(
builder: (BuildContext context, int pageIndex) {
return Menu.getPageWidget(pageIndex);
},
),
backgroundColor: Theme.of(context).colorScheme.background,
);
......
......@@ -115,8 +115,8 @@ class CellWidget extends StatelessWidget {
// Compute cell borders, from board size and cell state
Border getCellBorders(Game game) {
const Color cellBorderDarkColor = Colors.black;
const Color cellBorderLightColor = Colors.grey;
final Color cellBorderDarkColor = Colors.grey.shade800;
final Color cellBorderLightColor = Colors.grey.shade600;
const Color cellBorderSelectedColor = Colors.red;
Color cellBorderColor = cellBorderSelectedColor;
......
......@@ -36,7 +36,7 @@ class CellWidgetUpdate extends StatelessWidget {
decoration: BoxDecoration(
color: backgroundColor,
border: Border.all(
color: Colors.black,
color: Colors.grey.shade700,
width: 2,
),
),
......
......@@ -7,8 +7,8 @@ import 'package:sudoku/ui/layout/board.dart';
import 'package:sudoku/ui/widgets/message_game_end.dart';
import 'package:sudoku/ui/widgets/bar_select_cell_value.dart';
class ScreenGame extends StatelessWidget {
const ScreenGame({super.key});
class GameWidget extends StatelessWidget {
const GameWidget({super.key});
@override
Widget build(BuildContext context) {
......@@ -16,17 +16,20 @@ class ScreenGame extends StatelessWidget {
builder: (BuildContext context, GameState gameState) {
final Game game = gameState.game;
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 8),
const BoardLayout(),
const SizedBox(height: 8),
game.isFinished ? const SizedBox.shrink() : const SelectCellValueBar(),
const Expanded(child: SizedBox.shrink()),
game.isFinished ? const EndGameMessage() : const SizedBox.shrink(),
],
return Container(
padding: const EdgeInsets.all(4),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 8),
const BoardLayout(),
const SizedBox(height: 8),
game.isFinished ? const SizedBox.shrink() : const SelectCellValueBar(),
const Expanded(child: SizedBox.shrink()),
game.isFinished ? const EndGameMessage() : const SizedBox.shrink(),
],
),
);
},
);
......
import 'package:badges/badges.dart' as badges;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:overlay_support/overlay_support.dart';
import 'package:sudoku/config/default_global_settings.dart';
import 'package:sudoku/config/menu.dart';
import 'package:sudoku/cubit/game_cubit.dart';
import 'package:sudoku/cubit/nav_cubit.dart';
import 'package:sudoku/models/game.dart';
class GlobalAppBar extends StatelessWidget implements PreferredSizeWidget {
......@@ -14,95 +15,123 @@ class GlobalAppBar extends StatelessWidget implements PreferredSizeWidget {
Widget build(BuildContext context) {
return BlocBuilder<GameCubit, GameState>(
builder: (BuildContext context, GameState gameState) {
final Game game = gameState.game;
return BlocBuilder<NavCubit, int>(
builder: (BuildContext context, int pageIndex) {
final Game game = gameState.game;
final List<Widget> menuActions = [];
final List<Widget> menuActions = [];
if (game.isRunning && !game.isFinished) {
final GameCubit gameCubit = BlocProvider.of<GameCubit>(context);
if (game.isRunning && !game.isFinished) {
final GameCubit gameCubit = BlocProvider.of<GameCubit>(context);
menuActions.add(TextButton(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: Colors.white,
width: 3,
),
),
child: const Image(
image: AssetImage('assets/icons/button_back.png'),
fit: BoxFit.fill,
),
),
onPressed: () => toast('Long press to quit game...'),
onLongPress: () {
gameCubit.quitGame();
},
));
menuActions.add(const Spacer(flex: 6));
menuActions.add(TextButton(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: Colors.white,
width: 3,
),
),
child: badges.Badge(
showBadge: game.givenTipsCount == 0 ? false : true,
badgeStyle: badges.BadgeStyle(
badgeColor: game.givenTipsCount < 10
? Colors.green
: game.givenTipsCount < 20
? Colors.orange
: Colors.red,
menuActions.add(TextButton(
onPressed: null,
onLongPress: () {
gameCubit.quitGame();
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: Colors.white,
width: 3,
),
),
child: const Image(
image: AssetImage('assets/icons/button_back.png'),
fit: BoxFit.fill,
),
),
badgeContent: Text(
game.givenTipsCount == 0 ? '' : game.givenTipsCount.toString(),
style: const TextStyle(color: Colors.white),
));
menuActions.add(const Spacer(flex: 6));
menuActions.add(TextButton(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: Colors.white,
width: 3,
),
),
child: badges.Badge(
showBadge: game.givenTipsCount == 0 ? false : true,
badgeStyle: badges.BadgeStyle(
badgeColor: game.givenTipsCount < 10
? Colors.green
: game.givenTipsCount < 20
? Colors.orange
: Colors.red,
),
badgeContent: Text(
game.givenTipsCount == 0 ? '' : game.givenTipsCount.toString(),
style: const TextStyle(color: Colors.white),
),
child: Container(
padding: EdgeInsets.all(15 *
game.buttonTipsCountdown /
DefaultGlobalSettings.defaultTipCountDownValueInSeconds),
child: const Image(
image: AssetImage('assets/icons/button_help.png'),
fit: BoxFit.fill,
),
),
),
),
onPressed: () {
final GameCubit gameCubit = BlocProvider.of<GameCubit>(context);
game.canGiveTip() ? game.showTip(gameCubit) : null;
},
));
menuActions.add(const Spacer());
menuActions.add(TextButton(
child: Container(
padding: EdgeInsets.all(15 *
game.buttonTipsCountdown /
DefaultGlobalSettings.defaultTipCountDownValueInSeconds),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: game.showConflicts == true ? Colors.blue : Colors.white,
width: 3,
),
),
child: const Image(
image: AssetImage('assets/icons/button_help.png'),
image: AssetImage('assets/icons/button_show_conflicts.png'),
fit: BoxFit.fill,
),
),
),
),
onPressed: () {
final GameCubit gameCubit = BlocProvider.of<GameCubit>(context);
game.canGiveTip() ? game.showTip(gameCubit) : null;
},
));
menuActions.add(const Spacer());
menuActions.add(TextButton(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: game.showConflicts == true ? Colors.blue : Colors.white,
width: 3,
),
),
child: const Image(
image: AssetImage('assets/icons/button_show_conflicts.png'),
fit: BoxFit.fill,
),
),
onPressed: () {
gameCubit.toggleShowConflicts();
},
));
}
onPressed: () {
gameCubit.toggleShowConflicts();
},
));
} else {
if (pageIndex == Menu.indexGame) {
// go to Settings page
menuActions.add(ElevatedButton(
onPressed: () {
context.read<NavCubit>().switchToSettingsPage();
},
style: ElevatedButton.styleFrom(
shape: const CircleBorder(),
),
child: Menu.menuItemSettings.icon,
));
} else {
// back to Home page
menuActions.add(ElevatedButton(
onPressed: () {
context.read<NavCubit>().goToGamePage();
},
style: ElevatedButton.styleFrom(
shape: const CircleBorder(),
),
child: Menu.menuItemGame.icon,
));
}
}
return AppBar(
title: const SizedBox.shrink(),
actions: menuActions,
return AppBar(
title: const SizedBox.shrink(),
actions: menuActions,
);
},
);
},
);
......
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
class AppHeader extends StatelessWidget {
const AppHeader({super.key, required this.text});
final String text;
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
tr(text),
textAlign: TextAlign.start,
style: Theme.of(context).textTheme.headlineSmall!.apply(fontWeightDelta: 2),
),
const SizedBox(height: 8),
],
);
}
}
......@@ -11,8 +11,8 @@ import 'package:sudoku/ui/widgets/button_game_start_new.dart';
import 'package:sudoku/ui/widgets/button_resume_saved_game.dart';
import 'package:sudoku/ui/widgets/parameter_image.dart';
class ScreenParameters extends StatelessWidget {
const ScreenParameters({super.key, required this.canResume});
class Parameters extends StatelessWidget {
const Parameters({super.key, required this.canResume});
final bool canResume;
......@@ -22,6 +22,8 @@ class ScreenParameters extends StatelessWidget {
Widget build(BuildContext context) {
final List<Widget> lines = [];
lines.add(SizedBox(height: separatorHeight));
// Game settings
for (String code in DefaultGameSettings.availableParameters) {
lines.add(Row(
......
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