From 85639c5c8f5d664190246a2ac61d503b840acad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Harrault?= <benoit@harrault.fr> Date: Thu, 2 May 2024 21:51:56 +0200 Subject: [PATCH] Add "settings" page with dart/light theme selector --- android/gradle.properties | 4 +- assets/translations/en.json | 8 +- assets/translations/fr.json | 8 +- .../metadata/android/en-US/changelogs/70.txt | 1 + .../metadata/android/fr-FR/changelogs/70.txt | 1 + lib/config/menu.dart | 48 +++++ lib/cubit/nav_cubit.dart | 37 ++++ lib/cubit/theme_cubit.dart | 31 +++ lib/cubit/theme_state.dart | 15 ++ lib/main.dart | 33 ++-- lib/ui/layout/board.dart | 2 +- lib/ui/screens/game_page.dart | 26 +++ lib/ui/screens/screen_game.dart | 34 ---- lib/ui/screens/settings_page.dart | 26 +++ lib/ui/skeleton.dart | 25 +-- lib/ui/widgets/cell_widget.dart | 4 +- lib/ui/widgets/cell_widget_update.dart | 2 +- lib/ui/widgets/game.dart | 37 ++++ lib/ui/widgets/global_app_bar.dart | 187 ++++++++++-------- lib/ui/widgets/header_app.dart | 24 +++ .../parameters.dart} | 6 +- lib/ui/widgets/settings/settings_form.dart | 63 ++++++ lib/ui/widgets/settings/theme_card.dart | 47 +++++ pubspec.lock | 24 +-- pubspec.yaml | 9 +- 25 files changed, 528 insertions(+), 174 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/70.txt create mode 100644 fastlane/metadata/android/fr-FR/changelogs/70.txt create mode 100644 lib/config/menu.dart create mode 100644 lib/cubit/nav_cubit.dart create mode 100644 lib/cubit/theme_cubit.dart create mode 100644 lib/cubit/theme_state.dart create mode 100644 lib/ui/screens/game_page.dart delete mode 100644 lib/ui/screens/screen_game.dart create mode 100644 lib/ui/screens/settings_page.dart create mode 100644 lib/ui/widgets/game.dart create mode 100644 lib/ui/widgets/header_app.dart rename lib/ui/{screens/screen_parameters.dart => widgets/parameters.dart} (96%) create mode 100644 lib/ui/widgets/settings/settings_form.dart create mode 100644 lib/ui/widgets/settings/theme_card.dart diff --git a/android/gradle.properties b/android/gradle.properties index 6da1d1a..4f6eb84 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,5 +1,5 @@ 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 diff --git a/assets/translations/en.json b/assets/translations/en.json index a815023..0047e08 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -1,3 +1,9 @@ { - "app_name": "Sudoku" + "app_name": "Sudoku", + + "bottom_nav_game": "Game", + "bottom_nav_settings": "Settings", + + "settings_title": "Settings", + "settings_label_theme": "Theme mode" } diff --git a/assets/translations/fr.json b/assets/translations/fr.json index a815023..80f4637 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -1,3 +1,9 @@ { - "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" } diff --git a/fastlane/metadata/android/en-US/changelogs/70.txt b/fastlane/metadata/android/en-US/changelogs/70.txt new file mode 100644 index 0000000..1a4f9ce --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/70.txt @@ -0,0 +1 @@ +Add "settings" page with dark/light theme selector. diff --git a/fastlane/metadata/android/fr-FR/changelogs/70.txt b/fastlane/metadata/android/fr-FR/changelogs/70.txt new file mode 100644 index 0000000..269e6de --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/70.txt @@ -0,0 +1 @@ +Ajout d'une page de réglages avec sélection du thème clair ou sombre. diff --git a/lib/config/menu.dart b/lib/config/menu.dart new file mode 100644 index 0000000..a21c528 --- /dev/null +++ b/lib/config/menu.dart @@ -0,0 +1,48 @@ +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; +} diff --git a/lib/cubit/nav_cubit.dart b/lib/cubit/nav_cubit.dart new file mode 100644 index 0000000..845ecc2 --- /dev/null +++ b/lib/cubit/nav_cubit.dart @@ -0,0 +1,37 @@ +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}; + } +} diff --git a/lib/cubit/theme_cubit.dart b/lib/cubit/theme_cubit.dart new file mode 100644 index 0000000..b793e89 --- /dev/null +++ b/lib/cubit/theme_cubit.dart @@ -0,0 +1,31 @@ +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()}; + } +} diff --git a/lib/cubit/theme_state.dart b/lib/cubit/theme_state.dart new file mode 100644 index 0000000..e479a50 --- /dev/null +++ b/lib/cubit/theme_state.dart @@ -0,0 +1,15 @@ +part of 'theme_cubit.dart'; + +@immutable +class ThemeModeState extends Equatable { + const ThemeModeState({ + this.themeMode, + }); + + final ThemeMode? themeMode; + + @override + List<Object?> get props => <Object?>[ + themeMode, + ]; +} diff --git a/lib/main.dart b/lib/main.dart index a5c7e89..a69d933 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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, + ); + }, ), ); } diff --git a/lib/ui/layout/board.dart b/lib/ui/layout/board.dart index 3d44197..188d51c 100644 --- a/lib/ui/layout/board.dart +++ b/lib/ui/layout/board.dart @@ -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), diff --git a/lib/ui/screens/game_page.dart b/lib/ui/screens/game_page.dart new file mode 100644 index 0000000..9a8173f --- /dev/null +++ b/lib/ui/screens/game_page.dart @@ -0,0 +1,26 @@ +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, + ); + }, + ), + ); + } +} diff --git a/lib/ui/screens/screen_game.dart b/lib/ui/screens/screen_game.dart deleted file mode 100644 index 0992122..0000000 --- a/lib/ui/screens/screen_game.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'package:sudoku/cubit/game_cubit.dart'; -import 'package:sudoku/models/game.dart'; -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}); - - @override - Widget build(BuildContext context) { - return BlocBuilder<GameCubit, GameState>( - 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(), - ], - ); - }, - ); - } -} diff --git a/lib/ui/screens/settings_page.dart b/lib/ui/screens/settings_page.dart new file mode 100644 index 0000000..aea7bd5 --- /dev/null +++ b/lib/ui/screens/settings_page.dart @@ -0,0 +1,26 @@ +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(), + ], + ), + ); + } +} diff --git a/lib/ui/skeleton.dart b/lib/ui/skeleton.dart index 2bfdbfe..0923b76 100644 --- a/lib/ui/skeleton.dart +++ b/lib/ui/skeleton.dart @@ -1,9 +1,8 @@ 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, ); diff --git a/lib/ui/widgets/cell_widget.dart b/lib/ui/widgets/cell_widget.dart index eb697a0..6bc0200 100644 --- a/lib/ui/widgets/cell_widget.dart +++ b/lib/ui/widgets/cell_widget.dart @@ -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; diff --git a/lib/ui/widgets/cell_widget_update.dart b/lib/ui/widgets/cell_widget_update.dart index a8bb005..37371a5 100644 --- a/lib/ui/widgets/cell_widget_update.dart +++ b/lib/ui/widgets/cell_widget_update.dart @@ -36,7 +36,7 @@ class CellWidgetUpdate extends StatelessWidget { decoration: BoxDecoration( color: backgroundColor, border: Border.all( - color: Colors.black, + color: Colors.grey.shade700, width: 2, ), ), diff --git a/lib/ui/widgets/game.dart b/lib/ui/widgets/game.dart new file mode 100644 index 0000000..b9160d1 --- /dev/null +++ b/lib/ui/widgets/game.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:sudoku/cubit/game_cubit.dart'; +import 'package:sudoku/models/game.dart'; +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 GameWidget extends StatelessWidget { + const GameWidget({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + final Game game = gameState.game; + + 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(), + ], + ), + ); + }, + ); + } +} diff --git a/lib/ui/widgets/global_app_bar.dart b/lib/ui/widgets/global_app_bar.dart index 5583405..22cccc1 100644 --- a/lib/ui/widgets/global_app_bar.dart +++ b/lib/ui/widgets/global_app_bar.dart @@ -1,10 +1,11 @@ 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, + ); + }, ); }, ); diff --git a/lib/ui/widgets/header_app.dart b/lib/ui/widgets/header_app.dart new file mode 100644 index 0000000..b5c5be0 --- /dev/null +++ b/lib/ui/widgets/header_app.dart @@ -0,0 +1,24 @@ +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), + ], + ); + } +} diff --git a/lib/ui/screens/screen_parameters.dart b/lib/ui/widgets/parameters.dart similarity index 96% rename from lib/ui/screens/screen_parameters.dart rename to lib/ui/widgets/parameters.dart index ff6e2ab..4018b05 100644 --- a/lib/ui/screens/screen_parameters.dart +++ b/lib/ui/widgets/parameters.dart @@ -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( diff --git a/lib/ui/widgets/settings/settings_form.dart b/lib/ui/widgets/settings/settings_form.dart new file mode 100644 index 0000000..53576c1 --- /dev/null +++ b/lib/ui/widgets/settings/settings_form.dart @@ -0,0 +1,63 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:unicons/unicons.dart'; + +import 'package:sudoku/ui/widgets/settings/theme_card.dart'; + +class SettingsForm extends StatefulWidget { + const SettingsForm({super.key}); + + @override + State<SettingsForm> createState() => _SettingsFormState(); +} + +class _SettingsFormState extends State<SettingsForm> { + @override + void dispose() { + super.dispose(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: <Widget>[ + // Light/dark theme + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: <Widget>[ + const Text('settings_label_theme').tr(), + const Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ThemeCard( + mode: ThemeMode.system, + icon: UniconsLine.cog, + ), + ThemeCard( + mode: ThemeMode.light, + icon: UniconsLine.sun, + ), + ThemeCard( + mode: ThemeMode.dark, + icon: UniconsLine.moon, + ) + ], + ), + ], + ), + + const SizedBox(height: 16), + ], + ); + } +} diff --git a/lib/ui/widgets/settings/theme_card.dart b/lib/ui/widgets/settings/theme_card.dart new file mode 100644 index 0000000..39b3d6d --- /dev/null +++ b/lib/ui/widgets/settings/theme_card.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:sudoku/cubit/theme_cubit.dart'; + +class ThemeCard extends StatelessWidget { + const ThemeCard({ + super.key, + required this.mode, + required this.icon, + }); + + final IconData icon; + final ThemeMode mode; + + @override + Widget build(BuildContext context) { + return BlocBuilder<ThemeCubit, ThemeModeState>( + builder: (BuildContext context, ThemeModeState state) { + return Card( + elevation: 2, + shadowColor: Theme.of(context).colorScheme.shadow, + color: state.themeMode == mode + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.surface, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + margin: const EdgeInsets.all(5), + child: InkWell( + onTap: () => BlocProvider.of<ThemeCubit>(context).getTheme( + ThemeModeState(themeMode: mode), + ), + borderRadius: const BorderRadius.all(Radius.circular(12)), + child: Icon( + icon, + size: 32, + color: state.themeMode != mode + ? Theme.of(context).colorScheme.primary + : Colors.white, + ), + ), + ); + }, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 4991f4d..b35bc75 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,14 +9,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.5.0" - async: - dependency: transitive - description: - name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" - url: "https://pub.dev" - source: hosted - version: "2.11.0" badges: dependency: "direct main" description: @@ -192,14 +184,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" - overlay_support: - dependency: "direct main" - description: - name: overlay_support - sha256: fc39389bfd94e6985e1e13b2a88a125fc4027608485d2d4e2847afe1b2bb339c - url: "https://pub.dev" - source: hosted - version: "2.1.0" path: dependency: transitive description: @@ -357,6 +341,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + unicons: + dependency: "direct main" + description: + name: unicons + sha256: dbfcf93ff4d4ea19b324113857e358e4882115ab85db04417a4ba1c72b17a670 + url: "https://pub.dev" + source: hosted + version: "2.1.1" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index fca46c7..d71e600 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,24 +1,25 @@ name: sudoku description: A sudoku game application. -publish_to: 'none' +publish_to: "none" -version: 0.1.20+69 +version: 0.1.21+70 environment: - sdk: '^3.0.0' + sdk: "^3.0.0" dependencies: flutter: sdk: flutter + badges: ^3.1.2 easy_localization: ^3.0.1 equatable: ^2.0.5 flutter_bloc: ^8.1.1 hive: ^2.2.3 hydrated_bloc: ^9.0.0 - overlay_support: ^2.1.0 path_provider: ^2.0.11 + unicons: ^2.1.1 dev_dependencies: flutter_lints: ^3.0.1 -- GitLab