├── .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 | 
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 |
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 |
--------------------------------------------------------------------------------