├── .gitignore ├── .metadata ├── LICENSE.md ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── org │ │ │ │ └── cifruktus │ │ │ │ └── math_training │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── JetBrainsMono-Light.ttf ├── JetBrainsMono-Medium.ttf └── icon.png ├── docs └── images │ └── preview.png ├── lib ├── core │ ├── scores │ │ ├── bloc │ │ │ └── scored_bloc.dart │ │ └── models │ │ │ └── result.dart │ └── settings │ │ ├── cubit │ │ └── app_settings_cubit.dart │ │ └── models │ │ ├── math_session_type.dart │ │ └── session_durations.dart ├── features │ ├── home │ │ └── view │ │ │ ├── home_page.dart │ │ │ └── widgets.dart │ ├── math_game │ │ ├── bloc │ │ │ ├── game_bloc.dart │ │ │ ├── game_events.dart │ │ │ └── game_state.dart │ │ ├── util │ │ │ ├── question.dart │ │ │ ├── ticker.dart │ │ │ └── training.dart │ │ └── view │ │ │ └── math_game_page.dart │ └── settings │ │ └── view │ │ ├── settings_page.dart │ │ └── widgets.dart ├── main.dart └── widgets │ ├── game_input_scaffold.dart │ └── theme.dart ├── pubspec.lock ├── pubspec.yaml └── web ├── favicon.png ├── index.html └── manifest.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | 48 | # Related to flutter_icons, remove unnecessary icons 49 | android/app/src/main/res/*/ic_launcher.png 50 | ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App*.* -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 18116933e77adc82f80866c928266a5b4f1ed645 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Cifruktus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | ----------------------------------------------------------- 24 | JetBrains Mono Font Copyright Notice 25 | ----------------------------------------------------------- 26 | 27 | Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) 28 | 29 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 30 | This license is copied below, and is also available with a FAQ at: 31 | https://scripts.sil.org/OFL 32 | 33 | ----------------------------------------------------------- 34 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 35 | ----------------------------------------------------------- 36 | 37 | PREAMBLE 38 | The goals of the Open Font License (OFL) are to stimulate worldwide 39 | development of collaborative font projects, to support the font creation 40 | efforts of academic and linguistic communities, and to provide a free and 41 | open framework in which fonts may be shared and improved in partnership 42 | with others. 43 | 44 | The OFL allows the licensed fonts to be used, studied, modified and 45 | redistributed freely as long as they are not sold by themselves. The 46 | fonts, including any derivative works, can be bundled, embedded, 47 | redistributed and/or sold with any software provided that any reserved 48 | names are not used by derivative works. The fonts and derivatives, 49 | however, cannot be released under any other type of license. The 50 | requirement for fonts to remain under this license does not apply 51 | to any document created using the fonts or their derivatives. 52 | 53 | DEFINITIONS 54 | "Font Software" refers to the set of files released by the Copyright 55 | Holder(s) under this license and clearly marked as such. This may 56 | include source files, build scripts and documentation. 57 | 58 | "Reserved Font Name" refers to any names specified as such after the 59 | copyright statement(s). 60 | 61 | "Original Version" refers to the collection of Font Software components as 62 | distributed by the Copyright Holder(s). 63 | 64 | "Modified Version" refers to any derivative made by adding to, deleting, 65 | or substituting -- in part or in whole -- any of the components of the 66 | Original Version, by changing formats or by porting the Font Software to a 67 | new environment. 68 | 69 | "Author" refers to any designer, engineer, programmer, technical 70 | writer or other person who contributed to the Font Software. 71 | 72 | PERMISSION & CONDITIONS 73 | Permission is hereby granted, free of charge, to any person obtaining 74 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 75 | redistribute, and sell modified and unmodified copies of the Font 76 | Software, subject to the following conditions: 77 | 78 | 1) Neither the Font Software nor any of its individual components, 79 | in Original or Modified Versions, may be sold by itself. 80 | 81 | 2) Original or Modified Versions of the Font Software may be bundled, 82 | redistributed and/or sold with any software, provided that each copy 83 | contains the above copyright notice and this license. These can be 84 | included either as stand-alone text files, human-readable headers or 85 | in the appropriate machine-readable metadata fields within text or 86 | binary files as long as those fields can be easily viewed by the user. 87 | 88 | 3) No Modified Version of the Font Software may use the Reserved Font 89 | Name(s) unless explicit written permission is granted by the corresponding 90 | Copyright Holder. This restriction only applies to the primary font name as 91 | presented to the users. 92 | 93 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 94 | Software shall not be used to promote, endorse or advertise any 95 | Modified Version, except to acknowledge the contribution(s) of the 96 | Copyright Holder(s) and the Author(s) or with their explicit written 97 | permission. 98 | 99 | 5) The Font Software, modified or unmodified, in part or in whole, 100 | must be distributed entirely under this license, and must not be 101 | distributed under any other license. The requirement for fonts to 102 | remain under this license does not apply to any document created 103 | using the Font Software. 104 | 105 | TERMINATION 106 | This license becomes null and void if any of the above conditions are 107 | not met. 108 | 109 | DISCLAIMER 110 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 111 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 112 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 113 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 114 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 115 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 116 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 117 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 118 | OTHER DEALINGS IN THE FONT SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Math Training app 2 | 3 | A mobile application for training mental math skills. 4 | 5 | ![app preview](docs/images/preview.png) 6 | 7 | The app has several different modes, including simple addition/subtraction training and and more tricky ones like converting hex numbers to decimal. 8 | 9 | The app will save results of each training so you can track your progress. 10 | 11 | Get it on Google Play 12 | 13 | You can try the online version out [here.](https://cifruktus.github.io/MathTrainingWebapp/#/) 14 | 15 | 16 | ## How to build 17 | 18 | 1. Generate icons: 19 | `flutter pub run flutter_launcher_icons:main` 20 | 21 | 2. Build an application: 22 | `flutter build apk` 23 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | linter: 4 | rules: 5 | prefer_const_constructors: false 6 | always_use_package_imports: true 7 | avoid_relative_lib_imports: true 8 | no_adjacent_strings_in_list: true 9 | use_build_context_synchronously: true; 10 | 11 | 12 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | def keystoreProperties = new Properties() 29 | def keystorePropertiesFile = rootProject.file('key.properties') 30 | if (keystorePropertiesFile.exists()) { 31 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 32 | } 33 | 34 | android { 35 | namespace "org.cifruktus.math_training" 36 | compileSdkVersion 33 37 | 38 | compileOptions { 39 | sourceCompatibility JavaVersion.VERSION_1_8 40 | targetCompatibility JavaVersion.VERSION_1_8 41 | } 42 | 43 | kotlinOptions { 44 | jvmTarget = '1.8' 45 | } 46 | 47 | sourceSets { 48 | main.java.srcDirs += 'src/main/kotlin' 49 | } 50 | 51 | defaultConfig { 52 | applicationId "org.cifruktus.mathtraining" 53 | minSdkVersion 16 54 | targetSdkVersion 33 55 | 56 | versionCode flutterVersionCode.toInteger() 57 | versionName flutterVersionName 58 | } 59 | 60 | def keystoreAvailable = keystoreProperties['storeFile'] 61 | 62 | if (keystoreAvailable) { 63 | println('\u001b[32mSigning with provided key\u001b[0m') 64 | } else { 65 | println('\u001B[31mKeystore wasn\'t found, using debug singning config\u001b[0m') 66 | } 67 | 68 | signingConfigs { 69 | release { 70 | keyAlias keystoreProperties['keyAlias'] 71 | keyPassword keystoreProperties['keyPassword'] 72 | storeFile keystoreAvailable ? file(keystoreProperties['storeFile']) : null 73 | storePassword keystoreProperties['storePassword'] 74 | } 75 | } 76 | buildTypes { 77 | release { 78 | signingConfig keystoreAvailable ? signingConfigs.release : signingConfigs.debug 79 | } 80 | } 81 | } 82 | 83 | flutter { 84 | source '../..' 85 | } 86 | 87 | dependencies { 88 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 89 | } 90 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 15 | 19 | 23 | 28 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/org/cifruktus/math_training/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package org.cifruktus.math_training 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.8.22' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:8.1.1' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /assets/JetBrainsMono-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cifruktus/MathTraining/b4262756f0b3ed3e77b13fdf2b6700d52ecb189b/assets/JetBrainsMono-Light.ttf -------------------------------------------------------------------------------- /assets/JetBrainsMono-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cifruktus/MathTraining/b4262756f0b3ed3e77b13fdf2b6700d52ecb189b/assets/JetBrainsMono-Medium.ttf -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cifruktus/MathTraining/b4262756f0b3ed3e77b13fdf2b6700d52ecb189b/assets/icon.png -------------------------------------------------------------------------------- /docs/images/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cifruktus/MathTraining/b4262756f0b3ed3e77b13fdf2b6700d52ecb189b/docs/images/preview.png -------------------------------------------------------------------------------- /lib/core/scores/bloc/scored_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:hydrated_bloc/hydrated_bloc.dart'; 3 | import 'package:json_annotation/json_annotation.dart'; 4 | import 'package:math_training/core/scores/models/result.dart'; 5 | import 'package:math_training/core/settings/models/math_session_type.dart'; 6 | 7 | 8 | part 'scored_bloc.g.dart'; 9 | 10 | @immutable @JsonSerializable() 11 | class Scores { 12 | final List mathTestScores; 13 | 14 | const Scores({this.mathTestScores = const []}); 15 | 16 | Iterable getResultsBySessionType(MathSessionType type){ 17 | return mathTestScores.where((r) => r.type == type); 18 | } 19 | 20 | int getProblemsSolvedBySessionType(MathSessionType type){ 21 | return mathTestScores 22 | .where((r) => r.type == type) 23 | .fold(0, (int sum, r) => sum + r.correct); 24 | } 25 | 26 | double getAverageAccuracyBySessionType(MathSessionType type) { 27 | var selectedScores = mathTestScores.where((r) => r.type == type); 28 | var problemCount = selectedScores.fold(0, (int sum, r) => sum + r.questions); 29 | var correct = selectedScores.fold(0, (int sum, r) => sum + r.correct); 30 | 31 | if (problemCount == 0) return 0; 32 | return correct / problemCount; 33 | } 34 | 35 | double getAverageSpeedBySessionType(MathSessionType type) { 36 | var selectedScores = mathTestScores.where((r) => r.type == type); 37 | var totalProblems = selectedScores.fold(0, (int sum, r) => sum + r.correct); 38 | var totalTime = selectedScores.fold(0, (int sum, r) => sum + r.duration) / 60; // converting to minutes 39 | 40 | if (totalTime == 0) return 0; 41 | return totalProblems / totalTime; 42 | } 43 | 44 | static Scores fromJson(Map json){ 45 | return _$ScoresFromJson(json); 46 | } 47 | 48 | Map toJson() { 49 | return _$ScoresToJson(this); 50 | } 51 | } 52 | 53 | class ScoresCubit extends HydratedCubit { 54 | ScoresCubit() : super(const Scores()); 55 | 56 | void addScore(MathTestResult score) { 57 | emit(Scores(mathTestScores: [...state.mathTestScores, score])); 58 | } 59 | 60 | void clearScores(){ 61 | emit(const Scores(mathTestScores: [])); 62 | } 63 | 64 | @override 65 | Scores? fromJson(Map json) { 66 | return Scores.fromJson(json); 67 | } 68 | 69 | @override 70 | Map? toJson(Scores state) { 71 | return state.toJson(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/core/scores/models/result.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'package:math_training/core/settings/models/math_session_type.dart'; 3 | 4 | part 'result.g.dart'; 5 | 6 | @JsonSerializable() 7 | class MathTestResult { 8 | final int correct; 9 | final int incorrect; 10 | final int duration; 11 | final MathSessionType type; 12 | final DateTime time; 13 | 14 | int get questions => correct + incorrect; 15 | 16 | double get speed => ((correct / duration) * 60 * 100).roundToDouble() / 100; 17 | 18 | MathTestResult({ 19 | required this.correct, 20 | required this.incorrect, 21 | required this.time, 22 | required this.duration, 23 | required this.type, 24 | }); 25 | 26 | factory MathTestResult.fromJson(Map json) => _$MathTestResultFromJson(json); 27 | 28 | Map toJson() => _$MathTestResultToJson(this); 29 | } 30 | -------------------------------------------------------------------------------- /lib/core/settings/cubit/app_settings_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:hydrated_bloc/hydrated_bloc.dart'; 3 | import 'package:json_annotation/json_annotation.dart'; 4 | import 'package:math_training/core/settings/models/math_session_type.dart'; 5 | import 'package:math_training/core/settings/models/session_durations.dart'; 6 | 7 | part 'app_settings_cubit.g.dart'; 8 | 9 | class AppSettingsCubit extends HydratedCubit { 10 | AppSettingsCubit() : super(AppSettings.defaultValue()); 11 | 12 | set mathSessionDuration (Duration val) => emit(state.copyWith(mathSessionDuration: val)); 13 | set mathSessionType (MathSessionType val) => emit(state.copyWith(mathSessionType: val)); 14 | 15 | @override 16 | AppSettings fromJson(Map json) => AppSettings.fromJson(json); 17 | 18 | @override 19 | Map toJson(AppSettings state) => state.toJson(); 20 | } 21 | 22 | @immutable @JsonSerializable() 23 | class AppSettings { 24 | final MathSessionType mathSessionType; 25 | final Duration mathSessionDuration; 26 | 27 | AppSettings copyWith({ 28 | MathSessionType? mathSessionType, 29 | Duration? mathSessionDuration, 30 | }) { 31 | print(mathSessionType); 32 | return AppSettings( 33 | mathSessionType: mathSessionType ?? this.mathSessionType, 34 | mathSessionDuration: mathSessionDuration ?? this.mathSessionDuration); 35 | } 36 | 37 | factory AppSettings.defaultValue () => AppSettings( 38 | mathSessionDuration: defaultDuration, 39 | mathSessionType: MathSessionType.defaultType, 40 | ); 41 | 42 | factory AppSettings.fromJson(Map json) => _$AppSettingsFromJson(json); 43 | 44 | Map toJson() => _$AppSettingsToJson(this); 45 | 46 | const AppSettings({required this.mathSessionType, required this.mathSessionDuration}); 47 | } 48 | -------------------------------------------------------------------------------- /lib/core/settings/models/math_session_type.dart: -------------------------------------------------------------------------------- 1 | enum MathSessionType { 2 | easy("Easy"), 3 | normal("Normal"), 4 | hard("Hard"), 5 | hexDecode("Hex decode"); 6 | 7 | static const MathSessionType defaultType = easy; 8 | 9 | const MathSessionType(this.name); 10 | final String name; 11 | } 12 | -------------------------------------------------------------------------------- /lib/core/settings/models/session_durations.dart: -------------------------------------------------------------------------------- 1 | final defaultDuration = durationOptions[0]; 2 | 3 | const List durationOptions = [ 4 | Duration(minutes: 1), 5 | Duration(minutes: 2), 6 | Duration(minutes: 3), 7 | Duration(minutes: 5), 8 | Duration(minutes: 7), 9 | ]; -------------------------------------------------------------------------------- /lib/features/home/view/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_bloc/flutter_bloc.dart'; 5 | import 'package:math_training/core/settings/cubit/app_settings_cubit.dart'; 6 | import 'package:math_training/features/home/view/widgets.dart'; 7 | import 'package:math_training/features/math_game/view/math_game_page.dart'; 8 | import 'package:math_training/features/settings/view/settings_page.dart'; 9 | import 'package:math_training/core/scores/bloc/scored_bloc.dart'; 10 | import 'package:math_training/widgets/theme.dart'; 11 | 12 | 13 | class MainPage extends StatelessWidget { 14 | const MainPage({Key? key}) : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return Scaffold( 19 | body: CustomScrollView( 20 | slivers: [ 21 | TransitionAppBar(), 22 | ScoresList(), 23 | ], 24 | )); 25 | } 26 | } 27 | 28 | class TransitionAppBar extends StatelessWidget { 29 | TransitionAppBar({Key? key}) : super(key: key); 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | final MediaQueryData data = MediaQuery.of(context); 34 | EdgeInsets padding = data.padding; 35 | 36 | var sessionType = context.select((AppSettingsCubit s) => s.state.mathSessionType); 37 | var problemsSolved = context.select((ScoresCubit s) => s.state.getProblemsSolvedBySessionType(sessionType)); 38 | var averageSpeed = context.select((ScoresCubit s) => s.state.getAverageSpeedBySessionType(sessionType)); 39 | var averageAccuracy = context.select((ScoresCubit s) => s.state.getAverageAccuracyBySessionType(sessionType)); 40 | 41 | return SliverPersistentHeader( 42 | pinned: true, 43 | delegate: _TransitionAppBarDelegate( 44 | safeTopPadding: padding.top, 45 | problemsSolved: problemsSolved, 46 | averageAccuracy: averageAccuracy, 47 | averageSpeed: averageSpeed, 48 | sessionType: sessionType.name, 49 | ), 50 | ); 51 | } 52 | } 53 | 54 | class _TransitionAppBarDelegate extends SliverPersistentHeaderDelegate { 55 | static const kAppBarHeight = 56.0; 56 | static const kButtonRadius = 30.0; 57 | static const kButtonCutoutRadius = 40.0; 58 | static const kBottomPadding = 8.0; 59 | 60 | final double extent = 300; 61 | final double safeTopPadding; 62 | 63 | final double averageSpeed; 64 | final double averageAccuracy; 65 | final int problemsSolved; 66 | final String sessionType; 67 | 68 | _TransitionAppBarDelegate({ 69 | required this.sessionType, 70 | required this.averageSpeed, 71 | required this.averageAccuracy, 72 | required this.problemsSolved, 73 | required this.safeTopPadding, 74 | }); 75 | 76 | @override 77 | Widget build( 78 | BuildContext context, 79 | double shrinkOffset, 80 | bool overlapsContent, 81 | ) { 82 | double extendedValue = 1 - shrinkOffset / (maxExtent - minExtent); 83 | extendedValue = min(max(0, extendedValue), 1); 84 | 85 | double reflectionOpacity = min(max(0, (extendedValue - 0.7) * 3), 1); 86 | double smallStatsOpacity = min(max(0, (extendedValue - 0.1) * 1.5), 1); 87 | double centerStatsOpacity = min(max(0, (extendedValue - 0.4) * 2), 1); 88 | 89 | var theme = Theme.of(context).extension()!.data; 90 | 91 | var bottomColor = Color.lerp(theme.primaryColor, theme.secondaryColor, extendedValue)!; 92 | var buttonColor = Color.lerp(theme.primaryColor, bottomColor, 0.8)!; 93 | 94 | var accuracyPercent = (averageAccuracy * 100).toStringAsFixed(1); 95 | var speedRounded = averageSpeed.toStringAsFixed(1); 96 | 97 | return Padding( 98 | padding: const EdgeInsets.only(bottom: kBottomPadding), 99 | child: Stack( 100 | children: [ 101 | Positioned.fill( // background decoration and shape 102 | child: Container( 103 | margin: EdgeInsets.only(bottom: kButtonRadius), 104 | decoration: ShapeDecoration( 105 | shadows: [ 106 | BoxShadow( 107 | color: Colors.black26, 108 | blurRadius: 6.0, 109 | spreadRadius: 2.0, 110 | offset: Offset(2.0, 2.0), 111 | ), 112 | ], 113 | gradient: LinearGradient( 114 | begin: Alignment.topCenter, 115 | end: Alignment(0.0, 1.0), 116 | colors: [theme.primaryColor, bottomColor], 117 | ), 118 | shape: AppBarShapeBorder(cutoutRadius: kButtonCutoutRadius)), 119 | ), 120 | ), 121 | Positioned.fill( // reflection 122 | child: Opacity( 123 | opacity: reflectionOpacity, 124 | child: Padding( 125 | padding: EdgeInsets.only(top: (1 - extendedValue) * 100), 126 | child: Container( 127 | decoration: ShapeDecoration( 128 | gradient: LinearGradient( 129 | begin: Alignment.topCenter, 130 | end: Alignment(0.0, 1.0), 131 | colors: [Colors.white12, Color(0x00FFFFFF)], 132 | ), 133 | shape: ReflectionShape( 134 | leftPadding: safeTopPadding + kAppBarHeight * 2, 135 | rightPadding: safeTopPadding + kAppBarHeight, 136 | radius: 1.3), //CustomShapeBorder() 137 | ), 138 | ), 139 | ), 140 | ), 141 | ), 142 | Align( 143 | alignment: Alignment.topCenter, 144 | child: AppBar( 145 | toolbarHeight: kAppBarHeight, 146 | backgroundColor: Colors.transparent, 147 | elevation: 0, 148 | actions: [ 149 | new IconButton( 150 | icon: new Icon(Icons.settings), 151 | onPressed: () => Navigator.push(context, SettingsPage.route()), 152 | ), 153 | ], 154 | title: Row(children: [ 155 | Text("Training"), 156 | Opacity(opacity: smallStatsOpacity, child: Text(" - ${sessionType}")), 157 | ]), 158 | )), 159 | Align( 160 | alignment: Alignment.center, 161 | child: AppBarStatText( 162 | opacity: centerStatsOpacity, 163 | mainData: true, 164 | name: "Problems solved", 165 | value: "$problemsSolved", 166 | )), 167 | Align( 168 | alignment: Alignment.bottomLeft, 169 | child: FractionallySizedBox( 170 | widthFactor: 0.5, 171 | child: Padding( 172 | padding: const EdgeInsets.only(bottom: kButtonCutoutRadius, right: kButtonCutoutRadius), 173 | child: AppBarStatText( 174 | opacity: smallStatsOpacity, 175 | name: "Speed", 176 | value: "${speedRounded} p/min", 177 | ), 178 | ), 179 | ), 180 | ), 181 | Align( 182 | alignment: Alignment.bottomRight, 183 | child: FractionallySizedBox( 184 | widthFactor: 0.5, 185 | child: Padding( 186 | padding: const EdgeInsets.only(bottom: kButtonCutoutRadius, left: kButtonCutoutRadius), 187 | child: AppBarStatText( 188 | opacity: smallStatsOpacity, 189 | name: "Accuracy", 190 | value: "${accuracyPercent}%", 191 | ), 192 | ), 193 | )), 194 | Align( 195 | alignment: Alignment.bottomCenter, 196 | child: StartButton( 197 | size: kButtonRadius * 2, 198 | color: buttonColor, 199 | onPressed: () => Navigator.push(context, MathGamePage.route()), 200 | ), 201 | ), 202 | ], 203 | ), 204 | ); 205 | } 206 | 207 | @override 208 | double get maxExtent => extent; 209 | 210 | @override 211 | double get minExtent => safeTopPadding + kButtonRadius + kAppBarHeight + kBottomPadding; 212 | 213 | @override 214 | bool shouldRebuild(covariant _TransitionAppBarDelegate oldDelegate) { 215 | return sessionType != oldDelegate.sessionType || 216 | problemsSolved != oldDelegate.problemsSolved || 217 | averageAccuracy != oldDelegate.averageAccuracy || 218 | averageSpeed != oldDelegate.averageSpeed || 219 | safeTopPadding != oldDelegate.safeTopPadding || 220 | extent != oldDelegate.extent; 221 | } 222 | } 223 | 224 | class ScoresList extends StatelessWidget { 225 | @override 226 | Widget build(BuildContext context) { 227 | var sessionType = context.select((AppSettingsCubit s) => s.state.mathSessionType); 228 | var scores = context.select((ScoresCubit scores) => scores.state.getResultsBySessionType(sessionType)).toList(); 229 | 230 | var theme = Theme.of(context).extension()!.data; 231 | 232 | if (scores.isEmpty) { 233 | return SliverFillRemaining( 234 | child: Center( 235 | child: Text( 236 | "No scores", 237 | style: theme.cardTextHighlighted, 238 | )), 239 | ); 240 | } 241 | 242 | return SliverList( 243 | delegate: SliverChildBuilderDelegate((context, index) { 244 | return MathTestResultCard(scores[scores.length - 1 - index]); 245 | }, 246 | childCount: scores.length 247 | ), 248 | ); 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /lib/features/home/view/widgets.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:math_training/core/scores/models/result.dart'; 5 | import 'package:math_training/widgets/theme.dart'; 6 | 7 | class MathTestResultCard extends StatelessWidget { 8 | static const divider = Divider(color: Colors.white54); 9 | 10 | final MathTestResult result; 11 | 12 | const MathTestResultCard(this.result, {Key? key}) : super(key: key); 13 | 14 | // It's better to use intl 15 | String _getTimeString(DateTime time) { 16 | var year = time.year.toString(); 17 | var month = time.month.toString().padLeft(2, "0"); 18 | var day = time.day.toString().padLeft(2, "0"); 19 | var hours = time.hour.toString().padLeft(2, "0"); 20 | var minutes = time.minute.toString().padLeft(2, "0"); 21 | return "$day.$month.$year $hours:$minutes"; 22 | } 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | var theme = Theme.of(context).extension()!.data; 27 | 28 | return Padding( 29 | padding: const EdgeInsets.all(8.0), 30 | child: Container( 31 | decoration: BoxDecoration( 32 | boxShadow: [ 33 | BoxShadow( 34 | color: Colors.black26, 35 | blurRadius: 6.0, 36 | spreadRadius: 2.0, 37 | offset: Offset(2.0, 2.0), 38 | ), 39 | ], 40 | gradient: LinearGradient( 41 | begin: Alignment.topCenter, 42 | end: Alignment.bottomRight, 43 | colors: [ 44 | theme.primaryColor, 45 | Color.lerp(theme.primaryColor, theme.secondaryColor, 0.33)!, 46 | theme.secondaryColor, 47 | ], 48 | ), 49 | borderRadius: BorderRadius.circular(16), 50 | ), 51 | child: Padding( 52 | padding: const EdgeInsets.all(16.0), 53 | child: Column( 54 | crossAxisAlignment: CrossAxisAlignment.start, 55 | mainAxisSize: MainAxisSize.min, 56 | children: [ 57 | Padding( 58 | padding: const EdgeInsets.only(bottom: 16.0), 59 | child: Center( 60 | child: Text( 61 | _getTimeString(result.time), 62 | style: theme.cardTitleText, 63 | )), 64 | ), 65 | DefaultTextStyle.merge( 66 | style: theme.cardText.copyWith(color: Colors.white), 67 | child: Column( 68 | children: [ 69 | _ResultsCardNameValue( 70 | name: Text("Solved"), 71 | value: Text("${result.correct} / ${result.questions}"), 72 | ), 73 | divider, 74 | _ResultsCardNameValue( 75 | name: Text("Duration"), 76 | value: Text("${result.duration} sec"), 77 | ), 78 | divider, 79 | _ResultsCardNameValue( 80 | name: Text("Speed"), 81 | value: Text("${result.speed} p/min"), 82 | ), 83 | ], 84 | ), 85 | ), 86 | ], 87 | ), 88 | ), 89 | ), 90 | ); 91 | } 92 | } 93 | 94 | class _ResultsCardNameValue extends StatelessWidget { 95 | final Widget name; 96 | final Widget value; 97 | 98 | const _ResultsCardNameValue({Key? key, required this.name, required this.value}) : super(key: key); 99 | 100 | @override 101 | Widget build(BuildContext context) { 102 | return Row( 103 | children: [ 104 | Expanded( 105 | child: name, 106 | ), 107 | value, 108 | ], 109 | ); 110 | } 111 | } 112 | 113 | class StartButton extends StatelessWidget { 114 | final Color? color; 115 | final double size; 116 | final void Function() onPressed; 117 | 118 | const StartButton({ 119 | Key? key, 120 | this.color, 121 | this.size = 60, 122 | required this.onPressed, 123 | }) : super(key: key); 124 | 125 | @override 126 | Widget build(BuildContext context) { 127 | return Material( 128 | color: color ?? Theme.of(context).primaryColor, 129 | elevation: 6, 130 | shape: CircleBorder(), 131 | child: Container( 132 | width: size, 133 | height: size, 134 | child: IconButton( 135 | onPressed: onPressed, 136 | icon: Icon( 137 | Icons.play_arrow_rounded, 138 | color: Colors.white, 139 | size: size * 0.60, 140 | ), 141 | )), 142 | // onPressed: (){}, 143 | ); 144 | } 145 | } 146 | 147 | class AppBarStatText extends StatelessWidget { 148 | final double opacity; 149 | final String name; 150 | final String value; 151 | final bool mainData; 152 | 153 | const AppBarStatText({ 154 | Key? key, 155 | required this.opacity, 156 | required this.name, 157 | required this.value, 158 | this.mainData = false, 159 | }) : super(key: key); 160 | 161 | @override 162 | Widget build(BuildContext context) { 163 | var theme = Theme.of(context).extension()!.data; 164 | 165 | return Opacity( 166 | opacity: opacity, 167 | child: DefaultTextStyle.merge( 168 | style: TextStyle(color: Colors.white70), 169 | child: Column( 170 | crossAxisAlignment: CrossAxisAlignment.center, 171 | mainAxisSize: MainAxisSize.min, 172 | children: [ 173 | Text(name), 174 | Text(value, style: mainData ? theme.homePageMainStatText : theme.homePageStatText), 175 | ], 176 | ), 177 | ), 178 | ); 179 | } 180 | } 181 | 182 | class AppBarShapeBorder extends ContinuousRectangleBorder { 183 | final double cutoutBorderRadius; 184 | final double cutoutRadius; 185 | final double bottomPadding; 186 | 187 | AppBarShapeBorder({ 188 | required this.cutoutRadius, 189 | this.cutoutBorderRadius = 30.0, 190 | this.bottomPadding = 0, 191 | }); 192 | 193 | @override 194 | Path getOuterPath(Rect rect, {TextDirection? textDirection}) { 195 | final double middle = rect.width / 2; 196 | final double height = rect.height - bottomPadding; 197 | 198 | double cosD = cutoutBorderRadius / (cutoutBorderRadius + cutoutRadius); 199 | double sinD = sqrt(1 - cosD * cosD); 200 | double h = cutoutRadius * cosD; 201 | double d = cutoutRadius * sinD; 202 | double d2 = (cutoutRadius + cutoutBorderRadius) * sinD; 203 | 204 | Path path = Path(); 205 | path.lineTo(0, height); 206 | 207 | path.lineTo(middle - d2, height); 208 | 209 | path.arcToPoint( 210 | Offset(middle - d, height - h), 211 | radius: Radius.circular(cutoutBorderRadius), 212 | clockwise: false, 213 | ); 214 | 215 | path.arcToPoint( 216 | Offset(middle + d, height - h), 217 | radius: Radius.circular(cutoutRadius), 218 | ); 219 | 220 | path.arcToPoint( 221 | Offset(middle + d2, height), 222 | radius: Radius.circular(cutoutBorderRadius), 223 | clockwise: false, 224 | ); 225 | 226 | path.lineTo(rect.width, height); 227 | path.lineTo(rect.width, 0.0); 228 | 229 | path.close(); 230 | 231 | return path.shift(Offset(rect.left, rect.top)); 232 | } 233 | } 234 | 235 | class ReflectionShape extends ContinuousRectangleBorder { 236 | final double leftPadding; 237 | final double rightPadding; 238 | final double radius; 239 | 240 | ReflectionShape({ 241 | required this.leftPadding, 242 | required this.rightPadding, 243 | required this.radius, 244 | }); 245 | 246 | @override 247 | Path getOuterPath(Rect rect, {TextDirection? textDirection}) { 248 | double leftPadding = min(this.leftPadding, rect.height); 249 | double rightPadding = min(this.rightPadding, rect.height); 250 | 251 | Path path = Path(); 252 | 253 | path.lineTo(0, leftPadding); 254 | 255 | double radius = Offset(rect.width, rightPadding - leftPadding).distance * this.radius; 256 | 257 | path.arcToPoint( 258 | Offset(rect.width, rightPadding), 259 | radius: Radius.circular(radius), 260 | ); 261 | 262 | path.lineTo(rect.width, rect.height); 263 | path.lineTo(0.0, rect.height); 264 | 265 | path.close(); 266 | 267 | return path.shift(Offset(rect.left, rect.top)); 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /lib/features/math_game/bloc/game_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:bloc/bloc.dart'; 3 | 4 | import 'package:math_training/core/scores/models/result.dart'; 5 | import 'package:math_training/core/settings/cubit/app_settings_cubit.dart'; 6 | import 'package:math_training/core/settings/models/math_session_type.dart'; 7 | import 'package:math_training/features/math_game/bloc/game_events.dart'; 8 | import 'package:math_training/features/math_game/bloc/game_state.dart'; 9 | import 'package:math_training/features/math_game/util/ticker.dart'; 10 | import 'package:math_training/features/math_game/util/training.dart'; 11 | 12 | class MathGameBloc extends Bloc { 13 | final Ticker _ticker; 14 | final MathSessionType sessionType; 15 | final QuestionGenerator _questions; 16 | final Duration _duration; 17 | 18 | Duration get duration => _duration; 19 | 20 | StreamSubscription? _tickerSubscription; 21 | 22 | factory MathGameBloc.fromSettings(AppSettings settings) { 23 | var sessionType = settings.mathSessionType; 24 | var questions = getQuestionGenerator(settings.mathSessionType); 25 | var duration = settings.mathSessionDuration; 26 | 27 | return MathGameBloc( 28 | sessionType: sessionType, 29 | questions: questions, 30 | duration: duration, 31 | ); 32 | } 33 | 34 | MathGameBloc({ 35 | required QuestionGenerator questions, 36 | required MathSessionType sessionType, 37 | required Duration duration, 38 | }) : _ticker = const Ticker(), 39 | _questions = questions, 40 | _duration = duration, 41 | sessionType = sessionType, 42 | super(MathGameState( 43 | answered: [], 44 | inputValue: '', 45 | duration: duration.inSeconds, 46 | stateType: GameStateType.ready, 47 | currentQuestion: questions.next(), 48 | )) { 49 | on(_onPaused); 50 | on(_onResumed); 51 | on(_onTicked); 52 | on(_onNewInputData); 53 | on(_onAccept); 54 | on(_onStartGame); 55 | } 56 | 57 | @override 58 | Future close() { 59 | _tickerSubscription?.cancel(); 60 | return super.close(); 61 | } 62 | 63 | MathTestResult? getResult() { 64 | if (state.stateType != GameStateType.finished) return null; 65 | 66 | var time = DateTime.now(); 67 | int correct = 0; 68 | int incorrect = 0; 69 | for (var question in state.answered) { 70 | if (question.isCorrect) { 71 | correct++; 72 | } else { 73 | incorrect++; 74 | } 75 | } 76 | 77 | return MathTestResult( 78 | duration: _duration.inSeconds, 79 | correct: correct, 80 | incorrect: incorrect, 81 | time: time, 82 | type: sessionType, 83 | ); 84 | } 85 | 86 | void _onStartGame(MathGameStart event, Emitter emit) { 87 | if (state.stateType == GameStateType.ready) { 88 | emit(state.copyWith(stateType: GameStateType.going)); 89 | _tickerSubscription?.cancel(); 90 | _tickerSubscription = 91 | _ticker.tick(ticks: state.duration).listen((duration) => add(MathGameTimerTicked(duration: duration))); 92 | return; 93 | } 94 | } 95 | 96 | void _onNewInputData(MathGameNewInputData event, Emitter emit) { 97 | emit(state.copyWith(inputValue: event.input)); 98 | } 99 | 100 | void _onAccept(MathGameOnOkInput event, Emitter emit) { 101 | if (state.stateType == GameStateType.going) { 102 | if (event.input.isEmpty) return; 103 | var answered = state.currentQuestion.asAnswered(int.tryParse(event.input) ?? -1); 104 | var next = _questions.next(); 105 | emit(state.copyWith( 106 | inputValue: "", 107 | answered: [...state.answered, answered], 108 | currentQuestion: next, 109 | )); 110 | } 111 | } 112 | 113 | void _onPaused(MathGamePaused event, Emitter emit) { 114 | if (state.stateType == GameStateType.going) { 115 | _tickerSubscription?.pause(); 116 | emit(state.copyWith(stateType: GameStateType.paused)); 117 | } 118 | } 119 | 120 | void _onResumed(MathGameResumed resume, Emitter emit) { 121 | if (state.stateType == GameStateType.paused) { 122 | _tickerSubscription?.resume(); 123 | emit(state.copyWith(stateType: GameStateType.going)); 124 | } 125 | } 126 | 127 | void _onTicked(MathGameTimerTicked event, Emitter emit) { 128 | emit( 129 | event.duration > 0 130 | ? state.copyWith(duration: event.duration) 131 | : state.copyWith(stateType: GameStateType.finished, duration: 0), 132 | ); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /lib/features/math_game/bloc/game_events.dart: -------------------------------------------------------------------------------- 1 | abstract class MathGameEvent { 2 | const MathGameEvent(); 3 | } 4 | 5 | class MathGamePaused extends MathGameEvent { 6 | const MathGamePaused(); 7 | } 8 | 9 | class MathGameResumed extends MathGameEvent { 10 | const MathGameResumed(); 11 | } 12 | 13 | class MathGameNewInputData extends MathGameEvent { 14 | final String input; 15 | 16 | const MathGameNewInputData(this.input); 17 | } 18 | 19 | class MathGameStart extends MathGameEvent { 20 | const MathGameStart(); 21 | } 22 | 23 | class MathGameOnOkInput extends MathGameEvent { 24 | final String input; 25 | 26 | const MathGameOnOkInput(this.input); 27 | } 28 | 29 | class MathGameTimerTicked extends MathGameEvent { 30 | const MathGameTimerTicked({required this.duration}); 31 | final int duration; 32 | } -------------------------------------------------------------------------------- /lib/features/math_game/bloc/game_state.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | import 'package:math_training/features/math_game/util/question.dart'; 4 | 5 | 6 | part 'game_state.freezed.dart'; 7 | 8 | @freezed 9 | class MathGameState with _$MathGameState { 10 | const MathGameState._(); 11 | 12 | const factory MathGameState({ 13 | required int duration, 14 | required String inputValue, 15 | required List answered, 16 | required Question currentQuestion, 17 | required GameStateType stateType, 18 | }) = _MathGameState; 19 | 20 | bool get noMistakes { 21 | return answered.where((answered) => !answered.isCorrect).isEmpty; 22 | } 23 | } 24 | 25 | enum GameStateType { ready, going, paused, finished } 26 | -------------------------------------------------------------------------------- /lib/features/math_game/util/question.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | @immutable 4 | class Question { 5 | final String formula; 6 | final int refAnswer; 7 | 8 | String get formulaWithAnswer => "$formula = $refAnswer"; 9 | 10 | const Question(this.formula, this.refAnswer); 11 | 12 | bool checkAnswer(int answer) { 13 | return answer == refAnswer; 14 | } 15 | 16 | AnsweredQuestion asAnswered(int answer) { 17 | return AnsweredQuestion(formula, refAnswer, answer); 18 | } 19 | 20 | factory Question.addition(int a, int b) { 21 | if (a < b) { 22 | var tmp = a; 23 | a = b; 24 | b = tmp; 25 | } 26 | return Question("$a + $b", a + b); 27 | } 28 | 29 | factory Question.subtraction(int a, int b) { 30 | if (a < b) { 31 | var tmp = a; 32 | a = b; 33 | b = tmp; 34 | } 35 | return Question("$a - $b", a - b); 36 | } 37 | 38 | factory Question.multiplication(int a, int b) { 39 | if (a < b) { 40 | var tmp = a; 41 | a = b; 42 | b = tmp; 43 | } 44 | return Question("$a × $b", a * b); 45 | } 46 | 47 | factory Question.division(int a, int b) { 48 | if (a < b) { 49 | var tmp = a; 50 | a = b; 51 | b = tmp; 52 | } 53 | return Question("$a / $b", (a / b).floor()); 54 | } 55 | } 56 | 57 | class AnsweredQuestion extends Question { 58 | final int answer; 59 | 60 | bool get isCorrect => checkAnswer(answer); 61 | 62 | const AnsweredQuestion(String formula, int refAnswer, this.answer) : super(formula, refAnswer); 63 | } -------------------------------------------------------------------------------- /lib/features/math_game/util/ticker.dart: -------------------------------------------------------------------------------- 1 | class Ticker { 2 | const Ticker(); 3 | 4 | Stream tick({required int ticks}) { 5 | return Stream.periodic(const Duration(seconds: 1), (x) => ticks - x - 1).take(ticks); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/features/math_game/util/training.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:math_training/core/settings/models/math_session_type.dart'; 4 | import 'package:math_training/features/math_game/util/question.dart'; 5 | 6 | QuestionGenerator getQuestionGenerator(MathSessionType type) { 7 | switch (type) { 8 | case MathSessionType.easy: 9 | return EasyQuestionGenerator(); 10 | case MathSessionType.normal: 11 | return NormalQuestionGenerator(); 12 | case MathSessionType.hard: 13 | return HardQuestionGenerator(); 14 | case MathSessionType.hexDecode: 15 | return HexQuestionGenerator(); 16 | } 17 | } 18 | 19 | abstract class QuestionGenerator { 20 | Random r = Random(); 21 | 22 | Question next(); 23 | 24 | int nextInt(int start, int end) { 25 | return r.nextInt(end - start) + start; //end exclusive 26 | } 27 | } 28 | 29 | class HardQuestionGenerator extends QuestionGenerator { 30 | @override 31 | Question next() { 32 | switch (r.nextInt(4)) { 33 | case 0: 34 | return Question.addition(nextInt(101, 300), nextInt(101, 300)); 35 | case 1: 36 | return Question.subtraction(nextInt(101, 300), nextInt(101, 300)); 37 | case 2: 38 | return Question.division(nextInt(11, 300), nextInt(2, 10)); 39 | default: 40 | return Question.multiplication(nextInt(12, 20), nextInt(11, 20)); 41 | } 42 | } 43 | } 44 | 45 | class NormalQuestionGenerator extends QuestionGenerator { 46 | @override 47 | Question next() { 48 | switch (r.nextInt(3)) { 49 | case 0: 50 | return Question.addition(nextInt(11, 100), nextInt(11, 100)); 51 | case 1: 52 | return Question.subtraction(nextInt(11, 100), nextInt(11, 100)); 53 | default: 54 | return Question.multiplication(nextInt(12, 20), nextInt(2, 10)); 55 | } 56 | } 57 | } 58 | 59 | class EasyQuestionGenerator extends QuestionGenerator { 60 | @override 61 | Question next() { 62 | switch (r.nextInt(2)) { 63 | case 0: 64 | return Question.addition(nextInt(11, 100), nextInt(2, 10)); 65 | default: 66 | return Question.subtraction(nextInt(11, 100), nextInt(2, 10)); 67 | } 68 | } 69 | } 70 | 71 | class HexQuestionGenerator extends QuestionGenerator { 72 | @override 73 | Question next() { 74 | int val = r.nextInt(255); 75 | return new Question("0x" + val.toRadixString(16).toUpperCase(), val); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/features/math_game/view/math_game_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:math_training/core/scores/bloc/scored_bloc.dart'; 4 | import 'package:math_training/core/settings/cubit/app_settings_cubit.dart'; 5 | import 'package:math_training/features/math_game/bloc/game_bloc.dart'; 6 | import 'package:math_training/features/math_game/bloc/game_events.dart'; 7 | import 'package:math_training/features/math_game/bloc/game_state.dart'; 8 | import 'package:math_training/widgets/theme.dart'; 9 | import 'package:math_training/widgets/game_input_scaffold.dart'; 10 | 11 | 12 | class MathGamePage extends StatelessWidget { 13 | 14 | static Route route() { 15 | return MaterialPageRoute( 16 | builder: (c) => const MathGamePage(), 17 | ); 18 | } 19 | 20 | const MathGamePage({Key? key}) : super(key: key); 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | return BlocProvider( 25 | create: (context) => MathGameBloc.fromSettings(context.read().state), 26 | child: BlocListener( 27 | child: const MathGameView(), 28 | listenWhen: (a, b) => a.stateType != b.stateType, 29 | listener: (context, state) { 30 | if (state.stateType == GameStateType.finished) { 31 | var result = context.read().getResult(); 32 | if (result != null) { 33 | context.read().addScore(result); 34 | } 35 | } 36 | }, 37 | ), 38 | ); 39 | } 40 | } 41 | 42 | class MathGameView extends StatelessWidget { 43 | const MathGameView({Key? key}) : super(key: key); 44 | 45 | @override 46 | Widget build(BuildContext context) { 47 | var state = context.select((MathGameBloc bloc) => bloc.state.stateType); 48 | 49 | return GameInputScaffold( 50 | onApply: (s) => applyPressed(context, s), 51 | onChange: (s) => onChange(context, s), 52 | numericInputEnabled: state == GameStateType.going, 53 | appBar: AppBar( 54 | title: GameAppTitle(), 55 | ), 56 | body: MainGameWindow(), 57 | ); 58 | } 59 | 60 | void onChange(BuildContext context, String text){ 61 | context.read().add(MathGameNewInputData(text)); 62 | } 63 | 64 | void applyPressed(BuildContext context, String text) { 65 | var gameBloc = context.read(); 66 | 67 | switch (gameBloc.state.stateType) { 68 | case GameStateType.ready: 69 | gameBloc.add(MathGameStart()); 70 | return; 71 | case GameStateType.going: 72 | gameBloc.add(MathGameOnOkInput(text)); 73 | return; 74 | case GameStateType.finished: 75 | Navigator.of(context).pop(); 76 | return; 77 | default: 78 | return; 79 | } 80 | } 81 | } 82 | 83 | 84 | class MainGameWindow extends StatelessWidget { 85 | const MainGameWindow({Key? key}) : super(key: key); 86 | 87 | @override 88 | Widget build(BuildContext context) { 89 | var state = context.select((MathGameBloc bloc) => bloc.state.stateType); 90 | 91 | if (state == GameStateType.ready) return const BeforeGameView(); 92 | if (state == GameStateType.finished) return AfterGameView(); 93 | if (state == GameStateType.going) return QuestionTextField(); 94 | 95 | return Container(); 96 | } 97 | } 98 | 99 | class BeforeGameView extends StatelessWidget { 100 | const BeforeGameView({Key? key}) : super(key: key); 101 | 102 | @override 103 | Widget build(BuildContext context) { 104 | var game = context.watch(); 105 | var theme = Theme.of(context).extension()!.data; 106 | 107 | return Column( 108 | children: [ 109 | Expanded( 110 | child: Container(), 111 | ), 112 | Center( 113 | child: Card( 114 | child: Padding( 115 | padding: const EdgeInsets.all(8.0), 116 | child: Column( 117 | mainAxisSize: MainAxisSize.min, 118 | children: [ 119 | Text("Session type: ${game.sessionType.name}", style: theme.cardText,), 120 | Text("Duration: ${game.duration.inMinutes} min", style: theme.cardText), 121 | ], 122 | ), 123 | ), 124 | ), 125 | ), 126 | 127 | Expanded( 128 | child: Center( 129 | child: Text( 130 | "Press OK to \nstart", 131 | textAlign: TextAlign.center, 132 | style: TextStyle( 133 | fontSize: 24, 134 | color: Colors.black45 135 | ) 136 | ), 137 | ), 138 | ), 139 | ], 140 | ); 141 | } 142 | } 143 | 144 | class AfterGameView extends StatelessWidget { 145 | const AfterGameView({Key? key}) : super(key: key); 146 | 147 | @override 148 | Widget build(BuildContext context) { 149 | var questions = context.select((MathGameBloc bloc) => bloc.state.answered); 150 | var theme = Theme.of(context).extension()!.data; 151 | 152 | return ListView.builder( 153 | itemCount: questions.length, 154 | itemBuilder: (context, i) { 155 | var question = questions[i]; 156 | var correct = question.isCorrect; 157 | 158 | return Card( 159 | shape: correct 160 | ? null 161 | : new RoundedRectangleBorder( 162 | side: new BorderSide(color: theme.mistakeHighlightColor, width: 2.0), 163 | borderRadius: BorderRadius.circular(4.0), 164 | ), 165 | child: Padding( 166 | padding: const EdgeInsets.all(8.0), 167 | child: Row( 168 | mainAxisSize: MainAxisSize.max, 169 | children: [ 170 | Expanded( 171 | child: Text.rich( 172 | TextSpan(children: [ 173 | TextSpan(text: "${question.formula} = ${question.refAnswer} "), 174 | if (!correct) 175 | TextSpan( 176 | text: "${question.answer}", 177 | style: TextStyle(decoration: TextDecoration.lineThrough), 178 | ) 179 | ]), 180 | style: theme.cardText, 181 | ), 182 | ), 183 | Icon(correct ? Icons.check : Icons.clear), 184 | ], 185 | ), 186 | )); 187 | }, 188 | ); 189 | } 190 | } 191 | 192 | class GameAppTitle extends StatelessWidget { 193 | const GameAppTitle({Key? key}) : super(key: key); 194 | 195 | @override 196 | Widget build(BuildContext context) { 197 | var secondsToGo = context.select((MathGameBloc bloc) => bloc.state.duration); 198 | var state = context.select((MathGameBloc bloc) => bloc.state.stateType); 199 | 200 | switch (state) { 201 | case GameStateType.ready: 202 | return const Text("Math test"); 203 | case GameStateType.finished: 204 | return ResultsGameTitle(); 205 | default: 206 | return Text("Training (${secondsToGo}s)"); 207 | } 208 | } 209 | } 210 | 211 | class ResultsGameTitle extends StatelessWidget { 212 | @override 213 | Widget build(BuildContext context) { 214 | bool noMistakes = context.select((MathGameBloc bloc) => bloc.state.noMistakes); 215 | 216 | return noMistakes 217 | ? Row( 218 | children: [ 219 | Text("Results "), 220 | Icon(Icons.auto_awesome_rounded), 221 | ], 222 | ) 223 | : Text("Results"); 224 | } 225 | } 226 | 227 | class QuestionTextField extends StatelessWidget { 228 | const QuestionTextField({Key? key}) : super(key: key); 229 | 230 | @override 231 | Widget build(BuildContext context) { 232 | var inputValue = context.select((MathGameBloc bloc) => bloc.state.inputValue); 233 | var formula = context.select((MathGameBloc bloc) => bloc.state.currentQuestion.formula); 234 | return Padding( 235 | padding: const EdgeInsets.all(16.0), 236 | child: Column( 237 | children: [ 238 | Flexible( 239 | child: Center( 240 | child: FittedBox( 241 | fit: BoxFit.fitHeight, 242 | child: Text( 243 | formula, 244 | style: TextStyle(fontSize: 69), 245 | ), 246 | ), 247 | ), 248 | ), 249 | FractionallySizedBox( 250 | widthFactor: 0.5, 251 | child: Container( 252 | height: 5, 253 | decoration: const BoxDecoration( 254 | borderRadius: BorderRadius.all(Radius.circular(5)), 255 | color: Colors.black, 256 | ), 257 | ), 258 | ), 259 | Flexible( 260 | child: Center( 261 | child: inputValue.isNotEmpty 262 | ? FittedBox( 263 | fit: BoxFit.fitHeight, 264 | child: Text( 265 | inputValue, 266 | style: TextStyle(fontSize: 69), 267 | ), 268 | ) 269 | : Container(), 270 | ), 271 | ), 272 | ], 273 | ), 274 | ); 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /lib/features/settings/view/settings_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:math_training/core/settings/models/math_session_type.dart'; 4 | import 'package:math_training/core/settings/models/session_durations.dart'; 5 | import 'package:math_training/features/settings/view/widgets.dart'; 6 | import 'package:math_training/core/scores/bloc/scored_bloc.dart'; 7 | import 'package:math_training/core/settings/cubit/app_settings_cubit.dart'; 8 | import 'package:url_launcher/url_launcher.dart'; 9 | 10 | final Uri githubPage = Uri.parse('https://github.com/Cifruktus/MathTraining'); 11 | final Uri privacyPolicyPage = Uri.parse('https://cifruktus.github.io/MathTrainingWebapp/privacy-policy'); 12 | 13 | class SettingsPage extends StatelessWidget { 14 | const SettingsPage({Key? key}) : super(key: key); 15 | 16 | static Route route() { 17 | return MaterialPageRoute( 18 | builder: (_) => SettingsPage(), 19 | ); 20 | } 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | return Scaffold( 25 | appBar: AppBar( 26 | title: Text("Settings"), 27 | ), 28 | body: Column( 29 | crossAxisAlignment: CrossAxisAlignment.stretch, 30 | children: [ 31 | MathDurationEditor(), 32 | MathSessionEditor(), 33 | ClearScoresButton(), 34 | GithubPageButton(), 35 | PrivacyPageButton(), 36 | ], 37 | ), 38 | ); 39 | } 40 | } 41 | 42 | class ClearScoresButton extends StatelessWidget { 43 | @override 44 | Widget build(BuildContext context) { 45 | return NameValueCard( 46 | onTap: () => showDialog(context: context, builder: (c) => ClearScoresDialog()), 47 | name: Text("Clear scores"), 48 | value: Container(), 49 | ); 50 | } 51 | } 52 | 53 | class ClearScoresDialog extends StatelessWidget { 54 | const ClearScoresDialog({Key? key}): super(key: key); 55 | 56 | @override 57 | Widget build(BuildContext context) { 58 | return AlertDialog( 59 | content: Text("All scores will be deleted"), 60 | actions: [ 61 | TextButton( 62 | child: Text("Cancel"), 63 | onPressed: () => Navigator.pop(context), 64 | ), 65 | TextButton( 66 | child: Text("Ok"), 67 | onPressed: () { 68 | Navigator.pop(context); 69 | context.read().clearScores(); 70 | }, 71 | ), 72 | ], 73 | ); 74 | } 75 | } 76 | 77 | class GithubPageButton extends StatelessWidget { 78 | @override 79 | Widget build(BuildContext context) { 80 | return NameValueCard( 81 | onTap: () => launchUrl(githubPage, mode: LaunchMode.externalApplication), 82 | name: Text("View project on Github", 83 | style: TextStyle( 84 | color: Colors.blueAccent[700] 85 | ), 86 | ), 87 | value: Container(), 88 | ); 89 | } 90 | } 91 | 92 | class PrivacyPageButton extends StatelessWidget { 93 | @override 94 | Widget build(BuildContext context) { 95 | return NameValueCard( 96 | onTap: () => launchUrl(privacyPolicyPage, mode: LaunchMode.externalApplication), 97 | name: Text("Privacy Policy", 98 | style: TextStyle( 99 | color: Colors.blueAccent[700] 100 | ), 101 | ), 102 | value: Container(), 103 | ); 104 | } 105 | } 106 | 107 | class MathDurationEditor extends StatelessWidget { 108 | @override 109 | Widget build(BuildContext context) { 110 | var duration = context.select((AppSettingsCubit c) => c.state.mathSessionDuration); 111 | 112 | return NameValueCard( 113 | onTap: () => showDialog(context: context, builder: (c) => MathSessionDurationDialog()), 114 | name: Text("Game duration:"), 115 | value: Text("${duration.inMinutes} min"), 116 | ); 117 | } 118 | } 119 | 120 | class MathSessionDurationDialog extends StatelessWidget { 121 | const MathSessionDurationDialog({Key? key}) : super(key: key); 122 | 123 | @override 124 | Widget build(BuildContext context) { 125 | List> elements = durationOptions 126 | .map((option) => ListDialogElement( 127 | value: option, 128 | child: Text("${option.inMinutes} min"), 129 | )) 130 | .toList(); 131 | 132 | var settings = context.read(); 133 | 134 | return ListDialog( 135 | elements: elements, 136 | title: "Duration", 137 | selected: settings.state.mathSessionDuration, 138 | onChoiceMade: (data) => settings.mathSessionDuration = data, 139 | ); 140 | } 141 | } 142 | 143 | class MathSessionEditor extends StatelessWidget { 144 | @override 145 | Widget build(BuildContext context) { 146 | var sessionType = context.select((AppSettingsCubit c) => c.state.mathSessionType); 147 | 148 | return NameValueCard( 149 | onTap: () => showDialog(context: context, builder: (c) => MathSessionTypeDialog()), 150 | name: Text("Session type:"), 151 | value: Text(sessionType.name), 152 | ); 153 | } 154 | } 155 | 156 | class MathSessionTypeDialog extends StatelessWidget { 157 | const MathSessionTypeDialog({Key? key}) : super(key: key); 158 | 159 | @override 160 | Widget build(BuildContext context) { 161 | List> elements = MathSessionType.values 162 | .map((option) => ListDialogElement(value: option, child: Text("${option.name}"))) 163 | .toList(); 164 | 165 | var settings = context.read(); 166 | 167 | return ListDialog( 168 | elements: elements, 169 | title: "Session type", 170 | selected: settings.state.mathSessionType, 171 | onChoiceMade: (data) => settings.mathSessionType = data, 172 | ); 173 | } 174 | } 175 | 176 | -------------------------------------------------------------------------------- /lib/features/settings/view/widgets.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:math_training/widgets/theme.dart'; 3 | 4 | class NameValueCard extends StatelessWidget { 5 | final Widget name; 6 | final Widget value; 7 | final void Function() onTap; 8 | 9 | const NameValueCard({ 10 | Key? key, 11 | required this.name, 12 | required this.value, 13 | required this.onTap, 14 | }) : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | var theme = Theme.of(context).extension()!.data; 19 | 20 | return Card( 21 | child: Padding( 22 | padding: const EdgeInsets.all(8.0), 23 | child: GestureDetector( 24 | onTap: onTap, 25 | child: new Row(children: [ 26 | Expanded( 27 | child: DefaultTextStyle.merge( 28 | style: theme.cardText, 29 | child: name, 30 | ), 31 | ), 32 | Padding( 33 | padding: const EdgeInsets.only(left: 8.0), 34 | child: DefaultTextStyle.merge( 35 | style: theme.cardTextHighlighted, 36 | child: value, 37 | ), 38 | ) 39 | ]), 40 | ), 41 | )); 42 | } 43 | } 44 | 45 | class ListDialog extends StatelessWidget { 46 | final List> elements; 47 | final String title; 48 | final T? selected; 49 | final Function(T)? onChoiceMade; 50 | 51 | const ListDialog({ 52 | this.onChoiceMade, 53 | Key? key, 54 | required this.title, 55 | required this.elements, 56 | this.selected, 57 | }); 58 | 59 | static ListDialog fromStringList( 60 | String title, 61 | List list, { 62 | String? selected, 63 | Function(String)? onChoiceMade, 64 | }) { 65 | return ListDialog( 66 | onChoiceMade: onChoiceMade, 67 | title: title, 68 | selected: selected, 69 | elements: list 70 | .map((option) => ListDialogElement( 71 | value: option, 72 | child: Text(option), 73 | )) 74 | .toList()); 75 | } 76 | 77 | @override 78 | Widget build(BuildContext context) { 79 | return SimpleDialog( 80 | title: Text(title), 81 | children: elements.map((element) => _buildOption(element, context)).toList(), 82 | ); 83 | } 84 | 85 | Widget _buildOption(ListDialogElement element, BuildContext context) { 86 | return Container( 87 | color: element.value == selected ? Theme.of(context).colorScheme.primary.withAlpha(30) : Colors.transparent, 88 | child: SimpleDialogOption( 89 | onPressed: () { 90 | onChoiceMade?.call(element.value); 91 | Navigator.pop(context, element.value); 92 | }, 93 | child: element.child, 94 | ), 95 | ); 96 | } 97 | } 98 | 99 | class ListDialogElement extends StatelessWidget { 100 | final T value; 101 | final Widget child; 102 | 103 | const ListDialogElement({ 104 | Key? key, 105 | required this.value, 106 | required this.child, 107 | }) : super(key: key); 108 | 109 | @override 110 | Widget build(BuildContext context) { 111 | return child; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:hydrated_bloc/hydrated_bloc.dart'; 5 | import 'package:math_training/core/scores/bloc/scored_bloc.dart'; 6 | import 'package:math_training/core/settings/cubit/app_settings_cubit.dart'; 7 | import 'package:math_training/features/home/view/home_page.dart'; 8 | import 'package:math_training/widgets/theme.dart'; 9 | 10 | import 'package:path_provider/path_provider.dart'; 11 | 12 | void main() async { 13 | WidgetsFlutterBinding.ensureInitialized(); 14 | HydratedBloc.storage = await HydratedStorage.build( 15 | storageDirectory: kIsWeb 16 | ? HydratedStorage.webStorageDirectory 17 | : await getTemporaryDirectory(), 18 | ); 19 | runApp(MyApp()); 20 | } 21 | 22 | class MyApp extends StatelessWidget { 23 | const MyApp({Key? key}) : super(key: key); 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return MultiBlocProvider( 28 | providers: [ 29 | BlocProvider(create: (context) => AppSettingsCubit()), 30 | BlocProvider(create: (context) => ScoresCubit()), 31 | ], 32 | child: MaterialApp( 33 | title: 'Math Training', 34 | theme: materialThemeData, 35 | home: MainPage(), 36 | )); 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /lib/widgets/game_input_scaffold.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:math_training/widgets/theme.dart'; 4 | 5 | class InputTextProcessor { 6 | final int maxLength; 7 | final RegExp _validator = new RegExp(r"[0-9]"); 8 | final void Function(String) onChange; 9 | 10 | String value = ""; 11 | 12 | InputTextProcessor({ 13 | required this.maxLength, 14 | required this.onChange, 15 | }); 16 | 17 | bool validateInput(String s) { 18 | return _validator.hasMatch(s); 19 | } 20 | 21 | void addIfValid(String s){ 22 | if (!validateInput(s)) return; 23 | add(s); 24 | } 25 | 26 | void add(String s) { 27 | if (s == "0" && value == "0") return; 28 | if (value.length >= maxLength) return; 29 | if (value == "0") value = ""; 30 | value += s; 31 | onChange(value); 32 | } 33 | 34 | void backspace() { 35 | if (value.length <= 1) { 36 | value = ""; 37 | } else { 38 | value = value.substring(0, value.length - 1); 39 | } 40 | onChange(value); 41 | } 42 | 43 | void clear(){ 44 | value = ""; 45 | onChange(value); 46 | } 47 | } 48 | 49 | class GameInputScaffold extends StatefulWidget { 50 | final Widget body; 51 | final PreferredSizeWidget appBar; 52 | final bool numericInputEnabled; 53 | final void Function(String) onChange; 54 | final void Function(String) onApply; 55 | 56 | const GameInputScaffold({ 57 | Key? key, 58 | this.numericInputEnabled = true, 59 | required this.onChange, 60 | required this.onApply, 61 | required this.body, 62 | required this.appBar 63 | }) : super(key: key); 64 | 65 | @override 66 | _GameInputScaffoldState createState() => _GameInputScaffoldState(); 67 | } 68 | 69 | class _GameInputScaffoldState extends State { 70 | static const int maxNumberLength = 7; 71 | bool focused = false; 72 | late FocusNode focusNode; 73 | late InputTextProcessor inputText; 74 | 75 | @override 76 | void initState() { 77 | super.initState(); 78 | inputText = new InputTextProcessor( 79 | maxLength: maxNumberLength, 80 | onChange: widget.onChange, 81 | ); 82 | focusNode = new FocusNode(); 83 | } 84 | 85 | void _apply(){ 86 | widget.onApply(inputText.value); 87 | inputText.clear(); 88 | } 89 | 90 | void _processKeyboardEvent(KeyEvent event) { 91 | if (event is KeyDownEvent) { 92 | if (event.logicalKey == LogicalKeyboardKey.enter || event.logicalKey == LogicalKeyboardKey.numpadEnter) { 93 | _apply(); 94 | return; 95 | } else if (event.logicalKey == LogicalKeyboardKey.backspace) { 96 | inputText.backspace(); 97 | return; 98 | } 99 | } 100 | 101 | var character = event.character; 102 | 103 | if (character == null) return; 104 | inputText.addIfValid(character); 105 | } 106 | 107 | @override 108 | Widget build(BuildContext context) { 109 | return KeyboardListener( 110 | focusNode: focusNode, 111 | autofocus: true, 112 | onKeyEvent: _processKeyboardEvent, 113 | child: Scaffold( 114 | appBar: widget.appBar, 115 | body: Column( 116 | mainAxisAlignment: MainAxisAlignment.center, 117 | mainAxisSize: MainAxisSize.max, 118 | crossAxisAlignment: CrossAxisAlignment.stretch, 119 | children: [ 120 | Flexible( 121 | flex: 4, 122 | child: widget.body, 123 | ), 124 | Flexible( 125 | flex: 3, 126 | child: NumericKeyboard( 127 | numericInputEnabled: widget.numericInputEnabled, 128 | onBackspace: inputText.backspace, 129 | onEnter: _apply, 130 | onInput: inputText.add, 131 | ), 132 | ), 133 | ], 134 | ), 135 | ), 136 | ); 137 | } 138 | } 139 | 140 | class NumericKeyboard extends StatelessWidget { 141 | 142 | final bool numericInputEnabled; 143 | final bool enabled; 144 | final void Function() onBackspace; 145 | final void Function(String) onInput; 146 | final void Function() onEnter; 147 | 148 | const NumericKeyboard({ 149 | Key? key, 150 | this.numericInputEnabled = true, 151 | this.enabled = true, 152 | required this.onBackspace, 153 | required this.onInput, 154 | required this.onEnter, 155 | }) : super(key: key); 156 | 157 | @override 158 | Widget build(BuildContext context) { 159 | var theme = Theme.of(context).extension()!.data; 160 | return Container( 161 | decoration: BoxDecoration( 162 | gradient: LinearGradient( 163 | begin: Alignment.topCenter, 164 | end: Alignment(0.0, 1.0), 165 | colors: [theme.primaryColor, theme.secondaryColor], 166 | ), 167 | ), 168 | child: Column( 169 | mainAxisSize: MainAxisSize.max, 170 | mainAxisAlignment: MainAxisAlignment.spaceAround, 171 | crossAxisAlignment: CrossAxisAlignment.stretch, 172 | children: [ 173 | Flexible( 174 | child: Row( 175 | children: [ 176 | _KeyboardButton( 177 | text: "1", 178 | onPress: () => onInput("1"), 179 | enabled: numericInputEnabled && enabled, 180 | ), 181 | _KeyboardButton( 182 | text: "2", 183 | onPress: () => onInput("2"), 184 | enabled: numericInputEnabled && enabled, 185 | ), 186 | _KeyboardButton( 187 | text: "3", 188 | onPress: () => onInput("3"), 189 | enabled: numericInputEnabled && enabled, 190 | ), 191 | ], 192 | ), 193 | ), 194 | Flexible( 195 | child: Row( 196 | children: [ 197 | _KeyboardButton( 198 | text: "4", 199 | onPress: () => onInput("4"), 200 | enabled: numericInputEnabled && enabled, 201 | ), 202 | _KeyboardButton( 203 | text: "5", 204 | onPress: () => onInput("5"), 205 | enabled: numericInputEnabled && enabled, 206 | ), 207 | _KeyboardButton( 208 | text: "6", 209 | onPress: () => onInput("6"), 210 | enabled: numericInputEnabled && enabled, 211 | ), 212 | ], 213 | ), 214 | ), 215 | Flexible( 216 | child: Row( 217 | children: [ 218 | _KeyboardButton( 219 | text: "7", 220 | onPress: () => onInput("7"), 221 | enabled: numericInputEnabled && enabled, 222 | ), 223 | _KeyboardButton( 224 | text: "8", 225 | onPress: () => onInput("8"), 226 | enabled: numericInputEnabled && enabled, 227 | ), 228 | _KeyboardButton( 229 | text: "9", 230 | onPress: () => onInput("9"), 231 | enabled: numericInputEnabled && enabled, 232 | ), 233 | ], 234 | ), 235 | ), 236 | Flexible( 237 | child: Row( 238 | children: [ 239 | _KeyboardButton( 240 | text: "<|", 241 | onPress: onBackspace, 242 | enabled: numericInputEnabled && enabled, 243 | ), 244 | _KeyboardButton( 245 | text: "0", 246 | onPress: () => onInput("0"), 247 | enabled: numericInputEnabled && enabled, 248 | ), 249 | _KeyboardButton( 250 | text: "Ok", 251 | onPress: onEnter, 252 | enabled: enabled, 253 | ), 254 | ], 255 | ), 256 | ), 257 | ], 258 | ), 259 | ); 260 | } 261 | } 262 | 263 | class _KeyboardButton extends StatefulWidget { 264 | final String text; 265 | final Function() onPress; 266 | final bool enabled; 267 | 268 | const _KeyboardButton({ 269 | Key? key, 270 | required this.text, 271 | required this.onPress, 272 | this.enabled = true, 273 | }) : super(key: key); 274 | 275 | @override 276 | State<_KeyboardButton> createState() => _KeyboardButtonState(); 277 | } 278 | 279 | class _KeyboardButtonState extends State<_KeyboardButton> { 280 | bool pressed = false; 281 | 282 | @override 283 | Widget build(BuildContext context) { 284 | return Flexible( 285 | child: GestureDetector( 286 | onTapDown: (_) => _onButtonDown(), 287 | onTapUp: (_) => _onButtonUp(), 288 | onTapCancel: () => _onButtonUp(), 289 | child: Container( 290 | decoration: BoxDecoration( 291 | color: pressed ? Colors.black12 : null, 292 | border: Border.all(color: Colors.white60), 293 | ), 294 | child: Center( 295 | child: FittedBox( 296 | fit: BoxFit.fitHeight, 297 | child: Text( 298 | widget.text, 299 | style: TextStyle(color: widget.enabled ? Colors.white60 : Colors.white24, fontSize: 69), 300 | ), 301 | ), 302 | ), 303 | ), 304 | )); 305 | } 306 | 307 | void _onButtonDown() { 308 | if (!widget.enabled) return; 309 | 310 | widget.onPress(); 311 | 312 | setState(() { 313 | pressed = true; 314 | }); 315 | } 316 | 317 | void _onButtonUp() { 318 | if (!widget.enabled) return; 319 | 320 | setState(() { 321 | pressed = false; 322 | }); 323 | } 324 | 325 | @override 326 | void didUpdateWidget(covariant _KeyboardButton oldWidget) { 327 | super.didUpdateWidget(oldWidget); 328 | 329 | if (!widget.enabled) pressed = false; 330 | } 331 | } 332 | -------------------------------------------------------------------------------- /lib/widgets/theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | part 'theme.freezed.dart'; 5 | 6 | const Color primaryColor = Color(0xFFC56C35); 7 | const Color secondaryColor = Color(0xFF8B0145); 8 | const Color textOnWhiteColor = Color(0xFF3D1220); 9 | const Color mistakeHighlightColor = Color(0xFFD45562); 10 | 11 | const MaterialColor primaryColorSwatch = MaterialColor(0xFFC56C35, { 12 | 50: Color(0xFFF8EDE7), 13 | 100: Color(0xFFEED3C2), 14 | 200: Color(0xFFE2B69A), 15 | 300: Color(0xFFD69872), 16 | 400: Color(0xFFCE8253), 17 | 500: Color(0xFFC56C35), 18 | 600: Color(0xFFBF6430), 19 | 700: Color(0xFFB85928), 20 | 800: Color(0xFFB04F22), 21 | 900: Color(0xFFA33D16), 22 | }); 23 | 24 | ThemeData materialThemeData = ThemeData( 25 | fontFamily: "JetBrainsMono", 26 | primarySwatch: primaryColorSwatch, 27 | extensions: [ 28 | AppTheme(customThemeData) 29 | ], 30 | ); 31 | 32 | AppThemeData customThemeData = AppThemeData( 33 | primaryColor: primaryColor, 34 | secondaryColor: secondaryColor, 35 | textOnWhiteColor: textOnWhiteColor, 36 | mistakeHighlightColor: mistakeHighlightColor, 37 | cardText: TextStyle( 38 | fontSize: 18, 39 | ), 40 | cardTextHighlighted: TextStyle( 41 | fontSize: 18, 42 | color: primaryColor, 43 | fontWeight: FontWeight.bold, 44 | ), 45 | 46 | homePageMainStatText: new TextStyle( 47 | fontSize: 40, 48 | fontWeight: FontWeight.bold, 49 | color: Colors.white, 50 | ), 51 | homePageStatText: new TextStyle( 52 | fontSize: 20, 53 | fontWeight: FontWeight.bold, 54 | color: Colors.white, 55 | ), 56 | cardTitleText: new TextStyle( 57 | fontSize: 20, 58 | fontWeight: FontWeight.bold, 59 | color: Colors.white, 60 | ), 61 | ); 62 | 63 | class AppTheme extends ThemeExtension { 64 | final AppThemeData data; 65 | 66 | AppTheme(this.data); 67 | 68 | @override 69 | ThemeExtension copyWith({AppThemeData? data}) { 70 | return AppTheme(data ?? this.data); 71 | } 72 | 73 | @override 74 | ThemeExtension lerp(covariant ThemeExtension? other, double t) { 75 | throw UnimplementedError(); 76 | } 77 | 78 | } 79 | 80 | @freezed 81 | abstract class AppThemeData with _$AppThemeData { 82 | const factory AppThemeData({ 83 | required Color primaryColor, 84 | required Color secondaryColor, 85 | required Color textOnWhiteColor, 86 | required Color mistakeHighlightColor, 87 | required TextStyle cardText, 88 | required TextStyle cardTextHighlighted, 89 | required TextStyle homePageStatText, 90 | required TextStyle homePageMainStatText, 91 | required TextStyle cardTitleText, 92 | }) = _AppThemeData; 93 | } 94 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | _fe_analyzer_shared: 5 | dependency: transitive 6 | description: 7 | name: _fe_analyzer_shared 8 | sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "61.0.0" 12 | analyzer: 13 | dependency: transitive 14 | description: 15 | name: analyzer 16 | sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "5.13.0" 20 | archive: 21 | dependency: transitive 22 | description: 23 | name: archive 24 | sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "3.3.7" 28 | args: 29 | dependency: transitive 30 | description: 31 | name: args 32 | sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "2.4.2" 36 | async: 37 | dependency: transitive 38 | description: 39 | name: async 40 | sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "2.10.0" 44 | bloc: 45 | dependency: "direct main" 46 | description: 47 | name: bloc 48 | sha256: "6f1b87b6eca9041d5672b6e29273cd1594db48ebb66fd2471066e9f3c3a516bd" 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "7.2.1" 52 | boolean_selector: 53 | dependency: transitive 54 | description: 55 | name: boolean_selector 56 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "2.1.1" 60 | build: 61 | dependency: transitive 62 | description: 63 | name: build 64 | sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "2.3.1" 68 | build_config: 69 | dependency: transitive 70 | description: 71 | name: build_config 72 | sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "1.1.1" 76 | build_daemon: 77 | dependency: transitive 78 | description: 79 | name: build_daemon 80 | sha256: "757153e5d9cd88253cb13f28c2fb55a537dc31fefd98137549895b5beb7c6169" 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "3.1.1" 84 | build_resolvers: 85 | dependency: transitive 86 | description: 87 | name: build_resolvers 88 | sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" 89 | url: "https://pub.dev" 90 | source: hosted 91 | version: "2.2.1" 92 | build_runner: 93 | dependency: "direct dev" 94 | description: 95 | name: build_runner 96 | sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 97 | url: "https://pub.dev" 98 | source: hosted 99 | version: "2.3.3" 100 | build_runner_core: 101 | dependency: transitive 102 | description: 103 | name: build_runner_core 104 | sha256: "0671ad4162ed510b70d0eb4ad6354c249f8429cab4ae7a4cec86bbc2886eb76e" 105 | url: "https://pub.dev" 106 | source: hosted 107 | version: "7.2.7+1" 108 | built_collection: 109 | dependency: transitive 110 | description: 111 | name: built_collection 112 | sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" 113 | url: "https://pub.dev" 114 | source: hosted 115 | version: "5.1.1" 116 | built_value: 117 | dependency: transitive 118 | description: 119 | name: built_value 120 | sha256: "598a2a682e2a7a90f08ba39c0aaa9374c5112340f0a2e275f61b59389543d166" 121 | url: "https://pub.dev" 122 | source: hosted 123 | version: "8.6.1" 124 | characters: 125 | dependency: transitive 126 | description: 127 | name: characters 128 | sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c 129 | url: "https://pub.dev" 130 | source: hosted 131 | version: "1.2.1" 132 | checked_yaml: 133 | dependency: transitive 134 | description: 135 | name: checked_yaml 136 | sha256: dd007e4fb8270916820a0d66e24f619266b60773cddd082c6439341645af2659 137 | url: "https://pub.dev" 138 | source: hosted 139 | version: "2.0.1" 140 | clock: 141 | dependency: transitive 142 | description: 143 | name: clock 144 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 145 | url: "https://pub.dev" 146 | source: hosted 147 | version: "1.1.1" 148 | code_builder: 149 | dependency: transitive 150 | description: 151 | name: code_builder 152 | sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189" 153 | url: "https://pub.dev" 154 | source: hosted 155 | version: "4.5.0" 156 | collection: 157 | dependency: transitive 158 | description: 159 | name: collection 160 | sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 161 | url: "https://pub.dev" 162 | source: hosted 163 | version: "1.17.0" 164 | convert: 165 | dependency: transitive 166 | description: 167 | name: convert 168 | sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" 169 | url: "https://pub.dev" 170 | source: hosted 171 | version: "3.1.1" 172 | crypto: 173 | dependency: transitive 174 | description: 175 | name: crypto 176 | sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab 177 | url: "https://pub.dev" 178 | source: hosted 179 | version: "3.0.3" 180 | cupertino_icons: 181 | dependency: "direct main" 182 | description: 183 | name: cupertino_icons 184 | sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be 185 | url: "https://pub.dev" 186 | source: hosted 187 | version: "1.0.5" 188 | dart_style: 189 | dependency: transitive 190 | description: 191 | name: dart_style 192 | sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" 193 | url: "https://pub.dev" 194 | source: hosted 195 | version: "2.3.2" 196 | equatable: 197 | dependency: "direct main" 198 | description: 199 | name: equatable 200 | sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 201 | url: "https://pub.dev" 202 | source: hosted 203 | version: "2.0.5" 204 | fake_async: 205 | dependency: transitive 206 | description: 207 | name: fake_async 208 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 209 | url: "https://pub.dev" 210 | source: hosted 211 | version: "1.3.1" 212 | ffi: 213 | dependency: transitive 214 | description: 215 | name: ffi 216 | sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 217 | url: "https://pub.dev" 218 | source: hosted 219 | version: "2.0.2" 220 | file: 221 | dependency: transitive 222 | description: 223 | name: file 224 | sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" 225 | url: "https://pub.dev" 226 | source: hosted 227 | version: "6.1.4" 228 | fixnum: 229 | dependency: transitive 230 | description: 231 | name: fixnum 232 | sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" 233 | url: "https://pub.dev" 234 | source: hosted 235 | version: "1.1.0" 236 | flutter: 237 | dependency: "direct main" 238 | description: flutter 239 | source: sdk 240 | version: "0.0.0" 241 | flutter_bloc: 242 | dependency: "direct main" 243 | description: 244 | name: flutter_bloc 245 | sha256: cdd1351ced09eeb46cfa7946e095b7679344af927415ca9cd972928fa6d5b23f 246 | url: "https://pub.dev" 247 | source: hosted 248 | version: "7.3.3" 249 | flutter_launcher_icons: 250 | dependency: "direct dev" 251 | description: 252 | name: flutter_launcher_icons 253 | sha256: "559c600f056e7c704bd843723c21e01b5fba47e8824bd02422165bcc02a5de1d" 254 | url: "https://pub.dev" 255 | source: hosted 256 | version: "0.9.3" 257 | flutter_lints: 258 | dependency: "direct dev" 259 | description: 260 | name: flutter_lints 261 | sha256: b543301ad291598523947dc534aaddc5aaad597b709d2426d3a0e0d44c5cb493 262 | url: "https://pub.dev" 263 | source: hosted 264 | version: "1.0.4" 265 | flutter_test: 266 | dependency: "direct dev" 267 | description: flutter 268 | source: sdk 269 | version: "0.0.0" 270 | flutter_web_plugins: 271 | dependency: transitive 272 | description: flutter 273 | source: sdk 274 | version: "0.0.0" 275 | freezed: 276 | dependency: "direct main" 277 | description: 278 | name: freezed 279 | sha256: "2df89855fe181baae3b6d714dc3c4317acf4fccd495a6f36e5e00f24144c6c3b" 280 | url: "https://pub.dev" 281 | source: hosted 282 | version: "2.4.1" 283 | freezed_annotation: 284 | dependency: transitive 285 | description: 286 | name: freezed_annotation 287 | sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d 288 | url: "https://pub.dev" 289 | source: hosted 290 | version: "2.4.1" 291 | frontend_server_client: 292 | dependency: transitive 293 | description: 294 | name: frontend_server_client 295 | sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" 296 | url: "https://pub.dev" 297 | source: hosted 298 | version: "3.2.0" 299 | glob: 300 | dependency: transitive 301 | description: 302 | name: glob 303 | sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" 304 | url: "https://pub.dev" 305 | source: hosted 306 | version: "2.1.2" 307 | graphs: 308 | dependency: transitive 309 | description: 310 | name: graphs 311 | sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 312 | url: "https://pub.dev" 313 | source: hosted 314 | version: "2.3.1" 315 | hive: 316 | dependency: transitive 317 | description: 318 | name: hive 319 | sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" 320 | url: "https://pub.dev" 321 | source: hosted 322 | version: "2.2.3" 323 | http_multi_server: 324 | dependency: transitive 325 | description: 326 | name: http_multi_server 327 | sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" 328 | url: "https://pub.dev" 329 | source: hosted 330 | version: "3.2.1" 331 | http_parser: 332 | dependency: transitive 333 | description: 334 | name: http_parser 335 | sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" 336 | url: "https://pub.dev" 337 | source: hosted 338 | version: "4.0.2" 339 | hydrated_bloc: 340 | dependency: "direct main" 341 | description: 342 | name: hydrated_bloc 343 | sha256: "1e8a67ff40604d30ad2b9830fd0a509fd5bdd8992ad3b0e38ca358eb1f58f3e6" 344 | url: "https://pub.dev" 345 | source: hosted 346 | version: "7.1.0" 347 | image: 348 | dependency: transitive 349 | description: 350 | name: image 351 | sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6" 352 | url: "https://pub.dev" 353 | source: hosted 354 | version: "3.3.0" 355 | io: 356 | dependency: transitive 357 | description: 358 | name: io 359 | sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" 360 | url: "https://pub.dev" 361 | source: hosted 362 | version: "1.0.4" 363 | js: 364 | dependency: transitive 365 | description: 366 | name: js 367 | sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" 368 | url: "https://pub.dev" 369 | source: hosted 370 | version: "0.6.5" 371 | json_annotation: 372 | dependency: "direct main" 373 | description: 374 | name: json_annotation 375 | sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 376 | url: "https://pub.dev" 377 | source: hosted 378 | version: "4.8.1" 379 | json_serializable: 380 | dependency: "direct dev" 381 | description: 382 | name: json_serializable 383 | sha256: "43793352f90efa5d8b251893a63d767b2f7c833120e3cc02adad55eefec04dc7" 384 | url: "https://pub.dev" 385 | source: hosted 386 | version: "6.6.2" 387 | lints: 388 | dependency: transitive 389 | description: 390 | name: lints 391 | sha256: a2c3d198cb5ea2e179926622d433331d8b58374ab8f29cdda6e863bd62fd369c 392 | url: "https://pub.dev" 393 | source: hosted 394 | version: "1.0.1" 395 | logging: 396 | dependency: transitive 397 | description: 398 | name: logging 399 | sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" 400 | url: "https://pub.dev" 401 | source: hosted 402 | version: "1.2.0" 403 | matcher: 404 | dependency: transitive 405 | description: 406 | name: matcher 407 | sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" 408 | url: "https://pub.dev" 409 | source: hosted 410 | version: "0.12.13" 411 | material_color_utilities: 412 | dependency: transitive 413 | description: 414 | name: material_color_utilities 415 | sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 416 | url: "https://pub.dev" 417 | source: hosted 418 | version: "0.2.0" 419 | meta: 420 | dependency: transitive 421 | description: 422 | name: meta 423 | sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" 424 | url: "https://pub.dev" 425 | source: hosted 426 | version: "1.8.0" 427 | mime: 428 | dependency: transitive 429 | description: 430 | name: mime 431 | sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e 432 | url: "https://pub.dev" 433 | source: hosted 434 | version: "1.0.4" 435 | nested: 436 | dependency: transitive 437 | description: 438 | name: nested 439 | sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" 440 | url: "https://pub.dev" 441 | source: hosted 442 | version: "1.0.0" 443 | package_config: 444 | dependency: transitive 445 | description: 446 | name: package_config 447 | sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" 448 | url: "https://pub.dev" 449 | source: hosted 450 | version: "2.1.0" 451 | path: 452 | dependency: transitive 453 | description: 454 | name: path 455 | sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b 456 | url: "https://pub.dev" 457 | source: hosted 458 | version: "1.8.2" 459 | path_provider: 460 | dependency: "direct main" 461 | description: 462 | name: path_provider 463 | sha256: "909b84830485dbcd0308edf6f7368bc8fd76afa26a270420f34cabea2a6467a0" 464 | url: "https://pub.dev" 465 | source: hosted 466 | version: "2.1.0" 467 | path_provider_android: 468 | dependency: transitive 469 | description: 470 | name: path_provider_android 471 | sha256: "5d44fc3314d969b84816b569070d7ace0f1dea04bd94a83f74c4829615d22ad8" 472 | url: "https://pub.dev" 473 | source: hosted 474 | version: "2.1.0" 475 | path_provider_foundation: 476 | dependency: transitive 477 | description: 478 | name: path_provider_foundation 479 | sha256: "1b744d3d774e5a879bb76d6cd1ecee2ba2c6960c03b1020cd35212f6aa267ac5" 480 | url: "https://pub.dev" 481 | source: hosted 482 | version: "2.3.0" 483 | path_provider_linux: 484 | dependency: transitive 485 | description: 486 | name: path_provider_linux 487 | sha256: ba2b77f0c52a33db09fc8caf85b12df691bf28d983e84cf87ff6d693cfa007b3 488 | url: "https://pub.dev" 489 | source: hosted 490 | version: "2.2.0" 491 | path_provider_platform_interface: 492 | dependency: transitive 493 | description: 494 | name: path_provider_platform_interface 495 | sha256: bced5679c7df11190e1ddc35f3222c858f328fff85c3942e46e7f5589bf9eb84 496 | url: "https://pub.dev" 497 | source: hosted 498 | version: "2.1.0" 499 | path_provider_windows: 500 | dependency: transitive 501 | description: 502 | name: path_provider_windows 503 | sha256: ee0e0d164516b90ae1f970bdf29f726f1aa730d7cfc449ecc74c495378b705da 504 | url: "https://pub.dev" 505 | source: hosted 506 | version: "2.2.0" 507 | petitparser: 508 | dependency: transitive 509 | description: 510 | name: petitparser 511 | sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" 512 | url: "https://pub.dev" 513 | source: hosted 514 | version: "5.1.0" 515 | platform: 516 | dependency: transitive 517 | description: 518 | name: platform 519 | sha256: "57c07bf82207aee366dfaa3867b3164e4f03a238a461a11b0e8a3a510d51203d" 520 | url: "https://pub.dev" 521 | source: hosted 522 | version: "3.1.1" 523 | plugin_platform_interface: 524 | dependency: transitive 525 | description: 526 | name: plugin_platform_interface 527 | sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd" 528 | url: "https://pub.dev" 529 | source: hosted 530 | version: "2.1.5" 531 | pointycastle: 532 | dependency: transitive 533 | description: 534 | name: pointycastle 535 | sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" 536 | url: "https://pub.dev" 537 | source: hosted 538 | version: "3.7.3" 539 | pool: 540 | dependency: transitive 541 | description: 542 | name: pool 543 | sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" 544 | url: "https://pub.dev" 545 | source: hosted 546 | version: "1.5.1" 547 | provider: 548 | dependency: transitive 549 | description: 550 | name: provider 551 | sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f 552 | url: "https://pub.dev" 553 | source: hosted 554 | version: "6.0.5" 555 | pub_semver: 556 | dependency: transitive 557 | description: 558 | name: pub_semver 559 | sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" 560 | url: "https://pub.dev" 561 | source: hosted 562 | version: "2.1.4" 563 | pubspec_parse: 564 | dependency: transitive 565 | description: 566 | name: pubspec_parse 567 | sha256: "0e01f805457ef610ccaf8d18067596afc34107a27149778b06b2083edbc140c1" 568 | url: "https://pub.dev" 569 | source: hosted 570 | version: "1.1.0" 571 | shelf: 572 | dependency: transitive 573 | description: 574 | name: shelf 575 | sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 576 | url: "https://pub.dev" 577 | source: hosted 578 | version: "1.4.1" 579 | shelf_web_socket: 580 | dependency: transitive 581 | description: 582 | name: shelf_web_socket 583 | sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" 584 | url: "https://pub.dev" 585 | source: hosted 586 | version: "1.0.4" 587 | sky_engine: 588 | dependency: transitive 589 | description: flutter 590 | source: sdk 591 | version: "0.0.99" 592 | source_gen: 593 | dependency: transitive 594 | description: 595 | name: source_gen 596 | sha256: "373f96cf5a8744bc9816c1ff41cf5391bbdbe3d7a96fe98c622b6738a8a7bd33" 597 | url: "https://pub.dev" 598 | source: hosted 599 | version: "1.3.2" 600 | source_helper: 601 | dependency: transitive 602 | description: 603 | name: source_helper 604 | sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" 605 | url: "https://pub.dev" 606 | source: hosted 607 | version: "1.3.4" 608 | source_span: 609 | dependency: transitive 610 | description: 611 | name: source_span 612 | sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 613 | url: "https://pub.dev" 614 | source: hosted 615 | version: "1.9.1" 616 | stack_trace: 617 | dependency: transitive 618 | description: 619 | name: stack_trace 620 | sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 621 | url: "https://pub.dev" 622 | source: hosted 623 | version: "1.11.0" 624 | stream_channel: 625 | dependency: transitive 626 | description: 627 | name: stream_channel 628 | sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" 629 | url: "https://pub.dev" 630 | source: hosted 631 | version: "2.1.1" 632 | stream_transform: 633 | dependency: transitive 634 | description: 635 | name: stream_transform 636 | sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" 637 | url: "https://pub.dev" 638 | source: hosted 639 | version: "2.1.0" 640 | string_scanner: 641 | dependency: transitive 642 | description: 643 | name: string_scanner 644 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 645 | url: "https://pub.dev" 646 | source: hosted 647 | version: "1.2.0" 648 | synchronized: 649 | dependency: transitive 650 | description: 651 | name: synchronized 652 | sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" 653 | url: "https://pub.dev" 654 | source: hosted 655 | version: "3.1.0" 656 | term_glyph: 657 | dependency: transitive 658 | description: 659 | name: term_glyph 660 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 661 | url: "https://pub.dev" 662 | source: hosted 663 | version: "1.2.1" 664 | test_api: 665 | dependency: transitive 666 | description: 667 | name: test_api 668 | sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 669 | url: "https://pub.dev" 670 | source: hosted 671 | version: "0.4.16" 672 | timing: 673 | dependency: transitive 674 | description: 675 | name: timing 676 | sha256: c386d07d7f5efc613479a7c4d9d64b03710b03cfaa7e8ad5f2bfb295a1f0dfad 677 | url: "https://pub.dev" 678 | source: hosted 679 | version: "1.0.0" 680 | typed_data: 681 | dependency: transitive 682 | description: 683 | name: typed_data 684 | sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c 685 | url: "https://pub.dev" 686 | source: hosted 687 | version: "1.3.2" 688 | url_launcher: 689 | dependency: "direct main" 690 | description: 691 | name: url_launcher 692 | sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3 693 | url: "https://pub.dev" 694 | source: hosted 695 | version: "6.1.11" 696 | url_launcher_android: 697 | dependency: transitive 698 | description: 699 | name: url_launcher_android 700 | sha256: "3dd2388cc0c42912eee04434531a26a82512b9cb1827e0214430c9bcbddfe025" 701 | url: "https://pub.dev" 702 | source: hosted 703 | version: "6.0.38" 704 | url_launcher_ios: 705 | dependency: transitive 706 | description: 707 | name: url_launcher_ios 708 | sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" 709 | url: "https://pub.dev" 710 | source: hosted 711 | version: "6.1.4" 712 | url_launcher_linux: 713 | dependency: transitive 714 | description: 715 | name: url_launcher_linux 716 | sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5" 717 | url: "https://pub.dev" 718 | source: hosted 719 | version: "3.0.5" 720 | url_launcher_macos: 721 | dependency: transitive 722 | description: 723 | name: url_launcher_macos 724 | sha256: "1c4fdc0bfea61a70792ce97157e5cc17260f61abbe4f39354513f39ec6fd73b1" 725 | url: "https://pub.dev" 726 | source: hosted 727 | version: "3.0.6" 728 | url_launcher_platform_interface: 729 | dependency: transitive 730 | description: 731 | name: url_launcher_platform_interface 732 | sha256: bfdfa402f1f3298637d71ca8ecfe840b4696698213d5346e9d12d4ab647ee2ea 733 | url: "https://pub.dev" 734 | source: hosted 735 | version: "2.1.3" 736 | url_launcher_web: 737 | dependency: transitive 738 | description: 739 | name: url_launcher_web 740 | sha256: cc26720eefe98c1b71d85f9dc7ef0cada5132617046369d9dc296b3ecaa5cbb4 741 | url: "https://pub.dev" 742 | source: hosted 743 | version: "2.0.18" 744 | url_launcher_windows: 745 | dependency: transitive 746 | description: 747 | name: url_launcher_windows 748 | sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422" 749 | url: "https://pub.dev" 750 | source: hosted 751 | version: "3.0.7" 752 | vector_math: 753 | dependency: transitive 754 | description: 755 | name: vector_math 756 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 757 | url: "https://pub.dev" 758 | source: hosted 759 | version: "2.1.4" 760 | watcher: 761 | dependency: transitive 762 | description: 763 | name: watcher 764 | sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" 765 | url: "https://pub.dev" 766 | source: hosted 767 | version: "1.0.2" 768 | web_socket_channel: 769 | dependency: transitive 770 | description: 771 | name: web_socket_channel 772 | sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b 773 | url: "https://pub.dev" 774 | source: hosted 775 | version: "2.4.0" 776 | win32: 777 | dependency: transitive 778 | description: 779 | name: win32 780 | sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" 781 | url: "https://pub.dev" 782 | source: hosted 783 | version: "4.1.4" 784 | xdg_directories: 785 | dependency: transitive 786 | description: 787 | name: xdg_directories 788 | sha256: f0c26453a2d47aa4c2570c6a033246a3fc62da2fe23c7ffdd0a7495086dc0247 789 | url: "https://pub.dev" 790 | source: hosted 791 | version: "1.0.2" 792 | xml: 793 | dependency: transitive 794 | description: 795 | name: xml 796 | sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" 797 | url: "https://pub.dev" 798 | source: hosted 799 | version: "6.2.2" 800 | yaml: 801 | dependency: transitive 802 | description: 803 | name: yaml 804 | sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" 805 | url: "https://pub.dev" 806 | source: hosted 807 | version: "3.1.2" 808 | sdks: 809 | dart: ">=2.19.0 <3.0.0" 810 | flutter: ">=3.3.0" 811 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: math_training 2 | description: A new Flutter application. 3 | 4 | publish_to: 'none' 5 | 6 | version: 1.0.0+1 7 | 8 | environment: 9 | sdk: ">=2.17.0 <3.0.0" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | 15 | bloc: ^7.0.0 16 | hydrated_bloc: ^7.0.0 17 | flutter_bloc: ^7.0.0 18 | equatable: ^2.0.3 19 | json_annotation: ^4.8.1 20 | cupertino_icons: ^1.0.2 21 | path_provider: ^2.0.7 22 | url_launcher: ^6.1.3 23 | freezed: ^2.1.0 24 | 25 | dev_dependencies: 26 | flutter_test: 27 | sdk: flutter 28 | 29 | json_serializable: ^6.0.0 30 | build_runner: ^2.0.6 31 | flutter_lints: ^1.0.0 32 | flutter_launcher_icons: ^0.9.2 33 | 34 | flutter_icons: 35 | android: "ic_launcher" 36 | ios: false 37 | image_path: "assets/icon.png" 38 | 39 | flutter: 40 | 41 | uses-material-design: true 42 | 43 | fonts: 44 | - family: JetBrainsMono 45 | fonts: 46 | - asset: assets/JetBrainsMono-Light.ttf 47 | - asset: assets/JetBrainsMono-Medium.ttf 48 | weight: 700 -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cifruktus/MathTraining/b4262756f0b3ed3e77b13fdf2b6700d52ecb189b/web/favicon.png -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Math training 33 | 34 | 35 | 36 | 39 | 40 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "math_training", 3 | "short_name": "math_training", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [] 12 | } 13 | --------------------------------------------------------------------------------