diff --git a/lib/constants/AppConstraints.dart b/lib/constants/AppConstraints.dart new file mode 100644 index 00000000..7c32ae10 --- /dev/null +++ b/lib/constants/AppConstraints.dart @@ -0,0 +1,3 @@ +class AppConstraints { + static final int mobileWidth = 350; +} diff --git a/lib/constants/GameNotificationStatuses.dart b/lib/constants/GameNotificationStatuses.dart new file mode 100644 index 00000000..61e0f72a --- /dev/null +++ b/lib/constants/GameNotificationStatuses.dart @@ -0,0 +1 @@ +enum GameNotificationStatuses { warn, done, error } diff --git a/lib/constants/Locales.dart b/lib/constants/Locales.dart index 1291e9b7..9c3a2a68 100644 --- a/lib/constants/Locales.dart +++ b/lib/constants/Locales.dart @@ -1,14 +1,14 @@ import 'package:flutter/cupertino.dart'; class Locales { - static Locale get en => Locale(Languages.en, 'EN'); - static Locale get ru => Locale(Languages.ru, 'RU'); + static const Locale en = Locale(Languages.en, 'EN'); + static const Locale ru = Locale(Languages.ru, 'RU'); } class Languages { - static String ru = 'ru'; - static String en = 'en'; - static final List items = [ + static const String ru = 'ru'; + static const String en = 'en'; + static const List items = [ ru, en, ]; diff --git a/lib/entities/GameNotification.dart b/lib/entities/GameNotification.dart index b0937272..85ed4370 100644 --- a/lib/entities/GameNotification.dart +++ b/lib/entities/GameNotification.dart @@ -1,8 +1,11 @@ import 'package:flutter/foundation.dart'; +import 'package:word_by_word_game/constants/GameNotificationStatuses.dart'; import 'package:word_by_word_game/entities/LocalName.dart'; class GameNotification { - final bool status; + final GameNotificationStatuses status; final LocalName localName; - GameNotification({@required this.status, @required this.localName}); + final String newWord; + GameNotification( + {@required this.status, @required this.localName, this.newWord}); } diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 4615e579..ee032d4c 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -104,5 +104,10 @@ "@players": { "type": "text", "placeholders": {} + }, + "addToDictionary": "Add to dictionary", + "@addToDictionary": { + "type": "text", + "placeholders": {} } } diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 4cb84700..44d9578d 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2020-11-03T04:20:13.003304", + "@@last_modified": "2020-11-04T01:07:37.983370", "finishGame": "Finish game", "@finishGame": { "type": "text", @@ -104,5 +104,10 @@ "@players": { "type": "text", "placeholders": {} + }, + "addToDictionary": "Add to dictionary", + "@addToDictionary": { + "type": "text", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index b6c1f8a4..cbdc03bd 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -104,5 +104,10 @@ "@players": { "type": "text", "placeholders": {} + }, + "addToDictionary": "Добавить в словарь", + "@addToDictionary": { + "type": "text", + "placeholders": {} } } diff --git a/lib/l10n/messages_en.dart b/lib/l10n/messages_en.dart index cc5a93ff..8b3095a3 100644 --- a/lib/l10n/messages_en.dart +++ b/lib/l10n/messages_en.dart @@ -22,6 +22,7 @@ class MessageLookup extends MessageLookupByLibrary { final messages = _notInlinedMessages(_notInlinedMessages); static _notInlinedMessages(_) => { "addNewWord" : MessageLookupByLibrary.simpleMessage("add"), + "addToDictionary" : MessageLookupByLibrary.simpleMessage("Add to dictionary"), "congratulations" : MessageLookupByLibrary.simpleMessage("Congratulations!"), "continueGame" : MessageLookupByLibrary.simpleMessage("Continue "), "copyright" : MessageLookupByLibrary.simpleMessage("2020 © X Soul Space "), diff --git a/lib/l10n/messages_messages.dart b/lib/l10n/messages_messages.dart index 1ea33d1b..fd7ab0d1 100644 --- a/lib/l10n/messages_messages.dart +++ b/lib/l10n/messages_messages.dart @@ -22,6 +22,7 @@ class MessageLookup extends MessageLookupByLibrary { final messages = _notInlinedMessages(_notInlinedMessages); static _notInlinedMessages(_) => { "addNewWord" : MessageLookupByLibrary.simpleMessage("add"), + "addToDictionary" : MessageLookupByLibrary.simpleMessage("Add to dictionary"), "congratulations" : MessageLookupByLibrary.simpleMessage("Congratulations!"), "continueGame" : MessageLookupByLibrary.simpleMessage("Continue "), "copyright" : MessageLookupByLibrary.simpleMessage("2020 © X Soul Space "), diff --git a/lib/l10n/messages_ru.dart b/lib/l10n/messages_ru.dart index fcb71740..9ad36aaa 100644 --- a/lib/l10n/messages_ru.dart +++ b/lib/l10n/messages_ru.dart @@ -22,6 +22,7 @@ class MessageLookup extends MessageLookupByLibrary { final messages = _notInlinedMessages(_notInlinedMessages); static _notInlinedMessages(_) => { "addNewWord" : MessageLookupByLibrary.simpleMessage("добавить"), + "addToDictionary" : MessageLookupByLibrary.simpleMessage("Добавить в словарь"), "congratulations" : MessageLookupByLibrary.simpleMessage("Поздравляем!"), "continueGame" : MessageLookupByLibrary.simpleMessage("Продолжить "), "copyright" : MessageLookupByLibrary.simpleMessage("2020 © X Soul Space "), diff --git a/lib/localizations/MainLocalizations.dart b/lib/localizations/MainLocalizations.dart index eec0a7e8..1f74bbe5 100644 --- a/lib/localizations/MainLocalizations.dart +++ b/lib/localizations/MainLocalizations.dart @@ -112,6 +112,10 @@ class MainLocalizations { String get players { return Intl.message('Players: ', name: 'players'); } + + String get addToDictionary { + return Intl.message('Add to dictionary', name: 'addToDictionary'); + } } class MainLocalizationsDelegate diff --git a/lib/main.dart b/lib/main.dart index 0172fc5b..89c3077d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:provider/provider.dart'; import 'package:word_by_word_game/constants/Locales.dart'; import 'package:word_by_word_game/entities/FirstPlayer.dart'; import 'package:word_by_word_game/entities/Player.dart'; +import 'package:word_by_word_game/models/LocalDictionaryModel.dart'; import 'package:word_by_word_game/models/LocaleModel.dart'; import 'package:word_by_word_game/models/NotificationsModel.dart'; import 'package:word_by_word_game/models/PlayerColorsModel.dart'; @@ -44,6 +45,8 @@ class MyApp extends StatelessWidget { if (snapshot.connectionState == ConnectionState.done) { return MultiProvider(providers: [ ChangeNotifierProvider(create: (context) => snapshot.data), + ChangeNotifierProvider( + create: (context) => LocalDictionaryModel()), ChangeNotifierProvider( create: (context) => PlayersModel( {firstPlayer.id: firstPlayer}, @@ -52,7 +55,12 @@ class MyApp extends StatelessWidget { .map((color) => Player(id: color.id, playerColor: color)) .toList())), - ChangeNotifierProvider(create: (context) => WordsModel({}, {})), + ChangeNotifierProvider(create: (context) { + var localDictionaryModel = + Provider.of(context, listen: false); + return WordsModel({}, {}, + localDictionaryModel: localDictionaryModel); + }), ChangeNotifierProvider(create: (context) => PlayerColorsModel()), ChangeNotifierProvider(create: (context) => ScoreModel()), ChangeNotifierProvider(create: (context) => NotificationsModel()), @@ -61,8 +69,12 @@ class MyApp extends StatelessWidget { Provider.of(context, listen: false); var playersModel = Provider.of(context, listen: false); + var localDictionaryModel = + Provider.of(context, listen: false); return await StorageModel.create( - wordsModel: wordsModel, playersModel: playersModel); + wordsModel: wordsModel, + playersModel: playersModel, + localDictionaryModel: localDictionaryModel); }), ], child: ScaffoldApp()); } else { diff --git a/lib/models/LocalDictionaryModel.dart b/lib/models/LocalDictionaryModel.dart new file mode 100644 index 00000000..27c442cd --- /dev/null +++ b/lib/models/LocalDictionaryModel.dart @@ -0,0 +1,30 @@ +import 'package:flutter/foundation.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'LocalDictionaryModel.g.dart'; + +class LocalDictionaryModelConsts { + static String storagename = 'localdictionary'; +} + +@JsonSerializable(nullable: true) +class LocalDictionaryModel extends ChangeNotifier { + Set _words; + LocalDictionaryModel({Set words}) { + this._words = words ?? {}; + } + Set get words => _words; + void addWord(String value) { + _words.add(value); + notifyListeners(); + } + + void reloadState({@required Set words}) { + _words.clear(); + _words.addAll(words); + } + + factory LocalDictionaryModel.fromJson(Map json) => + _$LocalDictionaryModelFromJson(json); + Map toJson() => _$LocalDictionaryModelToJson(this); +} diff --git a/lib/models/LocalDictionaryModel.g.dart b/lib/models/LocalDictionaryModel.g.dart new file mode 100644 index 00000000..3916468b --- /dev/null +++ b/lib/models/LocalDictionaryModel.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'LocalDictionaryModel.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +LocalDictionaryModel _$LocalDictionaryModelFromJson(Map json) { + return LocalDictionaryModel( + words: (json['words'] as List)?.map((e) => e as String)?.toSet(), + ); +} + +Map _$LocalDictionaryModelToJson( + LocalDictionaryModel instance) => + { + 'words': instance.words?.toList(), + }; diff --git a/lib/models/LocaleModel.dart b/lib/models/LocaleModel.dart index 6939af68..ccae921a 100644 --- a/lib/models/LocaleModel.dart +++ b/lib/models/LocaleModel.dart @@ -45,10 +45,18 @@ class LocaleModel extends ChangeNotifier with StorageMixin { static Future loadSavedLocale() async { StorageUtil storage = await StorageUtil.getInstance(); String localeStr = storage.getString(LocaleModelConsts.storagename); - if (localeStr == null || localeStr == '') { - if (kIsWeb || Platform.isWindows) return Locales.en; + if (localeStr.isEmpty) { + // FIXME: strange things happend with locales on all OS! + // seems like it has new formats nn__UTF08__NN + if (kIsWeb || + Platform.isWindows || + Platform.isLinux || + Platform.isAndroid || + Platform.isIOS || + Platform.isMacOS) return Locales.en; - Intl.defaultLocale = await findSystemLocale(); + var systemLocale = await findSystemLocale(); + Intl.defaultLocale = systemLocale; return Locale(Intl.defaultLocale); } @@ -67,7 +75,7 @@ class LocaleModel extends ChangeNotifier with StorageMixin { NamedLocale get currentNamedLocale => LocaleModelConsts.namedLocales.firstWhere((namedLocale) { - var isEqual = _locale == namedLocale.locale; + var isEqual = _locale.languageCode == namedLocale.locale.languageCode; return isEqual; }); } diff --git a/lib/models/StorageModel.dart b/lib/models/StorageModel.dart index 6eb20583..29b58f58 100644 --- a/lib/models/StorageModel.dart +++ b/lib/models/StorageModel.dart @@ -1,34 +1,33 @@ import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:word_by_word_game/models/LocalDictionaryModel.dart'; import 'package:word_by_word_game/models/PlayersModel.dart'; import 'package:word_by_word_game/models/StorageMixin.dart'; import 'package:word_by_word_game/models/WordsModel.dart'; class StorageModel extends ChangeNotifier with StorageMixin { WordsModel _wordsModel; - set wordsModel(WordsModel wordsModel) { - if (wordsModel != null) _wordsModel = wordsModel; - } - PlayersModel _playersModel; - set playersModel(PlayersModel playersModel) { - if (playersModel != null) _playersModel = playersModel; - } + LocalDictionaryModel _localDictionaryModel; // Private constructor - StorageModel._create(this._wordsModel, this._playersModel); + StorageModel._create( + this._wordsModel, this._playersModel, this._localDictionaryModel); /// Public factory static Future create( {@required WordsModel wordsModel, - @required PlayersModel playersModel}) async { + @required PlayersModel playersModel, + @required LocalDictionaryModel localDictionaryModel}) async { // Call the private constructor - var storageModel = StorageModel._create(wordsModel, playersModel); + var storageModel = + StorageModel._create(wordsModel, playersModel, localDictionaryModel); await storageModel.checkAndLoadStorageInstance(); await storageModel.loadPlayersModel(); await storageModel.loadWordsModel(); + await storageModel.loadLocalDictionary(); // Return the fully initialized object return storageModel; } @@ -84,4 +83,20 @@ class StorageModel extends ChangeNotifier with StorageMixin { ..notifyListeners(); notifyListeners(); } + + Future loadLocalDictionary() async { + var modelStr = await load(LocalDictionaryModelConsts.storagename); + if (modelStr == null) return; + var model = LocalDictionaryModel.fromJson(modelStr); + _localDictionaryModel + ..reloadState(words: model.words) + ..notifyListeners(); + notifyListeners(); + } + + Future saveLocalDictionary() async { + await save( + key: LocalDictionaryModelConsts.storagename, + value: _localDictionaryModel.toJson()); + } } diff --git a/lib/models/WordsModel.dart b/lib/models/WordsModel.dart index d6306b7a..b1cb4960 100644 --- a/lib/models/WordsModel.dart +++ b/lib/models/WordsModel.dart @@ -1,11 +1,16 @@ import 'dart:collection'; +import 'package:english_words/english_words.dart' as EnglishWords; import 'package:flutter/widgets.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:russian_words/russian_words.dart' as RussianWords; +import 'package:word_by_word_game/constants/GameNotificationStatuses.dart'; +import 'package:word_by_word_game/constants/Locales.dart'; import 'package:word_by_word_game/entities/GameNotification.dart'; import 'package:word_by_word_game/entities/LocalName.dart'; import 'package:word_by_word_game/entities/Player.dart'; import 'package:word_by_word_game/entities/Word.dart'; +import 'package:word_by_word_game/models/LocalDictionaryModel.dart'; part 'WordsModel.g.dart'; @@ -15,6 +20,9 @@ class WordsModelConsts { @JsonSerializable(nullable: true) class WordsModel extends ChangeNotifier { + @JsonKey(ignore: true) + LocalDictionaryModel _localDictionaryModel; + /// /// Words stack /// @@ -49,42 +57,69 @@ class WordsModel extends ChangeNotifier { } Future addNewWordForPLayer( - {@required Player player}) async { + {@required Player player, @required Locale locale}) async { + /// checking limit var newWord = '$newWordBeginning$phraseFromLastword$newWordEnding' .toLowerCase() .replaceAll(' ', ''); - if (newWord.length < phraseLimit) + if (newWord.length <= phraseLimit) return GameNotification( - status: false, + status: GameNotificationStatuses.error, localName: LocalName( en: 'Word needs to have more then $phraseLimit', ru: 'Слово должно быть больше чем $phraseLimit')); - var isNewWordExists = allWordsValues.contains(newWord); + /// checking is Word was added + var isNewWordExists = allWordsValues.contains(newWord); if (isNewWordExists) return GameNotification( - status: false, + status: GameNotificationStatuses.error, localName: LocalName( en: 'This word was written aleady!', ru: 'Это слово уже записано!')); wordsIdMax++; - _allWordsByWordIdMap.putIfAbsent( - wordsIdMax, () => Word(id: wordsIdMax, value: newWord)); + /// checking with external dictionaries + var isExistsInDictionary = (() { + if (locale == Locales.en) { + return EnglishWords.nouns.contains(newWord); + } else if (locale == Locales.ru) { + return RussianWords.nouns.contains(newWord); + } + return true; + })(); + + /// checking with internal dictionaries + if (!isExistsInDictionary) { + var isExistsInLocalDictionary = + _localDictionaryModel.words.contains(newWord); + if (!isExistsInLocalDictionary) { + return GameNotification( + localName: LocalName( + en: 'Whoa, cannot find this word in dictionary! Try another, or add word to dictionary.', + ru: 'Ого, этого слова нет в словаре! Попробуйте другое или добавьте его в словарь.'), + status: GameNotificationStatuses.warn, + newWord: newWord); + } + } + /// checking word id identity var playerWordsIds = getWordsIdsListByPlayer(player: player); var isNewIdExists = playerWordsIds.contains(wordsIdMax); if (isNewIdExists) return GameNotification( - status: false, + status: GameNotificationStatuses.error, localName: LocalName( en: 'This word was written aleady!', ru: 'Это слово уже записано!')); + _allWordsByWordIdMap.putIfAbsent( + wordsIdMax, () => Word(id: wordsIdMax, value: newWord)); playerWordsIds.add(wordsIdMax); _wordsIdsByPlayerIdMap.putIfAbsent(player.id, () => playerWordsIds); + privateLastword = newWord; phraseLimit = phraseLimitMax; @@ -94,7 +129,7 @@ class WordsModel extends ChangeNotifier { newWordEnding = ''; notifyListeners(); return GameNotification( - status: true, + status: GameNotificationStatuses.done, localName: LocalName(en: 'Word added!', ru: 'Слово добавлено!')); } @@ -195,6 +230,7 @@ class WordsModel extends ChangeNotifier { resetToNewGame() { newWordBeginning = ''; newWordEnding = ''; + phraseFromLastword = ''; phraseLimit = 3; phraseLimitMax = 3; phraseLimitLettersLeft = 6; @@ -223,6 +259,7 @@ class WordsModel extends ChangeNotifier { Map> wordsIdsByPlayerIdMap, { String newWordBeginning = '', String newWordEnding = '', + LocalDictionaryModel localDictionaryModel, this.phraseLimit = 3, this.phraseLimitMax = 3, this.phraseLimitLettersLeft = 6, @@ -232,6 +269,8 @@ class WordsModel extends ChangeNotifier { this._wordsIdsByPlayerIdMap = wordsIdsByPlayerIdMap; this._newWordBeginning = newWordBeginning; this._newWordEnding = newWordEnding; + this._localDictionaryModel = localDictionaryModel; + notifyListeners(); } diff --git a/lib/screens/InputScreen.dart b/lib/screens/InputScreen.dart index 043140e3..a265b80c 100644 --- a/lib/screens/InputScreen.dart +++ b/lib/screens/InputScreen.dart @@ -34,40 +34,37 @@ class InputScreen extends StatelessWidget { var playersModel = Provider.of(context); return Scaffold( - body: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return Container( - height: constraints.maxHeight, - decoration: appGradientBoxDecoration( - playerColor: playersModel.currentPlayer.playerColor, - secondColor: Theme.of(context).primaryColor), - child: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: Column(children: [ - UpperToolbar(), - NotificationsWidget( - gameNotification: notificationsModel.gameNotification, + body: Container( + height: double.infinity, + decoration: appGradientBoxDecoration( + playerColor: playersModel.currentPlayer.playerColor, + secondColor: Theme.of(context).primaryColor), + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Column(children: [ + UpperToolbar(), + NotificationsWidget( + gameNotification: notificationsModel.gameNotification, + ), + Container( + color: Colors.white.withOpacity(0.85), + child: Padding( + padding: EdgeInsets.symmetric(vertical: 30, horizontal: 5), + child: InputWidget(), ), - Container( - color: Colors.white.withOpacity(0.85), - child: Padding( - padding: EdgeInsets.symmetric(vertical: 30, horizontal: 5), - child: InputWidget(), - ), + ), + Container( + color: Colors.white.withOpacity(0.85), + child: Visibility( + visible: wordsModel.isAtLeastOneWordRecorded, + maintainState: true, + maintainAnimation: true, + child: ExtraMenu(), ), - Container( - color: Colors.white.withOpacity(0.85), - child: Visibility( - visible: wordsModel.isAtLeastOneWordRecorded, - maintainState: true, - maintainAnimation: true, - child: ExtraMenu(), - ), - ), - ]), - ), - ); - }), + ), + ]), + ), + ), floatingActionButton: MenuFAB(), ); } diff --git a/lib/widgets/ExtraMenu.dart b/lib/widgets/ExtraMenu.dart index 03194812..a245e58d 100644 --- a/lib/widgets/ExtraMenu.dart +++ b/lib/widgets/ExtraMenu.dart @@ -38,9 +38,7 @@ class ExtraMenu extends StatelessWidget { ), leading: Icon(Icons.add_circle_outline), title: Text(MainLocalizations.of(context).finishGame), - onTap: () { - _showFinishGameDialog(context); - }), + onTap: () => _showFinishGameDialog(context)), ], ), ); diff --git a/lib/widgets/InputWidget.dart b/lib/widgets/InputWidget.dart index 0aa1a79a..08f89076 100644 --- a/lib/widgets/InputWidget.dart +++ b/lib/widgets/InputWidget.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; +import 'package:word_by_word_game/constants/AppConstraints.dart'; +import 'package:word_by_word_game/constants/GameNotificationStatuses.dart'; import 'package:word_by_word_game/localizations/MainLocalizations.dart'; +import 'package:word_by_word_game/models/LocaleModel.dart'; import 'package:word_by_word_game/models/NotificationsModel.dart'; import 'package:word_by_word_game/models/PlayersModel.dart'; import 'package:word_by_word_game/models/StorageModel.dart'; @@ -50,7 +53,7 @@ class _InputWidgetState extends State { playersModel.currentPlayer.playerColor.color.withOpacity(0.7))); } - _decreaseButton({@required bool isFromBeginning}) { + Widget _decreaseButton({@required bool isFromBeginning}) { var storageModel = Provider.of(context, listen: false); var wordsModel = Provider.of(context, listen: false); var playersModel = Provider.of(context, listen: false); @@ -72,7 +75,7 @@ class _InputWidgetState extends State { ); } - _updateWordsModelPhrases( + Future _updateWordsModelPhrases( {@required String value, @required bool isFromBeginning}) async { var wordsModel = Provider.of(context, listen: false); if (isFromBeginning) { @@ -86,6 +89,33 @@ class _InputWidgetState extends State { bool _isLeftControllerInitialized = false; bool _isRightControllerInitialized = false; + + Widget _playerWidget() { + var playersModel = Provider.of(context); + return Container( + height: 30.0, + width: 30.0, + child: FittedBox( + child: PlayerWidget( + player: playersModel.currentPlayer, + isDisabled: true, + fontSize: 24, + ))); + } + + Widget _lastwordWidget() { + return Flexible( + child: Consumer( + builder: (BuildContext buildContext, wordsModel, Widget widget) => + Text( + wordsModel.isAtLeastOneWordRecorded + ? '${MainLocalizations.of(context).lastword} ${wordsModel.lastword}' + : '', + textAlign: TextAlign.center, + )), + ); + } + @override Widget build(BuildContext context) { var wordsModel = Provider.of(context); @@ -99,94 +129,72 @@ class _InputWidgetState extends State { _rightTextController.text = wordsModel.newWordEnding; _isRightControllerInitialized = true; } - return Material( color: Colors.transparent, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.center, children: [ - Row( - children: [ - Text(MainLocalizations.of(context).player), - SizedBox(width: 10), - Container( - height: 30.0, - width: 30.0, - child: FittedBox( - child: PlayerWidget( - player: playersModel.currentPlayer, - isDisabled: true, - fontSize: 24, - ))), - SizedBox(width: 30), - Consumer( - builder: (BuildContext buildContext, wordsModel, - Widget widget) => - Text(wordsModel.isAtLeastOneWordRecorded - ? '${MainLocalizations.of(context).lastword} ${wordsModel.lastword}' - : '')), - ], - ), - Padding( - padding: EdgeInsets.symmetric(vertical: 10), - ), - Row( - children: [ - // first input - Visibility( - visible: wordsModel.isAtLeastOneWordRecorded && - wordsModel.isPhraseFromLastwordNotEmpty, - child: Expanded( - child: TextField( - style: TextStyle(fontSize: 14.0), - textAlign: TextAlign.end, - decoration: InputDecoration( - focusedBorder: _textFieldFocusOutlineInputBorder(), - border: _textFieldOutlineInputBorder(), - hintText: - MainLocalizations.of(context).hintAddBeginning), - onChanged: (String value) => _updateWordsModelPhrases( - isFromBeginning: true, value: value), - controller: _leftTextController, - ), - ), - ), - // // letters - Visibility( - visible: wordsModel.isAtLeastOneWordRecorded && - wordsModel.isPhraseFromLastwordNotEmpty, - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 5), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, + LayoutBuilder(builder: (context, constraints) { + if (constraints.maxWidth > AppConstraints.mobileWidth) { + return Row( + children: [ + Text(MainLocalizations.of(context).player), + SizedBox(width: 10), + _playerWidget(), + SizedBox(width: 30), + _lastwordWidget() + ], + ); + } else { + return Container( + height: 80, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - _decreaseButton(isFromBeginning: true), - Consumer( - builder: (BuildContext buildContext, wordsModel, - Widget widget) => - Text(wordsModel.phraseFromLastword)), - _decreaseButton(isFromBeginning: false) + Text(MainLocalizations.of(context).player), + SizedBox(width: 10), + _playerWidget(), ], ), - )), - // second input - Expanded( - child: TextField( - style: TextStyle(fontSize: 14.0), - decoration: InputDecoration( - focusedBorder: _textFieldFocusOutlineInputBorder(), - border: _textFieldOutlineInputBorder(), - hintText: wordsModel.isNoWordsRecordedYet - ? MainLocalizations.of(context).hintAddNewWord - : MainLocalizations.of(context).hintAddEnding), - onChanged: (String value) => _updateWordsModelPhrases( - isFromBeginning: false, value: value), - controller: _rightTextController, + SizedBox(height: 10), + _lastwordWidget() + ], ), - ) - ], + ); + } + }), + Padding( + padding: EdgeInsets.symmetric(vertical: 10), ), + LayoutBuilder(builder: (context, constraints) { + if (constraints.maxWidth > AppConstraints.mobileWidth) { + return Row( + children: [ + // first input + _beginnigTextField(), + // // letters + _decreaseAndPhraseWidget(), + // second input + _endingTextField() + ], + ); + } else { + return Container( + height: 150, + child: Column(children: [ + // first input + _beginnigTextField(), + // // letters + _decreaseAndPhraseWidget(), + // second input + _endingTextField() + ]), + ); + } + }), Padding( padding: EdgeInsets.only(top: 30), child: Center( @@ -210,15 +218,76 @@ class _InputWidgetState extends State { ])); } - _addNewWord() async { + Widget _decreaseAndPhraseWidget() { + var wordsModel = Provider.of(context); + return Visibility( + visible: wordsModel.isAtLeastOneWordRecorded && + wordsModel.isPhraseFromLastwordNotEmpty, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 5), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _decreaseButton(isFromBeginning: true), + Consumer( + builder: (BuildContext buildContext, wordsModel, + Widget widget) => + Text(wordsModel.phraseFromLastword)), + _decreaseButton(isFromBeginning: false) + ])), + ); + } + + Widget _beginnigTextField() { + var wordsModel = Provider.of(context); + return Visibility( + visible: wordsModel.isAtLeastOneWordRecorded && + wordsModel.isPhraseFromLastwordNotEmpty, + child: Expanded( + child: TextField( + style: TextStyle(fontSize: 14.0), + textAlign: TextAlign.end, + decoration: InputDecoration( + focusedBorder: _textFieldFocusOutlineInputBorder(), + border: _textFieldOutlineInputBorder(), + hintText: MainLocalizations.of(context).hintAddBeginning), + onChanged: (String value) => + _updateWordsModelPhrases(isFromBeginning: true, value: value), + controller: _leftTextController, + ), + ), + ); + } + + Widget _endingTextField() { + var wordsModel = Provider.of(context); + return Expanded( + child: TextField( + style: TextStyle(fontSize: 14.0), + decoration: InputDecoration( + focusedBorder: _textFieldFocusOutlineInputBorder(), + border: _textFieldOutlineInputBorder(), + hintText: wordsModel.isNoWordsRecordedYet + ? MainLocalizations.of(context).hintAddNewWord + : MainLocalizations.of(context).hintAddEnding), + onChanged: (String value) => + _updateWordsModelPhrases(isFromBeginning: false, value: value), + controller: _rightTextController, + ), + ); + } + + Future _addNewWord() async { var notificationsModel = Provider.of(context, listen: false); var wordsModel = Provider.of(context, listen: false); var playersModel = Provider.of(context, listen: false); + var localeModel = Provider.of(context, listen: false); var storageModel = Provider.of(context, listen: false); var gameNotification = await wordsModel.addNewWordForPLayer( - player: playersModel.currentPlayer); - if (gameNotification.status) { + player: playersModel.currentPlayer, locale: localeModel.locale); + if (gameNotification.status == GameNotificationStatuses.done) { await storageModel.saveWordsModel(); _leftTextController.text = wordsModel.newWordBeginning; _rightTextController.text = wordsModel.newWordEnding; diff --git a/lib/widgets/MenuWidget.dart b/lib/widgets/MenuWidget.dart index 3ea3cfa0..5ae37b91 100644 --- a/lib/widgets/MenuWidget.dart +++ b/lib/widgets/MenuWidget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; +import 'package:word_by_word_game/constants/AppConstraints.dart'; import 'package:word_by_word_game/entities/NamedLocale.dart'; import 'package:word_by_word_game/localizations/MainLocalizations.dart'; import 'package:word_by_word_game/models/LocaleModel.dart'; @@ -8,63 +9,108 @@ import 'package:word_by_word_game/widgets/EndGameDialog.dart'; import 'package:word_by_word_game/widgets/PlayerChooser.dart'; class MenuWidget extends StatelessWidget { - @override - Widget build(BuildContext context) { - Size size = MediaQuery.of(context).size; + Widget _newGameButton(BuildContext context) { + return FlatButton( + onPressed: () => showEndGameDialog(context), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Icon(Icons.add_circle_outline), + SizedBox( + width: 10, + ), + Text(MainLocalizations.of(context).newGame), + ], + ), + height: 50, + minWidth: 200, + ); + } + + Widget _languageToogle() { + return Consumer(builder: (context, localeModel, child) { + return DropdownButton( + value: localeModel.currentNamedLocale, + items: LocaleModelConsts.namedLocales + .map>((namedLocale) { + return DropdownMenuItem( + key: Key(namedLocale.name), + value: namedLocale, + child: Text( + namedLocale.name, + ), + ); + }).toList(), + onChanged: (NamedLocale namedLocale) async { + await localeModel.switchLang(namedLocale.locale); + }); + }); + } + + Widget _mainWidget({@required BuildContext context, @required bool extend}) { return Container( - color: Colors.white, - padding: EdgeInsets.all(24), + decoration: BoxDecoration( + borderRadius: BorderRadius.vertical( + top: extend ? Radius.zero : Radius.circular(24), + bottom: Radius.zero), + color: Colors.white, + ), + margin: EdgeInsets.symmetric(horizontal: extend ? 0 : 5), + padding: EdgeInsets.only( + top: extend ? 10 : 20, + bottom: 20, + right: extend ? 4 : 20, + left: extend ? 4 : 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - FlatButton( - onPressed: () => showEndGameDialog(context), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, + LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth > AppConstraints.mobileWidth) { + return Row( children: [ - Icon(Icons.add_circle_outline), + _newGameButton(context), SizedBox( - width: 10, + width: 35, ), - Text(MainLocalizations.of(context).newGame), + _languageToogle() ], - ), - height: 50, - minWidth: 200, - ), - SizedBox( - width: 35, - ), - Consumer(builder: (context, localeModel, child) { - return DropdownButton( - value: localeModel.currentNamedLocale, - items: LocaleModelConsts.namedLocales - .map>((namedLocale) { - return DropdownMenuItem( - key: Key(namedLocale.name), - value: namedLocale, - child: Text( - namedLocale.name, + ); + } else { + return Container( + height: 100, + child: Column( + children: [ + _newGameButton(context), + SizedBox( + height: 10, ), - ); - }).toList(), - onChanged: (NamedLocale namedLocale) async { - await localeModel.switchLang(namedLocale.locale); - }); - }) - ], + _languageToogle() + ], + )); + } + }, ), PlayerChooser(), - Spacer(), + SizedBox( + height: 20, + ), + Divider( + color: Colors.black, + indent: 5, + endIndent: 5, + thickness: 1.0, + ), + SizedBox( + height: 20, + ), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(MainLocalizations.of(context).sendFeedback), - SizedBox(height: 0.01 * size.height), + SizedBox(height: 10), Text(MainLocalizations.of(context).thankYou), - SizedBox(height: 0.01 * size.height), + SizedBox(height: 10), Text(MainLocalizations.of(context).copyright), ], ), @@ -73,6 +119,22 @@ class MenuWidget extends StatelessWidget { ); } + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + if (constraints.maxHeight > 350) { + return _mainWidget(context: context, extend: false); + } else { + return SingleChildScrollView( + scrollDirection: Axis.vertical, + child: _mainWidget( + extend: true, + context: context, + )); + } + }); + } + showEndGameDialog(BuildContext context) { bool isCanPop = Navigator.canPop(context); if (isCanPop) Navigator.pop(context); diff --git a/lib/widgets/NotificationsWidget.dart b/lib/widgets/NotificationsWidget.dart index 7c8d75d5..18e102e8 100644 --- a/lib/widgets/NotificationsWidget.dart +++ b/lib/widgets/NotificationsWidget.dart @@ -1,9 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; +import 'package:word_by_word_game/constants/GameNotificationStatuses.dart'; import 'package:word_by_word_game/entities/GameNotification.dart'; +import 'package:word_by_word_game/localizations/MainLocalizations.dart'; +import 'package:word_by_word_game/models/LocalDictionaryModel.dart'; import 'package:word_by_word_game/models/LocaleModel.dart'; import 'package:word_by_word_game/models/NotificationsModel.dart'; +import 'package:word_by_word_game/models/StorageModel.dart'; class NotificationsWidget extends StatefulWidget { final GameNotification gameNotification; @@ -66,6 +70,34 @@ class _NotificationsWidgetState extends State notificationsModel.gameNotification = null; } + Color _getColor(GameNotificationStatuses status) { + switch (status) { + case GameNotificationStatuses.done: + return Colors.greenAccent[100]; + case GameNotificationStatuses.error: + return Colors.deepOrange[100]; + case GameNotificationStatuses.warn: + return Colors.amber[100]; + } + return Colors.transparent; + } + + _clearNotification() { + var notificationsModel = + Provider.of(context, listen: false); + _animationController + .reverse() + .then((value) => notificationsModel.gameNotification = null); + } + + _addNewWordToDictionary({@required String newWord}) async { + var localDictionaryModel = + Provider.of(context, listen: false); + localDictionaryModel.addWord(newWord); + var storageModel = Provider.of(context, listen: false); + await storageModel.saveLocalDictionary(); + } + @override Widget build(BuildContext context) { var notificationsModel = Provider.of(context); @@ -83,24 +115,36 @@ class _NotificationsWidgetState extends State child: Container( padding: EdgeInsets.all(10), color: notificationsModel.gameNotification != null - ? notificationsModel.gameNotification.status - ? Colors.greenAccent[100] - : Colors.deepOrange[100] + ? _getColor(notificationsModel.gameNotification.status) : Colors.transparent, child: notificationsModel.gameNotification != null ? Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Consumer( - builder: (context, localeModel, widget) => Text( - notificationsModel.gameNotification.localName - .getName(localeModel.locale))), + Flexible( + child: Consumer( + builder: (context, localeModel, widget) => Text( + notificationsModel.gameNotification.localName + .getName(localeModel.locale))), + ), + Visibility( + visible: + notificationsModel.gameNotification != null && + notificationsModel.gameNotification.newWord != + null, + child: FlatButton( + child: Text(MainLocalizations.of(context) + .addToDictionary), + onPressed: () { + _addNewWordToDictionary( + newWord: notificationsModel + .gameNotification.newWord); + _clearNotification(); + }), + ), IconButton( icon: Icon(Icons.close), - onPressed: () { - _animationController.reverse().then((value) => - notificationsModel.gameNotification = null); - }) + onPressed: () => _clearNotification()) ]) : null, ), diff --git a/lib/widgets/PlayerChooser.dart b/lib/widgets/PlayerChooser.dart index 6f152656..dec4cb45 100644 --- a/lib/widgets/PlayerChooser.dart +++ b/lib/widgets/PlayerChooser.dart @@ -13,22 +13,27 @@ class PlayerChooser extends StatelessWidget { var playersModel = Provider.of(context); return Padding( padding: EdgeInsets.only(top: 10), - child: Column( - children: [ - Align( - alignment: Alignment.centerLeft, - child: Text(MainLocalizations.of(context).players), - ), - SizedBox( - height: 10, - ), - Row( - children: [ - ...playersModel.tempPlayers.map((player) => - _playerController(context: context, player: player)) - ], - ), - ], + child: Container( + height: 50, + child: ListView( + shrinkWrap: true, + scrollDirection: Axis.horizontal, + children: [ + Align( + alignment: Alignment.centerLeft, + child: Text(MainLocalizations.of(context).players), + ), + SizedBox( + height: 10, + ), + Row( + children: [ + ...playersModel.tempPlayers.map((player) => + _playerController(context: context, player: player)) + ], + ), + ], + ), ), ); } diff --git a/lib/widgets/PlayerWidget.dart b/lib/widgets/PlayerWidget.dart index 06a8b016..487f04a7 100644 --- a/lib/widgets/PlayerWidget.dart +++ b/lib/widgets/PlayerWidget.dart @@ -18,6 +18,7 @@ class PlayerWidget extends StatelessWidget { Widget build(BuildContext context) { return FloatingActionButton( elevation: 1, + hoverElevation: 6, backgroundColor: isEnabled ? player.playerColor.color : Colors.grey, onPressed: isDisabled ? null : onTap, child: Text( @@ -27,16 +28,5 @@ class PlayerWidget extends StatelessWidget { color: Theme.of(context).primaryTextTheme.subtitle1.color), ), ); - // InkWell( - // onTap: isDisabled ? null : onTap, - // child: (CircleAvatar( - // backgroundColor: isEnabled ? player.playerColor.color : Colors.grey, - // child: Text( - // '${player.id}', - // style: TextStyle( - // color: Theme.of(context).primaryTextTheme.subtitle1.color), - // ), - // )), - // ); } } diff --git a/lib/widgets/UpperToolbar.dart b/lib/widgets/UpperToolbar.dart index 67d96cf4..384707ec 100644 --- a/lib/widgets/UpperToolbar.dart +++ b/lib/widgets/UpperToolbar.dart @@ -9,6 +9,7 @@ import 'package:word_by_word_game/widgets/CircularSpinner.dart'; import 'package:word_by_word_game/widgets/MenuWidget.dart'; void openBottomMenu(BuildContext context) { + showModalBottomSheet( backgroundColor: Colors.transparent, enableDrag: true, @@ -21,70 +22,74 @@ void openBottomMenu(BuildContext context) { class UpperToolbar extends StatelessWidget { @override Widget build(BuildContext context) { + var size = MediaQuery.of(context).size; return Material( color: Colors.transparent, elevation: 3, shadowColor: Theme.of(context).shadowColor.withOpacity(0.2), child: Container( + height: 72, + width: size.width, color: Colors.white.withOpacity(0.4), - padding: EdgeInsets.only(top: 30, bottom: 20, left: 20, right: 20), - child: Row(children: [ - Wrap( - children: [ - Consumer3(builder: - (context, wordsModel, playersModel, scoreModel, child) { - var wordsList = wordsModel.getWordsListByPlayer( - player: playersModel.currentPlayer); - var highscore = scoreModel.calculateHighscore(wordsList); - scoreModel.highscore.then((currentHighscore) { - if (highscore > currentHighscore) { - scoreModel.saveHighscore(highscore); - } - }); - var currentHighscoreText = - MainLocalizations.of(context).currentScore; - return Text('$currentHighscoreText $highscore'); - }), - SizedBox( - width: 12, + padding: EdgeInsets.only(top: 35, bottom: 20, left: 20, right: 20), + child: ListView( + shrinkWrap: true, + scrollDirection: Axis.horizontal, + children: [ + Consumer3(builder: + (context, wordsModel, playersModel, scoreModel, child) { + var wordsList = wordsModel.getWordsListByPlayer( + player: playersModel.currentPlayer); + var highscore = scoreModel.calculateHighscore(wordsList); + scoreModel.highscore.then((currentHighscore) { + if (highscore > currentHighscore) { + scoreModel.saveHighscore(highscore); + } + }); + var currentHighscoreText = + MainLocalizations.of(context).currentScore; + return Text('$currentHighscoreText $highscore'); + }), + SizedBox( + width: 12, + ), + SizedBox( + width: 90, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(MainLocalizations.of(context).highscore), + Consumer( + builder: (context, value, child) => FutureBuilder( + future: value.highscore, + builder: (BuildContext context, + AsyncSnapshot snapshot) { + if (snapshot.connectionState == + ConnectionState.waiting) { + return SizedBox( + width: 8, + child: CircularSpinner(), + height: 8, + ); + } else { + return Text('${snapshot.data}'); + } + }), + ), + ], ), - SizedBox( - width: 90, - child: Row( - children: [ - Text(MainLocalizations.of(context).highscore), - Consumer( - builder: (context, value, child) => FutureBuilder( - future: value.highscore, - builder: (BuildContext context, - AsyncSnapshot snapshot) { - if (snapshot.connectionState == - ConnectionState.waiting) { - return SizedBox( - width: 8, - child: CircularSpinner(), - height: 8, - ); - } else { - return Text('${snapshot.data}'); - } - }), - ), - ], - ), - ), - SizedBox( - width: 10, - ), - Consumer(builder: (context, wordsModel, child) { - var lettersToRemoveText = - MainLocalizations.of(context).lettersToRemove; - return Text( - '$lettersToRemoveText ${wordsModel.phraseLimitLettersLeft}'); - }), - ], - ), - ]), + ), + SizedBox( + width: 10, + ), + Consumer(builder: (context, wordsModel, child) { + var lettersToRemoveText = + MainLocalizations.of(context).lettersToRemove; + return Text( + '$lettersToRemoveText ${wordsModel.phraseLimitLettersLeft}'); + }), + ], + ), ), ); } diff --git a/pubspec.lock b/pubspec.lock index 213f0aef..cee4749a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -183,6 +183,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.9" + english_words: + dependency: "direct main" + description: + name: english_words + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.5" fake_async: dependency: transitive description: @@ -476,6 +483,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.4+1" + russian_words: + dependency: "direct main" + description: + name: russian_words + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0" shared_preferences: dependency: "direct main" description: @@ -657,5 +671,5 @@ packages: source: hosted version: "2.2.1" sdks: - dart: ">=2.11.0-0.0 <=2.12.0-3.0.dev" + dart: ">=2.11.0-0.0 <=2.12.0-15.0.dev" flutter: ">=1.16.0 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index a87f5a84..553b771b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.0.5+1 +version: 0.0.6+1 environment: sdk: ">=2.7.0 <3.0.0" @@ -28,15 +28,18 @@ dependencies: flutter_localizations: sdk: flutter provider: ^4.3.2+2 + russian_words: ^0.1.0 + english_words: ^3.1.5 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.0 shared_preferences: ">=0.5.12+2 <2.0.0" json_annotation: ^3.1.0 - intl_translation: ^0.17.10+1 # FIXME: due problem with intl_tranlsation # analyzer dependency overriden. + # FIXME: to run flutter pub run build_runner build need to comment intl_translation and analyzer override + intl_translation: ^0.17.10+1 dependency_overrides: analyzer: 0.39.14