├── metadata ├── en-US │ ├── changelogs │ │ ├── 15.txt │ │ ├── 11.txt │ │ ├── 13.txt │ │ ├── 14.txt │ │ ├── 10.txt │ │ └── 12.txt │ ├── short_description.txt │ ├── images │ │ ├── icon.png │ │ └── phoneScreenshots │ │ │ ├── 1.png │ │ │ └── 2.png │ └── full_description.txt ├── de │ ├── short_description.txt │ └── full_description.txt ├── es │ ├── short_description.txt │ └── full_description.txt ├── it │ ├── short_description.txt │ └── full_description.txt ├── uk │ ├── short_description.txt │ └── full_description.txt └── fr │ ├── short_description.txt │ └── full_description.txt ├── android ├── README.md ├── app │ ├── src │ │ ├── main │ │ │ ├── kotlin │ │ │ │ └── io │ │ │ │ │ └── github │ │ │ │ │ └── mwageringel │ │ │ │ │ └── everest │ │ │ │ │ └── MainActivity.kt │ │ │ ├── res │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle └── build.gradle ├── l10n.yaml ├── .gitmodules ├── .metadata ├── lib ├── l10n │ ├── app_es.arb │ ├── app_it.arb │ ├── app_fr.arb │ ├── app_de.arb │ ├── app_uk.arb │ └── app_en.arb ├── storage.dart ├── expressions.dart ├── game.dart └── main.dart ├── web ├── manifest.json └── index.html ├── .gitignore ├── README.md ├── analysis_options.yaml ├── CHANGELOG.md ├── test ├── expressions_test.dart └── widget_test.dart ├── assets ├── launcher_icon.svg ├── launcher_icon_alt.svg ├── launcher_icon_maskable.svg └── launcher_icon_adaptive.svg ├── Makefile ├── website └── index.html ├── pubspec.yaml ├── pubspec.lock └── COPYING /metadata/en-US/changelogs/15.txt: -------------------------------------------------------------------------------- 1 | - Italian translation 2 | -------------------------------------------------------------------------------- /metadata/de/short_description.txt: -------------------------------------------------------------------------------- 1 | Mathematisches Rätselspiel 2 | -------------------------------------------------------------------------------- /metadata/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | Mathematical puzzle game 2 | -------------------------------------------------------------------------------- /metadata/es/short_description.txt: -------------------------------------------------------------------------------- 1 | Juego de acertijos matemáticos 2 | -------------------------------------------------------------------------------- /metadata/it/short_description.txt: -------------------------------------------------------------------------------- 1 | Un gioco di enigmi matematici 2 | -------------------------------------------------------------------------------- /metadata/uk/short_description.txt: -------------------------------------------------------------------------------- 1 | Математична гра-головоломка 2 | -------------------------------------------------------------------------------- /metadata/en-US/changelogs/11.txt: -------------------------------------------------------------------------------- 1 | - new first level 2 | - new icon 3 | -------------------------------------------------------------------------------- /metadata/en-US/changelogs/13.txt: -------------------------------------------------------------------------------- 1 | - Ukrainian and Spanish translations 2 | -------------------------------------------------------------------------------- /metadata/fr/short_description.txt: -------------------------------------------------------------------------------- 1 | Un jeu de casse-tête mathématique 2 | -------------------------------------------------------------------------------- /android/README.md: -------------------------------------------------------------------------------- 1 | The files in this directory were mostly generated by Flutter. 2 | -------------------------------------------------------------------------------- /metadata/en-US/changelogs/14.txt: -------------------------------------------------------------------------------- 1 | - French translation 2 | - revised icons in the main screen 3 | -------------------------------------------------------------------------------- /l10n.yaml: -------------------------------------------------------------------------------- 1 | arb-dir: lib/l10n 2 | template-arb-file: app_en.arb 3 | output-localization-file: app_localizations.dart -------------------------------------------------------------------------------- /metadata/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwageringel/everest/HEAD/metadata/en-US/images/icon.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/flutter"] 2 | path = vendor/flutter 3 | url = https://github.com/flutter/flutter.git 4 | -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwageringel/everest/HEAD/metadata/en-US/images/phoneScreenshots/1.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwageringel/everest/HEAD/metadata/en-US/images/phoneScreenshots/2.png -------------------------------------------------------------------------------- /metadata/en-US/changelogs/10.txt: -------------------------------------------------------------------------------- 1 | - added automatic scrolling to the next question 2 | - added support for keyboard input 3 | - improved rendering performance 4 | -------------------------------------------------------------------------------- /metadata/en-US/changelogs/12.txt: -------------------------------------------------------------------------------- 1 | - a few additional questions in the first levels 2 | - German translation 3 | - small fixes related to font sizes and navigation 4 | -------------------------------------------------------------------------------- /metadata/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | Discover the rules by solving small puzzles. You will learn about new mathematical ideas. 2 | 3 | No mathematical knowledge is required beyond basic arithmetics. 4 | -------------------------------------------------------------------------------- /metadata/it/full_description.txt: -------------------------------------------------------------------------------- 1 | Scopri le regole risolvendo piccoli puzzle. Imparerai nuove idee matematiche. 2 | 3 | Non è necessaria nessuna conoscenza matematica eccetto l'aritmetica di base. 4 | -------------------------------------------------------------------------------- /metadata/uk/full_description.txt: -------------------------------------------------------------------------------- 1 | Виявляйте правила вирішуючи маленькі головоломки. Ви дізнаєтеся про нові математичні ідеї. 2 | 3 | Ніяких математичних знань не вимагається, лише основна арифметика. 4 | -------------------------------------------------------------------------------- /metadata/es/full_description.txt: -------------------------------------------------------------------------------- 1 | Descubre las reglas resolviendo pequeños acertijos. Aprenderás nuevas ideas matemáticas. 2 | 3 | No es necesario tener más conocimiento matemático que aritmética básica. 4 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/io/github/mwageringel/everest/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.mwageringel.everest 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /metadata/de/full_description.txt: -------------------------------------------------------------------------------- 1 | Erkenn die Regeln, indem du kleine Rätsel löst. Dabei lernst du neue mathematische Konzepte kennen. 2 | 3 | Außer dem kleinen Einmaleins sind dafür keinerlei mathematische Vorkenntnisse nötig. 4 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | android.defaults.buildfeatures.buildconfig=true 5 | android.nonTransitiveRClass=false 6 | android.nonFinalResIds=false 7 | -------------------------------------------------------------------------------- /metadata/fr/full_description.txt: -------------------------------------------------------------------------------- 1 | Découvrez les règles du jeu par vous-même en résolvant des problèmes simples. Vous découvrirez de nouvelles idées mathématiques ! 2 | 3 | Aucune connaissance mathématique au-delà de l'arithmétique de base n'est requise. 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/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.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: c860cba910319332564e1e9d470a17074c1f2dfd 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.9.22' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:8.2.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 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | tasks.register("clean", Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /lib/l10n/app_es.arb: -------------------------------------------------------------------------------- 1 | { 2 | "settings": "Opciones", 3 | "theme": "Tema", 4 | "themeSystem": "Preferido del sistema", 5 | "themeLight": "Claro", 6 | "themeDark": "Oscuro", 7 | "darkThemeBlackBackground": "Fondo negro para el tema oscuro", 8 | "darkThemeBlackBackgroundSubtitle": "Principalmente para pantallas OLED", 9 | "restart": "Reiniciar", 10 | "restartSubtitle": "Mantenga presionado para reiniciar el progreso", 11 | "version": "Versión {label}", 12 | "moreInfo": "Más información en {url}", 13 | "levelTitle": "Nivel {number}", 14 | "endMessage": "¡Enhorabuena!", 15 | "extendedMessage": "wqFVbiBsb2dybyBleHRyYW9yZGluYXJpbyEgSGFzIGV4cGxvcmFkbyBsYSBhcml0bcOpdGljYSBkZSBsb3MgY2FtcG9zIGZpbml0b3MgY29uIDExIHkgMTIxIGVsZW1lbnRvcy4gKMK/Tm90YXN0ZSBhbGdvIHNpbWlsYXIgYSBsb3MgbsO6bWVyb3MgY29tcGxlam9zPyk=", 16 | "exitDialogTitle": "Guardado no disponible.", 17 | "exitDialogContent": "Si sales, todo tu progreso se perderá. ¿Continuar?", 18 | "dialogCancel": "Cancelar", 19 | "dialogOk": "Aceptar" 20 | } 21 | -------------------------------------------------------------------------------- /lib/l10n/app_it.arb: -------------------------------------------------------------------------------- 1 | { 2 | "settings": "Impostazioni", 3 | "theme": "Tema", 4 | "themeSystem": "Preferenze di sistema", 5 | "themeLight": "Chiaro", 6 | "themeDark": "Scuro", 7 | "darkThemeBlackBackground": "Sfondo nero con tema scuro", 8 | "darkThemeBlackBackgroundSubtitle": "Indicato principalmente per schermi OLED", 9 | "restart": "Riavvia", 10 | "restartSubtitle": "Premere a lungo per resettare i progressi salvati", 11 | "version": "Versione {label}", 12 | "moreInfo": "Più informazioni presso {url}", 13 | "levelTitle": "Livello {number}", 14 | "endMessage": "Congratulazioni!", 15 | "extendedMessage": "VW4gdHJhZ3VhcmRvIGluY3JlZGliaWxlISBIYWkgZXNwbG9yYXRvIGwnYXJpdG1ldGljYSBkZWkgY2FtcGkgZmluaXRpIGNvbiAxMSBlIDEyMSBlbGVtZW50aS4gKEhhaSBub3RhdG8gbGEgc29taWdsaWFuemEgY29uIGkgbnVtZXJpIGNvbXBsZXNzaT8p", 16 | "exitDialogTitle": "Salvataggio non disponibile.", 17 | "exitDialogContent": "Uscendo perderai tutti i progressi. Continuare?", 18 | "dialogCancel": "Annulla", 19 | "dialogOk": "OK" 20 | } 21 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Everest", 3 | "short_name": "Everest", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#536dfe", 7 | "theme_color": "#536dfe", 8 | "description": "A mathematical puzzle game.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /lib/l10n/app_fr.arb: -------------------------------------------------------------------------------- 1 | { 2 | "settings": "Paramètres", 3 | "theme": "Thème", 4 | "themeSystem": "Préférence du système", 5 | "themeLight": "Clair", 6 | "themeDark": "Sombre", 7 | "darkThemeBlackBackground": "Arrière-plan noir en thème sombre", 8 | "darkThemeBlackBackgroundSubtitle": "Généralement conçu pour les écrans OLED", 9 | "restart": "Recommencer", 10 | "restartSubtitle": "Maintenez appuyé pour réinitialiser la progression", 11 | "version": "Version {label}", 12 | "moreInfo": "Plus d'informations sur {url}", 13 | "levelTitle": "Niveau {number}", 14 | "endMessage": "Félicitations !", 15 | "extendedMessage": "UXVlbGxlIHLDqXVzc2l0ZSBleGNlcHRpb25uZWxsZSAhIFZvdXMgYXZleiBleHBsb3LDqSBsJ2FyaXRobcOpdGlxdWUgc3VyIGxlcyBjb3JwcyBmaW5pcyDDoCAxMSBldCAxMjEgw6lsw6ltZW50cyAhIChBdmV6LXZvdXMgcmVtYXJxdcOpIHVuZSBzaW1pbGFyaXTDqSBhdmVjIGxlcyBub21icmVzIGNvbXBsZXhlcyA/KQ==", 16 | "exitDialogTitle": "Sauvegarde non disponible", 17 | "exitDialogContent": "Toute la progression sera perdue si vous quittez. Continuer ?", 18 | "dialogCancel": "Annuler", 19 | "dialogOk": "OK" 20 | } 21 | -------------------------------------------------------------------------------- /lib/l10n/app_de.arb: -------------------------------------------------------------------------------- 1 | { 2 | "settings": "Einstellungen", 3 | "theme": "Farbschema", 4 | "themeSystem": "Systempräferenz", 5 | "themeLight": "Hell", 6 | "themeDark": "Dunkel", 7 | "darkThemeBlackBackground": "Schwarzer Hintergrund im dunklen Farbschema", 8 | "darkThemeBlackBackgroundSubtitle": "Hauptsächlich für OLED-Bildschirme gedacht", 9 | "restart": "Neustart", 10 | "restartSubtitle": "Gedrückt halten um Fortschritt zurückzusetzen", 11 | "version": "Version {label}", 12 | "moreInfo": "Mehr Informationen unter {url}", 13 | "levelTitle": "Level {number}", 14 | "endMessage": "Ausgezeichnet!", 15 | "extendedMessage": "SGVydm9ycmFnZW5kZSBMZWlzdHVuZyEgRHUgaGFzdCBkYXMgUmVjaG5lbiBpbiBkZW4gZW5kbGljaGVuIEvDtnJwZXJuIG1pdCAxMSB1bmQgMTIxIEVsZW1lbnRlbiBlcmZvcnNjaHQgdW5kIGdlbWVpc3RlcnQuIChJc3QgZGlyIGVpbmUgw4RobmxpY2hrZWl0IHp1IGtvbXBsZXhlbiBaYWhsZW4gYXVmZ2VmYWxsZW4/KQ==", 16 | "exitDialogTitle": "Speichern nicht verfügbar.", 17 | "exitDialogContent": "Der gesamte Fortschritt geht beim Schließen verloren. Fortfahren?", 18 | "dialogCancel": "Abbrechen", 19 | "dialogOk": "OK" 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /ios/ 2 | /windows/ 3 | /gh-pages/ 4 | *.sage.py 5 | /fonts/ 6 | /website/demo/ 7 | *.png 8 | *.ico 9 | *.apk 10 | *.apk.sha1 11 | /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml 12 | /android/app/src/main/res/values/colors.xml 13 | 14 | # Miscellaneous 15 | *.class 16 | *.log 17 | *.pyc 18 | *.swp 19 | .DS_Store 20 | .atom/ 21 | .buildlog/ 22 | .history 23 | .svn/ 24 | 25 | # IntelliJ related 26 | *.iml 27 | *.ipr 28 | *.iws 29 | .idea/ 30 | 31 | # The .vscode folder contains launch configuration and tasks you configure in 32 | # VS Code which you may wish to be included in version control, so this line 33 | # is commented out by default. 34 | #.vscode/ 35 | 36 | # Flutter/Dart/Pub related 37 | **/doc/api/ 38 | **/ios/Flutter/.last_build_id 39 | .dart_tool/ 40 | .flutter-plugins 41 | .flutter-plugins-dependencies 42 | .packages 43 | .pub-cache/ 44 | .pub/ 45 | /build/ 46 | 47 | # Web related 48 | 49 | # Symbolication related 50 | app.*.symbols 51 | 52 | # Obfuscation related 53 | app.*.map.json 54 | 55 | # Android Studio will place build artifacts here 56 | /android/app/debug 57 | /android/app/profile 58 | /android/app/release 59 | -------------------------------------------------------------------------------- /lib/l10n/app_uk.arb: -------------------------------------------------------------------------------- 1 | { 2 | "settings": "Налаштування", 3 | "theme": "Тема", 4 | "themeSystem": "Системні налаштування", 5 | "themeLight": "Світла", 6 | "themeDark": "Темна", 7 | "darkThemeBlackBackground": "Чорний фон у темній темі", 8 | "darkThemeBlackBackgroundSubtitle": "В основному призначено для ерканів OLED", 9 | "restart": "Перезапустити", 10 | "restartSubtitle": "Довге натискання, щоб скинути поступ", 11 | "version": "Версія {label}", 12 | "moreInfo": "Більше інформації на {url}", 13 | "levelTitle": "Рівень {number}", 14 | "endMessage": "Вітаємо!", 15 | "extendedMessage": "0JLQuNC00LDRgtC90LUg0LTQvtGB0Y/Qs9C90LXQvdC90Y8hINCS0Lgg0LTQvtGB0LvRltC00LjQu9C4INCw0YDQuNGE0LzQtdGC0LjQutGDINGB0LrRltC90YfQtdC90L3QuNGFINC/0L7Qu9GW0LIg0ZbQtyAxMSDRliAxMjEg0LXQu9C10LzQtdC90YLQsNC80LguICjQktC4INC30LDQvNGW0YLQuNC70Lgg0YHRhdC+0LbRltGB0YLRjCDRltC3INC60L7QvNC/0LvQtdC60YHQvdC40LzQuCDRh9C40YHQu9Cw0LzQuD8p", 16 | "exitDialogTitle": "Зберігання недоступне.", 17 | "exitDialogContent": "Увесь поступ буде втрачено під час виходу. Продовжити?", 18 | "dialogCancel": "Скасувати", 19 | "dialogOk": "Гаразд" 20 | } 21 | -------------------------------------------------------------------------------- /lib/l10n/app_en.arb: -------------------------------------------------------------------------------- 1 | { 2 | "settings": "Settings", 3 | "theme": "Theme", 4 | "themeSystem": "System preference", 5 | "themeLight": "Light", 6 | "themeDark": "Dark", 7 | "darkThemeBlackBackground": "Black background in dark theme", 8 | "darkThemeBlackBackgroundSubtitle": "Mainly intended for OLED screens", 9 | "restart": "Restart", 10 | "restartSubtitle": "Long press to reset the progress", 11 | "version": "Version {label}", 12 | "@version": {"placeholders": {"label": {"type": "String?", "example": "1.1.0"}}}, 13 | "moreInfo": "More information at {url}", 14 | "@moreInfo": {"placeholders": {"url": {"type": "String", "example": "https://example.org"}}}, 15 | "levelTitle": "Level {number}", 16 | "@levelTitle": {"placeholders": {"number": {"type": "int", "example": "1"}}}, 17 | "endMessage": "Congratulations!", 18 | "extendedMessage": "T3V0c3RhbmRpbmcgYWNjb21wbGlzaG1lbnQhIFlvdSBoYXZlIGV4cGxvcmVkIHRoZSBhcml0aG1ldGljcyBvZiB0aGUgZmluaXRlIGZpZWxkcyB3aXRoIDExIGFuZCAxMjEgZWxlbWVudHMuIChEaWQgeW91IG5vdGljZSBhIHNpbWlsYXJpdHkgd2l0aCBjb21wbGV4IG51bWJlcnM/KQ==", 19 | "exitDialogTitle": "Saving not available.", 20 | "exitDialogContent": "All progress will be lost if you exit. Continue?", 21 | "dialogCancel": "Cancel", 22 | "dialogOk": "OK" 23 | } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Everest 2 | 3 | A mathematical puzzle game. Try it at https://mwageringel.github.io/everest/ 4 | 5 | [Get it on F-Droid](https://f-droid.org/packages/io.github.mwageringel.everest/) 8 | 9 | ## Build dependencies 10 | 11 | Building with `make` requires `curl, rsvg-convert, imagemagick, java 17+` and optionally `flutter`. 12 | 13 | Either use the pinned version of Flutter (`./vendor/flutter/bin/flutter`): 14 | 15 | git submodule update --init --recursive 16 | make 17 | 18 | Or use the Flutter version installed on your system: 19 | 20 | make FLUTTER=flutter 21 | 22 | (If you use Android Studio, make sure that `flutter.sdk` and the Android `sdk.dir` 23 | are set correctly in the untracked file `./android/local.properties`.) 24 | 25 | 26 | APK signing certificate fingerprint (SHA-256) of releases on GitHub: 27 | 28 | 576bae61b2aba5d1d32a17d373baa36e05beaaefb67d9b47218d004c0e8333d9 29 | 30 | ## Available Languages 31 | 32 | - English 33 | - French 34 | - German 35 | - Italian 36 | - Spanish 37 | - Ukrainian 38 | 39 | Thanks to all the contributors. 40 | Additional translations are welcome – these are the files to translate: 41 | - [metadata/en-US/full_description.txt](metadata/en-US/full_description.txt) 42 | - [metadata/en-US/short_description.txt](metadata/en-US/short_description.txt) 43 | - [lib/l10n/app_en.arb](lib/l10n/app_en.arb) 44 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 14 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | 31 | 32 | # Exclude, in particular, huge `vendor/flutter` library from analysis. 33 | analyzer: 34 | exclude: 35 | - build/** 36 | - vendor/** 37 | - .dart_tool/** 38 | language: 39 | strict-casts: true 40 | strict-raw-types: true 41 | -------------------------------------------------------------------------------- /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 | android { 29 | compileSdkVersion flutter.compileSdkVersion 30 | 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_1_8 33 | targetCompatibility JavaVersion.VERSION_1_8 34 | } 35 | 36 | kotlinOptions { 37 | jvmTarget = '1.8' 38 | } 39 | 40 | sourceSets { 41 | main.java.srcDirs += 'src/main/kotlin' 42 | } 43 | 44 | defaultConfig { 45 | applicationId "io.github.mwageringel.everest" 46 | // TODO workaround for https://github.com/fluttercommunity/flutter_launcher_icons/issues/324 47 | minSdkVersion flutter.minSdkVersion 48 | // minSdkVersion flutter.minSdkVersion 49 | targetSdkVersion flutter.targetSdkVersion 50 | versionCode flutterVersionCode.toInteger() 51 | versionName flutterVersionName 52 | } 53 | 54 | buildTypes { 55 | release { 56 | // TODO: Add your own signing config for the release build. 57 | // Signing with the debug keys for now, so `flutter run --release` works. 58 | signingConfig signingConfigs.debug 59 | } 60 | } 61 | namespace 'io.github.mwageringel.everest' 62 | } 63 | 64 | flutter { 65 | source '../..' 66 | } 67 | 68 | dependencies { 69 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 70 | } 71 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased] 4 | 5 | ## [1.1.6] - 2024-02-04 6 | ### Added 7 | - Italian translation (by @gabriblas) 8 | - the web version now saves the progress (works on iOS, as well) (#26) 9 | 10 | ### Changed 11 | - upgraded to Flutter 3.16.9; the Flutter version is now pinned in a submodule 12 | 13 | ### Fixed 14 | - a UI issue affecting the html renderer of mobile browsers in which the focussed question was vertically displaced 15 | - a UI issue in which the scroll animation could be triggered incorrectly 16 | - an issue that could affect mobile browsers without WebAssembly support 17 | - a rendering issue affecting LineageOS (#17) 18 | 19 | ## [1.1.5] - 2022-09-19 20 | ### Added 21 | - French translation (by @DodoLeDev) 22 | - several tests 23 | 24 | ### Changed 25 | - sets of questions now show an icon for each question for better visual feedback 26 | 27 | ## [1.1.4] - 2022-09-01 28 | ### Added 29 | - Ukrainian translation (by @andmizyk) 30 | - Spanish translation (by @thermosflasche) 31 | 32 | ## [1.1.3] - 2022-08-15 33 | ### Added 34 | - German translation and support for translations to other languages (implemented by @vrifox) 35 | 36 | ### Changed 37 | - small revision of the first few levels 38 | - small changes to navigation to give more priority to the subpages, especially in the first levels 39 | 40 | ### Fixed 41 | - an issue in which level 1 did not unlock 42 | (Workaround for version 1.1.2: Solve the very first question again or change the theme.) 43 | - a display issue with enlarged system fonts 44 | - the website URL is now clickable 45 | 46 | ## [1.1.2] - 2022-07-22 47 | ### Added 48 | - a new first level 49 | - a new icon 50 | 51 | ## [1.1.1] - 2022-07-20 52 | ### Added 53 | - automatic scrolling to the next question 54 | - support for keyboard input 55 | 56 | ### Changed 57 | - improved rendering performance 58 | 59 | ## [1.1.0] - 2022-07-16 60 | ### Added 61 | - website 62 | 63 | ### Changed 64 | - appearance and color scheme 65 | 66 | [Unreleased]: https://github.com/mwageringel/everest/compare/1.1.6...HEAD 67 | [1.1.6]: https://github.com/mwageringel/everest/compare/1.1.5...1.1.6 68 | [1.1.5]: https://github.com/mwageringel/everest/compare/1.1.4...1.1.5 69 | [1.1.4]: https://github.com/mwageringel/everest/compare/1.1.3...1.1.4 70 | [1.1.3]: https://github.com/mwageringel/everest/compare/1.1.2...1.1.3 71 | [1.1.2]: https://github.com/mwageringel/everest/compare/1.1.1...1.1.2 72 | [1.1.1]: https://github.com/mwageringel/everest/compare/1.1.0...1.1.1 73 | [1.1.0]: https://github.com/mwageringel/everest/releases/tag/1.1.0 74 | -------------------------------------------------------------------------------- /test/expressions_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:everest/expressions.dart'; 3 | 4 | void main() { 5 | test('addition', () { 6 | final e = Con(3) + Con(7) + Con(100); 7 | expect(e.toString(), equals('3 + 7 + 100')); 8 | expect(e.eval({}), equals(110)); 9 | expect(e.eq(Con(110)).str({}), equals('3 + 7 + 100 = 110')); 10 | }); 11 | 12 | test('various expression operations', () { 13 | final e = -Con(3.0) * Con(4.0) + Con(14.0) / Con(7.0) - Con(1.0) + Con(3.0).square(); 14 | expect(e.toString(), equals('-3.0 * 4.0 + 14.0 / 7.0 - 1.0 + 3.0²')); 15 | expect(e.eval({}), equals(-2.0)); 16 | }); 17 | 18 | test('variables', () { 19 | Var y = Var(), z = Var(); 20 | final e = Con(3) + Con(4) * y + z; 21 | final vars = {y: 2, z: 5}; 22 | expect(e.toString(), equals('3 + 4 * ? + ?')); 23 | expect(e.str(vars), equals('3 + 4 * 2 + 5')); 24 | expect(e.str({z: 2, y: 5}), equals('3 + 4 * 5 + 2')); 25 | expect(e.eval(vars), equals(16)); 26 | expect(() => e.eval({}), throwsA(isA())); 27 | }); 28 | 29 | test('eq', () { 30 | final e = Con(G.F(2,3)) * Con(G.F(2,5)) * Con(G.F(6,4)) * Con(G.F(1,5)); 31 | expect(e.eq(Con(G.F(1,2))).eval({}), equals(true)); 32 | expect(e.eq(Con(G.F(5,8))).eval({}), equals(false)); 33 | expect(e.eq(Con(G.F(6,7))).eval({}), equals(false)); 34 | expect(e.eq(Con(G.F(0,0))).eval({}), equals(false)); 35 | expect(e.eq(Con(G.F(1,0))).eval({}), equals(false)); 36 | expect(e.eq(Con(G.F(0,1))).eval({}), equals(false)); 37 | }); 38 | 39 | final testElems = [G.F(1,2), G.F(3,4), G.F(5,7), G.F(9,2)]; 40 | 41 | test('pow', () { 42 | for (final dynamic a in testElems.cast().followedBy([F(3), F(4)])) { 43 | for (final n in [1,2,3,4,5,6,7,8,9,10]) { 44 | expect(a.pow(n), equals(List.filled(n-1, a).fold(a, (dynamic b, c) => b * c))); 45 | } 46 | } 47 | }); 48 | 49 | test('div', () { 50 | for (final a in testElems) { 51 | expect(a / a, equals(G.F(1,0))); 52 | } 53 | for (final a in [F(3), F(4), F(5), F(6)]) { 54 | expect(a / a, equals(F(1))); 55 | } 56 | }); 57 | 58 | test('operations on G', () { 59 | expect(-G.F(1,2) + G.F(3,4) * G.F(5,6) / G.F(7,8) - G.F(9,10).pow(2), equals(G.F(-1,6))); 60 | }); 61 | 62 | test('dot', () { 63 | expect((dot(3,4) * dot(5,8) * dot(6,5) * dot(7,X)).eq(dot(4, 2)).eval({}), equals(true)); 64 | }); 65 | 66 | test('parse', () { 67 | expect(F.parse('X'), equals(X.eval({}))); 68 | expect(F.parse('-1'), equals(F(-1))); 69 | expect(F.parse('123'), equals(F(123))); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | Everest 36 | 37 | 38 | 39 | 40 | 41 | 42 | 46 | 47 | 48 | 49 | 50 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /assets/launcher_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 37 | 39 | 42 | 46 | 50 | 51 | 60 | 61 | 66 | 75 | 76 | 81 | 86 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /assets/launcher_icon_alt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 37 | 39 | 42 | 46 | 50 | 51 | 60 | 61 | 76 | 81 | 86 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /assets/launcher_icon_maskable.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 37 | 39 | 42 | 46 | 50 | 51 | 60 | 61 | 76 | 81 | 86 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /assets/launcher_icon_adaptive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 37 | 39 | 42 | 46 | 50 | 51 | 60 | 61 | 76 | 81 | 86 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 4 | import 'package:everest/main.dart'; 5 | import 'package:everest/game.dart'; 6 | import 'package:everest/storage.dart'; 7 | 8 | FinderBase findKeypad(String n) => find.descendant(of: find.byType(KeyboardButton), matching: find.text(n)); 9 | 10 | void main() { 11 | test('debug mode disabled', () { 12 | expect(debugUnlockAll, equals(false)); 13 | }); 14 | 15 | for (final locale in AppLocalizations.supportedLocales) { 16 | testWidgets('Base64-encoded localized messages ($locale) can be decoded', (WidgetTester tester) async { 17 | await tester.pumpWidget( 18 | Localizations( 19 | delegates: AppLocalizations.localizationsDelegates, 20 | locale: locale, 21 | child: const ExtendedMessage(), 22 | ), 23 | ); 24 | }); 25 | } 26 | 27 | testWidgets('unlocking of level 1', (WidgetTester tester) async { 28 | // Check that unlocking of exam 1 works even when we click onto level 0 before opening the level 1 subpage. 29 | // The problem was that this makes level 0 active, such that lazy redrawing did not refresh exam 1 when it should become visible. 30 | final game0 = Game(SqfliteDatabaseWrapper(null)); 31 | final world0 = World(null, ThemeMode.light, false, Future.value(game0)); 32 | await tester.pumpWidget(MyApp(world0, game0)); 33 | await tester.tap(findKeypad('3')); 34 | await tester.pumpAndSettle(); 35 | await tester.tap(find.textContaining('1 + 2')); // tapping here means that level 0 becomes "active" (the main source of the redrawing issue) 36 | await tester.pumpAndSettle(); 37 | await tester.tap(find.text('Level 1')); 38 | await tester.pumpAndSettle(); 39 | await tester.pageBack(); 40 | await tester.pumpAndSettle(); 41 | expect(find.textContaining('5 + 5'), findsOneWidget); // the first exam questions of level 1 should be visible 42 | }); 43 | 44 | testWidgets('theme change', (WidgetTester tester) async { 45 | // This tests checks that the background color of the exam screen changes 46 | // immediately after switching from light to dark theme, a regression 47 | // introduced with the upgrade to flutter 3.3.0. 48 | final game0 = Game(SqfliteDatabaseWrapper(null)); 49 | const pureBlack0 = false; 50 | final world0 = World(null, ThemeMode.light, pureBlack0, Future.value(game0)); 51 | await tester.pumpWidget(MyApp(world0, game0)); 52 | 53 | checkExamBackgroundColors(Color expectedColor) { 54 | // helpful accessors: https://stackoverflow.com/a/47296248 https://stackoverflow.com/a/62641476 55 | List colors = tester.elementList(find.byType(ExamWidget)).map((w) => 56 | w.findAncestorWidgetOfExactType()!.color 57 | ).toList(); 58 | expect(colors.length, greaterThan(3)); 59 | for (final c in colors) { 60 | expect(c, equals(expectedColor)); 61 | } 62 | } 63 | 64 | checkExamBackgroundColors(MyApp.lightTheme().scaffoldBackgroundColor); 65 | await tester.tap(find.byIcon(Icons.menu)); 66 | await tester.pumpAndSettle(); 67 | await tester.tap(find.text('Dark')); 68 | await tester.pumpAndSettle(); // important for color transition animation to finish 69 | await tester.pageBack(); 70 | await tester.pumpAndSettle(); 71 | checkExamBackgroundColors(MyApp.darkTheme(pureBlack0).scaffoldBackgroundColor); 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # FLUTTER=flutter 2 | FLUTTER=./vendor/flutter/bin/flutter 3 | BASEHREF=/demo/ 4 | 5 | all: app web 6 | app: assets-android 7 | $(FLUTTER) config --no-analytics 8 | $(FLUTTER) build apk --release 9 | web: assets-web 10 | $(FLUTTER) build web --base-href=$(BASEHREF) --release 11 | rm -rf website/demo/ 12 | cp -p -r build/web/ website/demo/ 13 | gh-pages: 14 | $(FLUTTER) clean 15 | $(MAKE) BASEHREF=/everest/demo/ web 16 | # assumes worktree gh-pages is checked out in ./gh-pages/ 17 | cd gh-pages && git rm -rf . --ignore-unmatch && cp -p -r ../website/* . && git add . && git commit --amend --no-edit 18 | host: 19 | cd website && python -m http.server 8000 20 | run: assets-android assets-web 21 | $(FLUTTER) run 22 | test: 23 | $(FLUTTER) test test/* -r expanded 24 | clean: icons-clean 25 | $(FLUTTER) clean 26 | rm -rf website/demo/ 27 | 28 | assets-android: fonts icons-android 29 | assets-web: fonts icons-web 30 | 31 | # rasterized icons are generated from an svg file 32 | icons-android: android/app/src/main/res/mipmap-hdpi/ic_launcher.png 33 | assets/launcher_icon.png: assets/launcher_icon.svg 34 | rsvg-convert --width=1024 --height=1024 --keep-aspect-ratio assets/launcher_icon.svg > $@ 35 | assets/launcher_icon_adaptive.png: assets/launcher_icon_adaptive.svg 36 | rsvg-convert --width=1024 --height=1024 --keep-aspect-ratio assets/launcher_icon_adaptive.svg > $@ 37 | android/app/src/main/res/mipmap-hdpi/ic_launcher.png: assets/launcher_icon.png assets/launcher_icon_adaptive.png 38 | $(FLUTTER) pub get 39 | $(FLUTTER) pub run flutter_launcher_icons 40 | 41 | icons-fdroid: assets/launcher_icon.svg 42 | rsvg-convert --width=512 --height=512 --keep-aspect-ratio assets/launcher_icon.svg > metadata/en-US/images/icon.png 43 | 44 | icons-web: website/favicon.ico web/favicon.ico web/icons/Icon-192.png web/icons/Icon-maskable-192.png web/icons/Icon-512.png web/icons/Icon-maskable-512.png 45 | web/favicon.ico website/favicon.ico: assets/launcher_icon.svg 46 | magick -background none assets/launcher_icon.svg -define icon:auto-resize $@ 47 | web/icons/Icon-192.png: assets/launcher_icon.svg 48 | mkdir -p web/icons/ 49 | rsvg-convert --width=192 --height=192 --keep-aspect-ratio assets/launcher_icon.svg > $@ 50 | web/icons/Icon-512.png: assets/launcher_icon.svg 51 | mkdir -p web/icons/ 52 | rsvg-convert --width=512 --height=512 --keep-aspect-ratio assets/launcher_icon.svg > $@ 53 | web/icons/Icon-maskable-192.png: assets/launcher_icon_maskable.svg 54 | mkdir -p web/icons/ 55 | rsvg-convert --width=192 --height=192 --keep-aspect-ratio -b '#536dfeff' assets/launcher_icon_maskable.svg > $@ 56 | web/icons/Icon-maskable-512.png: assets/launcher_icon_maskable.svg 57 | mkdir -p web/icons/ 58 | rsvg-convert --width=512 --height=512 --keep-aspect-ratio -b '#536dfeff' assets/launcher_icon_maskable.svg > $@ 59 | icons-clean: 60 | rm -f android/app/src/main/res/mipmap-*/ic_launcher.png 61 | rm -f website/favicon.ico web/favicon.ico web/icons/*.png 62 | rm -f assets/launcher_icon.png assets/launcher_icon_adaptive.png 63 | 64 | # fonts are downloaded and bundled into the app 65 | fonts: fonts/NotoSansMath-Regular.ttf fonts/static/NotoSans/NotoSans-Regular.ttf 66 | fonts/NotoSansMath-Regular.ttf: | build/upstream/Noto_Sans_Math.zip 67 | mkdir -p fonts/ 68 | unzip -o build/upstream/Noto_Sans_Math.zip -d fonts/ 69 | build/upstream/Noto_Sans_Math.zip: 70 | mkdir -p build/upstream/ 71 | curl --output build/upstream/Noto_Sans_Math.zip https://fonts.google.com/download?family=Noto%20Sans%20Math 72 | fonts/static/NotoSans/NotoSans-Regular.ttf: | build/upstream/Noto_Sans.zip 73 | mkdir -p fonts/ 74 | unzip -o build/upstream/Noto_Sans.zip -d fonts/ 75 | build/upstream/Noto_Sans.zip: 76 | mkdir -p build/upstream/ 77 | curl --output build/upstream/Noto_Sans.zip https://fonts.google.com/download?family=Noto%20Sans 78 | .INTERMEDIATE: build/upstream/Noto_Sans_Math.zip build/upstream/Noto_Sans.zip 79 | 80 | .PHONY: all app web gh-pages host run test assets-android assets-web fonts icons-android icons-fdroid icons-web icons-clean clean 81 | -------------------------------------------------------------------------------- /website/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Everest 5 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 | 19 | 20 |
21 |

Everest

22 |

A mathematical puzzle game

23 |
24 | 25 | 26 |
27 |
28 | 29 |
30 |

Android App

31 |

32 | Recommended if you have an Android device (with Android 4.1 or above). 33 |

34 | 35 | Get it on F-Droid 36 | 37 | 38 |
39 | 40 |
41 |

iOS & Web App

42 |

43 | The web app opens directly in your web browser. 44 | Its layout works best with mobile devices or small browser windows. 45 |
46 | On iOS, you can add the app to your Home Screen via the Share menu. 47 |

48 | Open Web App (8 MB) 49 |
50 |
51 |
52 | 53 | 54 |
55 |
56 |

About the Game

57 |

58 | No mathematical knowledge is required beyond basic arithmetics. 59 | All the rules of the game are found through exploration, 60 | based on the concept of discovery learning. 61 |

62 |

63 | The game is free and fully open source – available on GitHub. 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 |

72 |
73 |
74 | 75 |
76 |

Inspired by The Witness

77 |
78 | 79 | 80 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: everest 2 | description: A mathematical puzzle game. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.1.6+15 19 | 20 | environment: 21 | sdk: ">=2.16.2 <3.0.0" 22 | 23 | # Dependencies specify other packages that your package needs in order to work. 24 | # To automatically upgrade your package dependencies to the latest versions 25 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 26 | # dependencies can be manually updated by changing the version numbers below to 27 | # the latest version available on pub.dev. To see which dependencies have newer 28 | # versions available, run `flutter pub outdated`. 29 | dependencies: 30 | flutter: 31 | sdk: flutter 32 | flutter_localizations: 33 | sdk: flutter 34 | intl: ^0.18.1 35 | sqflite: 36 | localstorage: ^4.0.0+1 37 | path: 38 | 39 | # The following adds the Cupertino Icons font to your application. 40 | # Use with the CupertinoIcons class for iOS style icons. 41 | cupertino_icons: ^1.0.2 42 | provider: ^6.0.2 43 | package_info_plus: ^5.0.1 44 | flex_color_scheme: ^7.3.1 45 | url_launcher: ^6.1.5 46 | # scrollable_positioned_list: ^0.2.3 47 | 48 | dev_dependencies: 49 | flutter_test: 50 | sdk: flutter 51 | 52 | # The "flutter_lints" package below contains a set of recommended lints to 53 | # encourage good coding practices. The lint set provided by the package is 54 | # activated in the `analysis_options.yaml` file located at the root of your 55 | # package. See that file for information about deactivating specific lint 56 | # rules and activating additional ones. 57 | flutter_lints: ^3.0.1 58 | test: ^1.21.1 59 | flutter_launcher_icons: ^0.13.1 60 | 61 | # For information on the generic Dart part of this file, see the 62 | # following page: https://dart.dev/tools/pub/pubspec 63 | 64 | # The following section is specific to Flutter. 65 | flutter: 66 | 67 | # Enabled for flutter_localizations, for more information see https://docs.flutter.dev/development/accessibility-and-localization/internationalization 68 | generate: true 69 | 70 | # The following line ensures that the Material Icons font is 71 | # included with your application, so that you can use the icons in 72 | # the material Icons class. 73 | uses-material-design: true 74 | 75 | # To add assets to your application, add an assets section, like this: 76 | # assets: 77 | # - images/a_dot_burr.jpeg 78 | # - images/a_dot_ham.jpeg 79 | 80 | # An image asset can refer to one or more resolution-specific "variants", see 81 | # https://flutter.dev/assets-and-images/#resolution-aware. 82 | 83 | # For details regarding adding assets from package dependencies, see 84 | # https://flutter.dev/assets-and-images/#from-packages 85 | 86 | # To add custom fonts to your application, add a fonts section here, 87 | # in this "flutter" section. Each entry in this list should have a 88 | # "family" key with the font family name, and a "fonts" key with a 89 | # list giving the asset and other descriptors for the font. For 90 | # example: 91 | # fonts: 92 | # - family: Schyler 93 | # fonts: 94 | # - asset: fonts/Schyler-Regular.ttf 95 | # - asset: fonts/Schyler-Italic.ttf 96 | # style: italic 97 | # - family: Trajan Pro 98 | # fonts: 99 | # - asset: fonts/TrajanPro.ttf 100 | # - asset: fonts/TrajanPro_Bold.ttf 101 | # weight: 700 102 | # 103 | # For details regarding fonts from package dependencies, 104 | # see https://flutter.dev/custom-fonts/#from-packages 105 | fonts: 106 | - family: NotoSansMath 107 | fonts: 108 | - asset: fonts/NotoSansMath-Regular.ttf 109 | - family: NotoSans 110 | fonts: 111 | - asset: fonts/static/NotoSans/NotoSans-Regular.ttf 112 | 113 | assets: 114 | # license for Noto fonts 115 | - fonts/OFL.txt 116 | 117 | flutter_launcher_icons: 118 | android: true 119 | ios: false 120 | image_path: assets/launcher_icon.png 121 | adaptive_icon_foreground: assets/launcher_icon_adaptive.png 122 | adaptive_icon_background: "#536dfe" 123 | -------------------------------------------------------------------------------- /lib/storage.dart: -------------------------------------------------------------------------------- 1 | import 'package:everest/game.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/services.dart'; 4 | import 'package:path/path.dart'; 5 | import 'package:sqflite/sqflite.dart'; 6 | import 'package:localstorage/localstorage.dart'; 7 | 8 | abstract class DatabaseWrapper { 9 | bool isUsable() => true; 10 | Future storeAnswer(Level level, Question question); 11 | Future> loadAnswers(List levels); 12 | Future deleteAnswers(List levels); 13 | Future loadKeyValue(String key); 14 | Future storeKeyValue(String key, String value); 15 | 16 | static Future create() async { 17 | if (kIsWeb) { 18 | final storage = LocalStorage('everest-data.json'); 19 | await storage.ready; 20 | return WebStorageDatabaseWrapper(storage); 21 | } else { 22 | Database? db; 23 | try { 24 | db = await openDatabase( 25 | join(await getDatabasesPath(), 'everest-data.db'), 26 | onCreate: (db, version) async { 27 | // note that adding additional tables to existing database file requires some extra steps 28 | await db.execute( 29 | 'CREATE TABLE $tableKV($columnKey TEXT PRIMARY KEY, $columnValue TEXT)', 30 | ); 31 | await db.execute( 32 | 'CREATE TABLE $tableAnswers($columnId TEXT PRIMARY KEY, $columnLevel TEXT, $columnQuestion TEXT, $columnInputs TEXT)', 33 | ); 34 | }, 35 | version: 1, 36 | ); 37 | } on MissingPluginException { 38 | db = null; // database is not available (should not happen on supported platforms anymore) 39 | } 40 | return SqfliteDatabaseWrapper(db); 41 | } 42 | } 43 | } 44 | 45 | class SqfliteDatabaseWrapper extends DatabaseWrapper { 46 | final Database? db; 47 | SqfliteDatabaseWrapper(this.db); 48 | 49 | @override 50 | bool isUsable() => db != null; 51 | 52 | @override 53 | Future storeAnswer(Level level, Question question) async { 54 | await db?.insert(tableAnswers, question.toMap(level), conflictAlgorithm: ConflictAlgorithm.replace); 55 | } 56 | 57 | @override 58 | Future> loadAnswers(List levels) async { 59 | if (db != null) { 60 | List> maps = await (db!.query(tableAnswers, columns: [columnId, columnInputs])); 61 | // map from fullId to stringified answer 62 | return Map.fromEntries(maps.expand((m) { 63 | final id = m[columnId]; 64 | final answer = m[columnInputs]; 65 | return (id != null && answer != null && id is String && answer is String) ? [MapEntry(id, answer)] : []; 66 | })); 67 | } else { 68 | return Future.value({}); 69 | } 70 | } 71 | 72 | @override 73 | Future deleteAnswers(List levels) async { 74 | if (db != null) { 75 | await db!.delete(tableAnswers); 76 | } 77 | } 78 | 79 | @override 80 | Future loadKeyValue(String key) async { 81 | if (db != null) { 82 | List> maps = await db!.query(tableKV, 83 | where: '$columnKey = ?', 84 | whereArgs: [key], 85 | ); 86 | final result = maps.isNotEmpty ? maps.first[columnValue] : null; 87 | return result is String ? result : null; 88 | } else { 89 | return null; 90 | } 91 | } 92 | 93 | @override 94 | Future storeKeyValue(String key, String value) async { 95 | if (db != null) { 96 | await db!.insert(tableKV, {columnKey: key, columnValue: value}, conflictAlgorithm: ConflictAlgorithm.replace); 97 | } 98 | } 99 | } 100 | 101 | class WebStorageDatabaseWrapper extends DatabaseWrapper { 102 | final LocalStorage storage; 103 | WebStorageDatabaseWrapper(this.storage); 104 | 105 | @override 106 | Future storeAnswer(Level level, Question question) { 107 | final item = question.toMap(level); 108 | return storage.setItem(item[columnId]!, item); 109 | } 110 | 111 | @override 112 | Future> loadAnswers(List levels) async { 113 | Map result = {}; 114 | for (final level in levels) { 115 | for (final question in level.exercise.questions.followedBy(level.exam.questions)) { 116 | final id = question.fullId(level); 117 | dynamic m = storage.getItem(id); 118 | if (m != null && m[columnId] != null) { 119 | final answer = m[columnInputs]; 120 | if (answer != null && answer is String) { 121 | result[id] = answer; 122 | } 123 | } 124 | } 125 | } 126 | return result; 127 | } 128 | 129 | @override 130 | Future deleteAnswers(List levels) async { 131 | Iterable> deletions = levels.expand((level) => 132 | level.exercise.questions 133 | .followedBy(level.exam.questions) 134 | .map((question) => storage.deleteItem(question.fullId(level))) 135 | ); 136 | await Future.wait(deletions); 137 | } 138 | 139 | @override 140 | Future loadKeyValue(String key) async { 141 | dynamic value = storage.getItem(key); 142 | return value is String ? value : null; 143 | } 144 | 145 | @override 146 | Future storeKeyValue(String key, String value) { 147 | return storage.setItem(key, value); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /lib/expressions.dart: -------------------------------------------------------------------------------- 1 | // expression algebra with variables, constants and unary and binary operations. 2 | 3 | A _evalMult(A left, A right) => ((left as dynamic) * right) as A; 4 | 5 | typedef EvalContext = Map, dynamic>; // all our variables will have type parameter F 6 | 7 | abstract class Expression { 8 | String str(EvalContext vars); 9 | A eval(EvalContext vars); 10 | String evalString(EvalContext vars) => '[${str(vars)}] = ${eval(vars)}'; // for debugging 11 | @override toString() => str({}); 12 | 13 | Expression eq(Expression other) => Bin(this, other, str: (s, t) => '$s = $t', eval: (a, b) => (a as dynamic).pow(1331) == (b as dynamic).pow(p)); 14 | Expression operator +(Expression other) => Bin(this, other, str: (s, t) => '$s + $t', eval: (a, b) => ((a as dynamic) + b) as A); 15 | Expression operator -(Expression other) => Bin(this, other, str: (s, t) => '$s - $t', eval: (a, b) => ((a as dynamic) - b) as A); 16 | Expression operator *(Expression other) => Bin(this, other, str: (s, t) => '$s * $t', eval: _evalMult); 17 | Expression operator /(Expression other) => Bin(this, other, str: (s, t) => '$s / $t', eval: (a, b) => ((a as dynamic) / b) as A); 18 | Expression operator -() => Unary(this, str: (s) => '-$s', eval: (a) => -(a as dynamic) as A); 19 | Expression square() => Unary(this, str: (s) => '$s²', eval: (A a) => _evalMult(a, a)); 20 | // Expression paren() => Unary(this, str: (s) => '($s)', eval: (A a) => a); // not needed yet 21 | } 22 | 23 | class Var extends Expression { 24 | @override str(vars) => vars[this]?.toString() ?? '?'; 25 | @override eval(vars) { 26 | final a = vars[this]; 27 | if (a == null) { 28 | throw UnsupportedError('can only evaluate variables in vars context'); 29 | } else { 30 | return a as A; 31 | } 32 | } 33 | } 34 | 35 | class Con extends Expression { 36 | final A con; 37 | final String? _str; 38 | Con(this.con, {String? str}) : _str = str; 39 | @override str(vars) => _str ?? con.toString(); 40 | @override eval(vars) => con; 41 | } 42 | 43 | class Bin extends Expression { 44 | final Expression left, right; 45 | final B Function(A, A) _eval; 46 | final String Function(String, String) _str; 47 | Bin(this.left, this.right, {required B Function(A, A) eval, required String Function(String, String) str}): 48 | _eval = eval, _str = str; 49 | @override str(vars) => _str(left.str(vars), right.str(vars)); 50 | @override eval(vars) => _eval(left.eval(vars), right.eval(vars)); 51 | } 52 | 53 | class Unary extends Expression { 54 | final Expression operand; 55 | final B Function(A) _eval; 56 | final String Function(String) _str; 57 | Unary(this.operand, {required B Function(A) eval, required String Function(String) str}): 58 | _eval = eval, _str = str; 59 | @override str(vars) => _str(operand.str(vars)); 60 | @override eval(vars) => _eval(operand.eval(vars)); 61 | } 62 | 63 | A _pow(A x, int n) { 64 | if (n <= 0) { 65 | throw UnsupportedError("exponent must be positive"); 66 | } else { 67 | A? res; // iterated squaring without 1 68 | while (n > 0) { 69 | if (n.isOdd) { 70 | res = res == null ? x : _evalMult(res, x); 71 | } 72 | x = _evalMult(x , x); 73 | n = n ~/ 2; 74 | } 75 | return res as A; // res is never null 76 | } 77 | } 78 | 79 | const int p = 11; 80 | class F { 81 | final int _u; 82 | F._mkF(this._u); 83 | static final List elems = List.unmodifiable(List.generate(p, F._mkF)); 84 | factory F(int u) => elems[u % p]; 85 | factory F.parse(String s) => s == 'X' ? X.con : F(int.parse(s)); 86 | @override toString() => _u.toString(); 87 | @override operator ==(other) => other is F ? _u == other._u : false; 88 | @override int get hashCode => _u.hashCode; 89 | F operator +(F other) => F(_u + other._u); 90 | F operator *(F other) => F(_u * other._u); 91 | F operator -(F other) => F(_u - other._u); 92 | F operator -() => F(-_u); 93 | F pow(int exponent) => F(_u.modPow(exponent, p)); 94 | F operator /(F other) => this * other.pow(p-2); // ignoring 0 95 | } 96 | 97 | class G { 98 | final F a, b; 99 | G(this.a, this.b); 100 | factory G.F(int a, int b) => G(F(a), F(b)); 101 | factory G.fromF(F a) => G(a, F(0)); 102 | @override toString() => '($a,$b)'; 103 | @override operator ==(other) => other is G ? a == other.a && b == other.b : false; 104 | @override int get hashCode => b.hashCode * p + a.hashCode; 105 | G operator +(G other) => G(a + other.a, b + other.b); 106 | G operator *(G other) { 107 | final c = other.a, d = other.b; 108 | return G(a*c + F(9)*b*d, b*c + (F(4)*b + a)*d); 109 | } 110 | G operator -() => G(-a, -b); 111 | G operator -(G other) => this + -other; 112 | G pow(int exponent) => _pow(this, exponent); 113 | G operator /(G other) => this * other.pow(119); // ignoring 0 (so `other` must not be user input) 114 | } 115 | 116 | final X = Con(F(7).pow(35), str: 'X'); 117 | 118 | Expression C(int u) => Con(F(u)); 119 | 120 | Expression _toExprF(x) { 121 | if (x is Expression) { 122 | return x as Expression; 123 | } else if (x is F) { 124 | return Con(x); 125 | } else if (x is int) { 126 | return Con(F(x)); 127 | } else { 128 | throw UnsupportedError("$x not viewable as Expression"); 129 | } 130 | } 131 | 132 | final G _s = G.F(3, 3); 133 | 134 | Expression dot(left, right) { 135 | return Bin(_toExprF(left), _toExprF(right), 136 | eval: (a, b) => G(b + F(6)*a, a + b) * _s, 137 | str: (s, t) => '$s.$t', 138 | ); 139 | } 140 | -------------------------------------------------------------------------------- /lib/game.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/foundation.dart' show listEquals, ChangeNotifier; 3 | import 'package:everest/expressions.dart'; 4 | import 'package:everest/storage.dart'; 5 | import 'package:flutter/widgets.dart'; 6 | 7 | const debugUnlockAll = false; 8 | const String tableKV = 'keyvalues', tableAnswers = 'answers'; 9 | const String columnKey = 'key', columnValue = 'value', columnId = 'id', columnLevel = 'level', columnQuestion = 'question', columnInputs = 'inputs'; 10 | const String levelsUnlockedKey = 'game:levelsUnlocked'; 11 | 12 | enum QuestionsStatus { wrong, partial, correct } 13 | enum QuestionsStatusCovered { wrong, partial, correct, covered } 14 | QuestionsStatus combineStatus(QuestionsStatus a, QuestionsStatus b) { 15 | if (a == QuestionsStatus.correct) { 16 | return b; 17 | } else if (a == QuestionsStatus.partial || b == QuestionsStatus.partial) { 18 | return QuestionsStatus.partial; 19 | } else { // a wrong, b not partial 20 | return QuestionsStatus.wrong; 21 | } 22 | } 23 | QuestionsStatus jointStatus(Iterable questions) { 24 | return questions.map((q) => q.status()).fold(QuestionsStatus.correct, combineStatus); 25 | } 26 | 27 | class Question { 28 | final Expression expr; 29 | final List> vars; 30 | late final String q; // pretty representation 31 | late final String id; // more canonical representation for database 32 | List? _cachedSolution; 33 | final List inputs = []; // we use String instead of F because of X 34 | bool isPartial; 35 | static const _dottedFence = '⦙', _equiv = '≡'; 36 | 37 | Question(this.expr, this.vars, {this.isPartial = false}) { 38 | final s = expr.toString(); 39 | q = s.replaceAll('=', _equiv).replaceAll('.', _dottedFence); 40 | id = s.replaceAll(' ', ''); 41 | final numBlanks = '?'.allMatches(q).length; 42 | assert(numBlanks == numVariables); 43 | } 44 | 45 | int get numVariables => vars.length; 46 | 47 | QuestionsStatus status() { 48 | if (inputs.length < numVariables) { 49 | return QuestionsStatus.partial; 50 | } else if (_cachedSolution != null && listEquals(inputs, _cachedSolution)) { 51 | return QuestionsStatus.correct; 52 | } else if (expr.eval(Map.fromIterables(vars, inputs.map(F.parse)))) { 53 | _cachedSolution = List.unmodifiable(inputs); // we cache a copy of the last correct solution only 54 | return QuestionsStatus.correct; 55 | } else { 56 | return QuestionsStatus.wrong; 57 | } 58 | } 59 | 60 | @override 61 | toString() => 'Q($q, ${inputs.join(',')})'; 62 | 63 | String stringifyInputs() => inputs.join(';'); 64 | List unstringifyInputs(String s) => s == '' ? [] : s.split(';'); 65 | String fullId(Level level) => '${level.id}:$id'; 66 | 67 | Map toMap(Level level) { 68 | return { 69 | columnId: fullId(level), 70 | columnLevel: level.id, 71 | columnQuestion: q, 72 | columnInputs: stringifyInputs(), 73 | }; 74 | } 75 | 76 | void updateInputs(List newInputs) { 77 | if (newInputs.length <= numVariables) { 78 | inputs.clear(); 79 | inputs.addAll(newInputs); 80 | } 81 | // otherwise, we ignore the change to avoid inconsistencies 82 | } 83 | } 84 | 85 | class QuestionsWithIndex { 86 | final List questions; 87 | int activeIndex = 0; 88 | QuestionsWithIndex(this.questions); 89 | Question get activeQuestion => questions[activeIndex]; 90 | 91 | Iterable _previousQuestions(int idx) sync* { 92 | for (var j = idx - 1; j >= 0; j--) { 93 | yield questions[j]; 94 | } 95 | } 96 | 97 | QuestionsStatus activeFullQuestionStatus() { 98 | int idx = activeIndex; 99 | while (questions[idx].isPartial) { 100 | idx++; 101 | } 102 | final s = combineStatus( 103 | questions[idx].status(), 104 | jointStatus(_previousQuestions(idx).takeWhile((q) => q.isPartial))); 105 | return s; 106 | } 107 | 108 | Iterable>> fullQuestions() sync* { 109 | List> buf = []; 110 | for (final e in questions.asMap().entries) { 111 | buf.add(e); 112 | if (!e.value.isPartial) { 113 | yield buf; 114 | buf = []; 115 | } 116 | } 117 | assert(buf.isEmpty, "last question must not be partial"); 118 | } 119 | } 120 | 121 | class Level { 122 | final String id; 123 | final QuestionsWithIndex exercise; 124 | final QuestionsWithIndex exam; 125 | bool clicked = false; 126 | Level(this.id, List questions, List examQuestions): 127 | exercise = QuestionsWithIndex(questions), 128 | exam = QuestionsWithIndex(examQuestions) { 129 | exam.questions.asMap().forEach((i, q) { 130 | q.isPartial = i < exam.questions.length - 1; 131 | }); 132 | } 133 | 134 | @override 135 | toString() { 136 | return 'Level(questions: [${exercise.questions.join(',')}], examQuestions: [${exam.questions.join(',')}], activeQuestion: ${exercise.activeIndex}, activeExamQuestion: ${exam.activeIndex})'; 137 | } 138 | 139 | bool isSolved() => jointStatus(exam.questions) == QuestionsStatus.correct; 140 | } 141 | 142 | enum ScrollType { none, smooth, jump } 143 | 144 | final Var y = Var(), z = Var(); 145 | final yz = dot(y, z); 146 | typedef Q = Question; 147 | Question q1(Expression lhs, {bool isPartial = false}) { 148 | return Question(lhs.eq(y), [y], isPartial: isPartial); 149 | } 150 | Question q2(Expression lhs, {bool isPartial = false}) { 151 | return Question(lhs.eq(yz), [y, z], isPartial: isPartial); 152 | } 153 | 154 | class Game with ChangeNotifier { 155 | final List levels = [ 156 | Level("0", [], [q1(C(1) + C(2))]), 157 | Level("0.5", [ 158 | q1(C(3) + C(4)), 159 | q1(C(4) + C(3)), 160 | q1(C(2) + C(6)), 161 | q1(C(1) + C(5)), 162 | q1(C(0) + X), 163 | q1(C(3) + C(7)), 164 | q1(C(2) + C(7)), 165 | q1(C(6) + C(4)), 166 | q1(C(9) + C(1)), 167 | q1(C(0) + C(0), isPartial: true), 168 | q1(C(3) + C(1)), 169 | q1(C(3) + X), 170 | q1(C(4) + X), 171 | q1(X + C(6)), 172 | q1(X + C(4)), 173 | ], [ 174 | q1(C(5) + C(5)), 175 | q1(C(1) + X), 176 | q1(X + C(9)), 177 | ]), 178 | Level("1", [ // addition 179 | q1(C(5) + C(7)), 180 | q1(C(5) + C(8)), 181 | q1(C(6) + C(6)), 182 | q1(C(9) + C(8)), 183 | q1(C(7) + C(7)), 184 | q1(C(5) + X), 185 | q1(C(4) + C(9)), 186 | q1(C(9) + C(6)), 187 | ], [ 188 | q1(C(7) + C(8)), 189 | q1(C(2) + C(9)), 190 | q1(X + X), 191 | ]), 192 | Level("2", [ // subtraction 193 | q1(C(6) - C(3)), 194 | q1(C(0) - C(3)), 195 | q1(-C(3)), 196 | q1(-C(5)), 197 | q1(C(7) - C(8)), 198 | q1(C(2) - C(6)), 199 | q1(C(1) - C(9)), 200 | ], [ 201 | q1(X - C(7)), 202 | q1(-C(1)), 203 | q1(-X), 204 | ]), 205 | Level("3", [ // multiplication 206 | q1(C(2) * C(3)), // below 11 207 | q1(C(5) * C(5)), // first above 22 208 | q1(C(6) * C(9)), // way larger 209 | q1(C(1) * C(0)), // 1 210 | q1(C(1) * X), // 1 211 | q1(C(7) * C(5)), 212 | q1(X * C(6)), 213 | q1(C(9) * C(9)), 214 | ], [ 215 | q1(C(7) * C(8)), 216 | q1(X * C(0)), 217 | q1(X * X), 218 | ]), 219 | Level("4", [ // division by 2 220 | q1(C(4) * C(2)), 221 | q1(C(8) / C(2)), 222 | q1(C(6) * C(2)), 223 | q1(C(1) / C(2)), 224 | q1(C(3) / C(2)), 225 | q1(C(7) * C(2)), 226 | q1(C(2) / C(2)), 227 | q1(C(6) / C(2)), 228 | q1(C(7) / C(2)), 229 | ], [ 230 | q1(C(5) / C(2)), 231 | q1(C(9) / C(2)), 232 | q1(X / C(2)), 233 | ]), 234 | Level("5", [ // division 235 | q1(C(1) / C(3)), 236 | q1(C(2) / C(3)), // double of previous 237 | q1(C(5) / C(4)), 238 | q1(X / C(4)), // double of previous 239 | q1(C(1) / C(6)), // denominator of exam 240 | q1(C(1) / C(9)), 241 | q1(C(3) / C(8)), 242 | q1(C(7) / C(5)), 243 | ], [ 244 | q1(C(1) / C(7)), 245 | q1(C(1) / X), 246 | q1(C(5) / C(6)), 247 | ]), 248 | Level("6", [ // addition 249 | q2(dot(1,3) + dot(4,0)), 250 | q2(dot(4,2) + dot(8,0)), // intentionally 1.2 vs 12=4+8 251 | q2(dot(4,4) + dot(0,1)), 252 | q2(dot(4,4) + dot(0,8)), 253 | q2(dot(5,5) + dot(9,9)), // componentwise structure 254 | q2(dot(7,X) + dot(X,5)), 255 | q2(dot(6,7) + dot(9,7)), 256 | ], [ 257 | q2(dot(0,X) + dot(X,X)), 258 | q2(dot(5,5) + dot(8,X)), 259 | q2(dot(X,6) + dot(X,6)), 260 | ]), 261 | Level("7", [ // multiplication (result partial 1) 262 | q2(dot(2,0) * dot(4,0)), 263 | q2(dot(3,0) * dot(6,0)), 264 | q2(dot(8,0) * dot(8,0)), 265 | Q((dot(0,1) * dot(0,1)).eq(dot(y,0)), [y]), 266 | q2(dot(0,1) * dot(0,3)), 267 | q2(dot(0,X) * dot(0,1)), 268 | q2(dot(0,5) * dot(0,4)), 269 | q2(dot(0,8) * dot(0,8)), 270 | ], [ 271 | q2(dot(7,0) * dot(9,0)), 272 | q2(dot(0,7) * dot(0,9)), 273 | q2(dot(0,X) * dot(0,X)), 274 | ]), 275 | Level("8", [ // multiplication (result partial 2) 276 | q2(dot(1,0) * dot(7,0)), 277 | q2(dot(1,0) * dot(X,0)), 278 | q2(dot(1,0) * dot(0,1)), 279 | q2(dot(2,0) * dot(0,1)), 280 | q2(dot(2,0) * dot(0,7)), 281 | q2(dot(2,0) * dot(1,0)), 282 | q2(dot(0,2) * dot(1,0)), // commutative 283 | q2(dot(0,5) * dot(6,0)), 284 | q2(dot(0,8) * dot(3,0)), 285 | q2(dot(0,6) * dot(9,0)), 286 | ], [ 287 | q2(dot(7,0) * dot(0,3)), 288 | q2(dot(0,7) * dot(3,0)), 289 | q2(dot(0,X) * dot(X,0)), 290 | ]), 291 | Level("8.1", [ // multiplication (factor partial 1) 292 | q2(dot(1,0) * dot(2,0)), 293 | q2(dot(1,0) * dot(0,4)), 294 | q2(dot(1,0) * dot(2,4)), 295 | q2(dot(2,0) * dot(2,4)), 296 | q2(dot(4,0) * dot(2,4)), 297 | q2(dot(7,0) * dot(1,8)), 298 | q2(dot(7,3) * dot(7,0)), // commutative 299 | q2(dot(2,9) * dot(2,0)), 300 | ], [ 301 | q2(dot(6,0) * dot(3,9)), 302 | q2(dot(2,8) * dot(5,0)), 303 | q2(dot(7,X) * dot(X,0)), 304 | ]), 305 | Level("8.2", [ // multiplication (factor partial 2) 306 | q2(dot(1,0) * dot(0,1)), 307 | q2(dot(0,3) * dot(0,1)), 308 | q2(dot(1,3) * dot(0,1)), // first complicated case 309 | q2(dot(1,3) * dot(0,5)), 310 | q2(dot(3,0) * dot(0,1)), 311 | q2(dot(0,9) * dot(0,1)), 312 | q2(dot(3,9) * dot(0,1)), 313 | q2(dot(8,X) * dot(0,1)), 314 | q2(dot(2,6) * dot(0,4)), 315 | q2(dot(0,1) * dot(5,4)), // commutative 316 | q2(dot(0,7) * dot(7,3)), 317 | ], [ 318 | q2(dot(5,8) * dot(0,4)), 319 | q2(dot(0,6) * dot(3,9)), 320 | q2(dot(1,1) * dot(0,1)), 321 | ]), 322 | Level("9", [ // multiplication (general) 323 | q2(dot(1,3) * dot(0,4)), 324 | q2(dot(1,3) * dot(2,0)), 325 | q2(dot(1,3) * dot(2,4)), // first general case 326 | q2(dot(3,3) * dot(0,1)), 327 | q2(dot(3,3) * dot(5,0)), 328 | q2(dot(3,3) * dot(5,1)), // repetition 329 | q2(dot(4,3) * dot(0,5), isPartial: true), 330 | q2(dot(4,3) * dot(3,0), isPartial: true), 331 | q2(dot(4,3) * dot(3,5)), // repetition 332 | q2(dot(2,5) * dot(0,4), isPartial: true), 333 | q2(dot(2,5) * dot(2,0), isPartial: true), 334 | q2(dot(2,5) * dot(2,4)), // repetition 335 | q2(dot(5,1) * dot(6,4)), // direct 336 | q2(dot(9,1) * dot(1,9)), // direct 337 | ], [ 338 | q2(dot(1,1) * dot(1,1)), 339 | q2(dot(X,X) * dot(6,4)), 340 | q2(dot(6,7) * dot(8,9)), 341 | ]), 342 | Level("9.1", [ 343 | q2(dot(0,7) * dot(0,4)), 344 | q2(-dot(0,4) * dot(0,4)), 345 | q2(dot(2,7) * dot(2,4)), 346 | Q((dot(2,y) * dot(2,1)).eq(dot(z,0)), [y,z]), // well-defined for non-zero first component 347 | Q((dot(1,y) * dot(1,3)).eq(dot(z,0)), [y,z]), 348 | Q((dot(2,y) * dot(2,2)).eq(dot(z,0)), [y,z]), 349 | Q((dot(5,y) * dot(5,5)).eq(dot(z,0)), [y,z]), 350 | ], [ 351 | Q((dot(1,y) * dot(1,9)).eq(dot(z,0)), [y,z]), 352 | Q((dot(3,y) * dot(3,X)).eq(dot(z,0)), [y,z]), 353 | Q((dot(X,y) * dot(X,X)).eq(dot(z,0)), [y,z]), 354 | ]), 355 | Level("9.2", [ // division (divisor partial) 356 | q2(dot(4,0) / dot(2,0)), 357 | q2(dot(2,0) / dot(2,0)), 358 | q2(dot(1,0) / dot(2,0)), 359 | q2(dot(1,6) / dot(2,0)), 360 | q2(dot(6,4) / dot(3,0)), 361 | q2(dot(5,2) / dot(4,0)), 362 | ], [ 363 | q2(dot(8,5) / dot(2,0)), 364 | q2(dot(3,X) / dot(7,0)), 365 | q2(dot(6,2) / dot(X,0)), 366 | ]), 367 | Level("10", [ // division (general) 368 | Q((dot(1,y) * dot(1,1)).eq(dot(z,0)), [y,z]), 369 | q2(dot(2,0) / dot(1,1)), 370 | q2(dot(1,0) / dot(1,1)), 371 | Q((dot(1,y) * dot(1,2)).eq(dot(z,0)), [y,z]), 372 | q2(dot(1,0) / dot(1,2)), // more direct 373 | Q((dot(1,y) * dot(1,3)).eq(dot(z,0)), [y,z]), 374 | q2(dot(1,0) / dot(1,3)), // repetition 375 | q2(dot(5,0) / dot(1,3)), 376 | Q((dot(4,y) * dot(4,5)).eq(dot(z,0)), [y,z], isPartial: true), 377 | q2(dot(1,0) / dot(4,5)), // repetition 378 | q2(dot(2,7) / dot(4,5)), // fully general case 379 | Q((dot(8,y) * dot(8,9)).eq(dot(z,0)), [y,z], isPartial: true), 380 | q2(dot(1,0) / dot(8,9), isPartial: true), 381 | q2(dot(2,6) / dot(8,9)), // repetition 382 | q2(dot(1,0) / dot(5,4)), // direct 383 | q2(dot(1,0) / dot(9,2)), // direct 384 | q2(dot(2,3) / dot(4,7)), // direct 385 | q2(dot(8,5) / dot(3,6)), // direct 386 | ], [ 387 | q2(dot(1,0) / dot(2,1)), 388 | q2(dot(3,1) / dot(3,X)), 389 | q2(dot(5,8) / dot(0,1)), 390 | ]), 391 | Level("11", [ // squares 392 | Q(dot(4,0).eq(dot(y,0).square()), [y]), 393 | Q(dot(9,0).eq(yz.square()), [y,z]), 394 | Q(dot(1,0).eq(yz.square()), [y,z]), 395 | Q(dot(X,0).eq(yz.square()), [y,z]), 396 | ], [ 397 | Q(dot(5,0).eq(yz.square()), [y,z]), 398 | Q(dot(2,0).eq(yz.square()), [y,z]), 399 | Q(dot(8,0).eq(yz.square()), [y,z]), 400 | ]), 401 | ]; 402 | 403 | final DatabaseWrapper db; 404 | int _inputCount = 0; 405 | int _doStatusAnimationAtCount = -1; 406 | int _doScrollAtCount = -1; 407 | int _doJumpAtCount = -1; 408 | int _doRedrawAtCount = -1; 409 | Game(this.db); 410 | 411 | bool doStatusAnimation() { 412 | return _doStatusAnimationAtCount == _inputCount; 413 | } 414 | ScrollType doScrollAnimation() { 415 | if (_doJumpAtCount == _inputCount) { 416 | return ScrollType.jump; 417 | } else if (_doScrollAtCount == _inputCount) { 418 | return ScrollType.smooth; 419 | } else { 420 | return ScrollType.none; 421 | } 422 | } 423 | void doneScrollAnimation() { 424 | if (doScrollAnimation() != ScrollType.none) { 425 | _inputCount++; // ensures that we only scroll once (otherwise, scrolling up and back down manually would retrigger the scroll animation) 426 | } else { 427 | // We have already left the scroll conditions due to some other input, so we do nothing to reduce risk of messing up the state. 428 | // TODO A safer implementation would pass the original _inputCount as token and check 429 | // whether it is still the same, or whether a scroll has already been performed for that token. 430 | } 431 | } 432 | bool doRedrawEverything() { 433 | return _doRedrawAtCount == _inputCount; 434 | } 435 | 436 | final List _activeLevelStack = [0]; 437 | int get activeLevel => _activeLevelStack.last; 438 | set activeLevel(int level) { 439 | _activeLevelStack[_activeLevelStack.length - 1] = level; 440 | } 441 | bool get inExamScreen => _activeLevelStack.length == 1; 442 | int levelsUnlocked = 0; 443 | bool get finished => levelsUnlocked >= levels.length; 444 | // To make it more intuitive to navigate to the exercises page, we initially hide the exams on the first few levels. 445 | // For higher levels, we show the exam immediately to motivate the goal of the level and to allow shortcuts. 446 | bool _exam123Unlocked(int i) => levelsUnlocked > i || levels[i].clicked || levels[i].exercise.questions.any((q) => q.inputs.isNotEmpty); 447 | bool examUnlocked(int i) => i <= levelsUnlocked && (i < 1 || i > 3 || _exam123Unlocked(i)) || debugUnlockAll; 448 | 449 | KeyEventResult keyPressed(String key) { 450 | if (key != 'backspace' && RegExp(r"[\dX]$").matchAsPrefix(key) == null) { 451 | return KeyEventResult.ignored; // ignore invalid keys 452 | } 453 | final l = levels[activeLevel]; 454 | final q = inExamScreen ? l.exam.activeQuestion : l.exercise.activeQuestion; 455 | final numBlanks = '?'.allMatches(q.q).length; 456 | if (!inExamScreen || examUnlocked(activeLevel)) { 457 | _inputCount++; 458 | if (key == 'backspace') { 459 | if (q.inputs.isNotEmpty) { 460 | q.inputs.removeLast(); 461 | } else { 462 | _movePrevious(l); 463 | } 464 | } else { // ordinary key 465 | if (q.inputs.length >= numBlanks) { 466 | q.inputs.clear(); 467 | } 468 | q.inputs.add(key); 469 | if (q.inputs.length == numBlanks) { 470 | _moveNext(l); 471 | } 472 | } 473 | } // else this exam is not yet unlocked, so we ignore the input (relevant for exam 1 only) 474 | notifyListeners(); 475 | db.storeAnswer(l, q).then((_) => storeLevelsUnlocked()); // asynchronous (order of database store events is not that important) 476 | return KeyEventResult.handled; 477 | } 478 | 479 | void _movePrevious(Level l) { 480 | final qq = inExamScreen ? l.exam : l.exercise; 481 | if (qq.activeIndex > 0) { 482 | qq.activeIndex -= 1; 483 | if (qq.activeQuestion.inputs.isNotEmpty) { 484 | qq.activeQuestion.inputs.removeLast(); 485 | } 486 | } 487 | } 488 | 489 | void _moveNext(Level l) { 490 | final qq = inExamScreen ? l.exam : l.exercise; 491 | final status = qq.activeFullQuestionStatus(); 492 | if (status == QuestionsStatus.wrong) { 493 | _doStatusAnimationAtCount = _inputCount; 494 | } 495 | if (qq.activeIndex < qq.questions.length - 1) { 496 | if (qq.activeQuestion.isPartial || status == QuestionsStatus.correct) { 497 | if (!qq.activeQuestion.isPartial) { // for partial questions, we have already scrolled to the end of the group 498 | _doScrollAtCount = _inputCount; 499 | } 500 | qq.activeIndex += 1; 501 | } 502 | } 503 | if (inExamScreen && activeLevel <= levels.length - 1 && l.isSolved()) { 504 | _doScrollAtCount = _inputCount; 505 | if (activeLevel == levels.length - 1) { 506 | levelsUnlocked = levels.length; // i.e. larger than last level, signalling the game is finished 507 | } else { // activeLevel < levels.length - 1 508 | activeLevel += 1; 509 | if (activeLevel > levelsUnlocked) { 510 | levelsUnlocked = activeLevel; 511 | } 512 | } 513 | } 514 | } 515 | 516 | void levelTapped(int questionIdx, {required bool inExam, int levelIdx = 0}) { 517 | _inputCount++; 518 | if (inExam) { 519 | activeLevel = levelIdx; 520 | levels[activeLevel].exam.activeIndex = questionIdx; 521 | } else { 522 | levels[activeLevel].exercise.activeIndex = questionIdx; 523 | } 524 | notifyListeners(); 525 | } 526 | 527 | void pushLevel(int levelIdx) { 528 | if (inExamScreen) { 529 | _inputCount++; 530 | _activeLevelStack.add(levelIdx); 531 | levels[activeLevel].clicked = true; 532 | // we do not notify listeners here to avoid flicker, as the screen is replaced by LevelScreen anyway 533 | } 534 | } 535 | void popLevel() { 536 | if (!inExamScreen) { 537 | _inputCount++; 538 | final last = _activeLevelStack.removeLast(); 539 | if (last == _activeLevelStack.last) { 540 | // To avoid unnecessary jumps, we only scroll to the end of the questions 541 | // when returning from the level that is also currently selected 542 | // (e.g. when the exam questions of the first levels are made visible) 543 | _doJumpAtCount = _inputCount; 544 | } 545 | _doRedrawAtCount = _inputCount; // redraw all exams, so that in particular the first levels become visible when they are uncovered (see regression tests) 546 | notifyListeners(); // to notify about change of active level 547 | } 548 | } 549 | 550 | void popSettings() { 551 | // This method is needed as a workaround for an issue introduced with flutter 3.3.0: 552 | // When the theme changes, the exam questions do not redraw anymore, 553 | // so we force a redraw by notifying about a game change. 554 | _inputCount++; 555 | _doRedrawAtCount = _inputCount; // this allows more lazy redrawing of exams (using selectors) whenever global settings are not involved 556 | notifyListeners(); 557 | } 558 | 559 | Future recomputeExamsState() async { 560 | // we store and load the id instead of the index to handle new levels 561 | // inserted before the currently unlocked level 562 | final levelId = await db.loadKeyValue(levelsUnlockedKey); 563 | for (var i = 0; i < levels.length; i++) { 564 | Level l = levels[i]; 565 | // restore activeExamQuestion 566 | for (var j = 0; j < l.exam.questions.length; j++) { 567 | l.exam.activeIndex = j; 568 | if (l.exam.activeQuestion.status() == QuestionsStatus.partial) { 569 | break; 570 | } 571 | } 572 | // restore unlocked and clicked status 573 | if (l.id == levelId) { 574 | levelsUnlocked = i; 575 | if (i == levels.length - 1 && l.isSolved()) { 576 | levelsUnlocked = levels.length; // game is finished 577 | } 578 | if (!l.clicked && l.exercise.questions.any((q) => q.inputs.isNotEmpty)) { 579 | l.clicked = true; // this avoids unnecessary arrow animation after relaunch 580 | } 581 | // The following sets cursor to highest unlocked level (also makes autoscroll after relaunch work for first levels when exam is made visible). 582 | // (automatically jumping the scrollable ListView to highest unlocked level at launch would be more difficult to implement though, 583 | // since only the visible part of the list is rendered) 584 | activeLevel = i; 585 | break; 586 | } 587 | } 588 | } 589 | 590 | Future storeLevelsUnlocked() { 591 | final levelId = levels[levelsUnlocked < levels.length ? levelsUnlocked : levels.length - 1].id; 592 | return db.storeKeyValue(levelsUnlockedKey, levelId); 593 | } 594 | 595 | Future resetProgress() async { 596 | levelsUnlocked = 0; 597 | await storeLevelsUnlocked(); 598 | await db.deleteAnswers(levels); 599 | } 600 | 601 | Future loadGameState() async { 602 | // initialization of state from database (to be called once for initialization) 603 | final answers = await db.loadAnswers(levels); 604 | for (final level in levels) { 605 | for (final question in level.exercise.questions.followedBy(level.exam.questions)) { 606 | final answer = answers[question.fullId(level)]; 607 | if (answer != null) { 608 | question.updateInputs(question.unstringifyInputs(answer)); 609 | } 610 | } 611 | } 612 | await recomputeExamsState(); 613 | } 614 | 615 | static Future initializedGame(DatabaseWrapper db, {required bool loadProgress}) async { 616 | final game = Game(db); 617 | if (loadProgress) { 618 | await game.loadGameState(); 619 | } 620 | return game; 621 | } 622 | } 623 | -------------------------------------------------------------------------------- /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: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "64.0.0" 12 | analyzer: 13 | dependency: transitive 14 | description: 15 | name: analyzer 16 | sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "6.2.0" 20 | archive: 21 | dependency: transitive 22 | description: 23 | name: archive 24 | sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "3.4.10" 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: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "2.11.0" 44 | boolean_selector: 45 | dependency: transitive 46 | description: 47 | name: boolean_selector 48 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "2.1.1" 52 | characters: 53 | dependency: transitive 54 | description: 55 | name: characters 56 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "1.3.0" 60 | checked_yaml: 61 | dependency: transitive 62 | description: 63 | name: checked_yaml 64 | sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "2.0.3" 68 | cli_util: 69 | dependency: transitive 70 | description: 71 | name: cli_util 72 | sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "0.4.1" 76 | clock: 77 | dependency: transitive 78 | description: 79 | name: clock 80 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "1.1.1" 84 | collection: 85 | dependency: transitive 86 | description: 87 | name: collection 88 | sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a 89 | url: "https://pub.dev" 90 | source: hosted 91 | version: "1.18.0" 92 | convert: 93 | dependency: transitive 94 | description: 95 | name: convert 96 | sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" 97 | url: "https://pub.dev" 98 | source: hosted 99 | version: "3.1.1" 100 | coverage: 101 | dependency: transitive 102 | description: 103 | name: coverage 104 | sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76" 105 | url: "https://pub.dev" 106 | source: hosted 107 | version: "1.7.2" 108 | crypto: 109 | dependency: transitive 110 | description: 111 | name: crypto 112 | sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab 113 | url: "https://pub.dev" 114 | source: hosted 115 | version: "3.0.3" 116 | cupertino_icons: 117 | dependency: "direct main" 118 | description: 119 | name: cupertino_icons 120 | sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d 121 | url: "https://pub.dev" 122 | source: hosted 123 | version: "1.0.6" 124 | fake_async: 125 | dependency: transitive 126 | description: 127 | name: fake_async 128 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 129 | url: "https://pub.dev" 130 | source: hosted 131 | version: "1.3.1" 132 | ffi: 133 | dependency: transitive 134 | description: 135 | name: ffi 136 | sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" 137 | url: "https://pub.dev" 138 | source: hosted 139 | version: "2.1.0" 140 | file: 141 | dependency: transitive 142 | description: 143 | name: file 144 | sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" 145 | url: "https://pub.dev" 146 | source: hosted 147 | version: "7.0.0" 148 | flex_color_scheme: 149 | dependency: "direct main" 150 | description: 151 | name: flex_color_scheme 152 | sha256: "32914024a4f404d90ff449f58d279191675b28e7c08824046baf06826e99d984" 153 | url: "https://pub.dev" 154 | source: hosted 155 | version: "7.3.1" 156 | flex_seed_scheme: 157 | dependency: transitive 158 | description: 159 | name: flex_seed_scheme 160 | sha256: "29c12aba221eb8a368a119685371381f8035011d18de5ba277ad11d7dfb8657f" 161 | url: "https://pub.dev" 162 | source: hosted 163 | version: "1.4.0" 164 | flutter: 165 | dependency: "direct main" 166 | description: flutter 167 | source: sdk 168 | version: "0.0.0" 169 | flutter_launcher_icons: 170 | dependency: "direct dev" 171 | description: 172 | name: flutter_launcher_icons 173 | sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" 174 | url: "https://pub.dev" 175 | source: hosted 176 | version: "0.13.1" 177 | flutter_lints: 178 | dependency: "direct dev" 179 | description: 180 | name: flutter_lints 181 | sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 182 | url: "https://pub.dev" 183 | source: hosted 184 | version: "3.0.1" 185 | flutter_localizations: 186 | dependency: "direct main" 187 | description: flutter 188 | source: sdk 189 | version: "0.0.0" 190 | flutter_test: 191 | dependency: "direct dev" 192 | description: flutter 193 | source: sdk 194 | version: "0.0.0" 195 | flutter_web_plugins: 196 | dependency: transitive 197 | description: flutter 198 | source: sdk 199 | version: "0.0.0" 200 | frontend_server_client: 201 | dependency: transitive 202 | description: 203 | name: frontend_server_client 204 | sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" 205 | url: "https://pub.dev" 206 | source: hosted 207 | version: "3.2.0" 208 | glob: 209 | dependency: transitive 210 | description: 211 | name: glob 212 | sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" 213 | url: "https://pub.dev" 214 | source: hosted 215 | version: "2.1.2" 216 | http: 217 | dependency: transitive 218 | description: 219 | name: http 220 | sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba 221 | url: "https://pub.dev" 222 | source: hosted 223 | version: "1.2.0" 224 | http_multi_server: 225 | dependency: transitive 226 | description: 227 | name: http_multi_server 228 | sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" 229 | url: "https://pub.dev" 230 | source: hosted 231 | version: "3.2.1" 232 | http_parser: 233 | dependency: transitive 234 | description: 235 | name: http_parser 236 | sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" 237 | url: "https://pub.dev" 238 | source: hosted 239 | version: "4.0.2" 240 | image: 241 | dependency: transitive 242 | description: 243 | name: image 244 | sha256: "004a2e90ce080f8627b5a04aecb4cdfac87d2c3f3b520aa291260be5a32c033d" 245 | url: "https://pub.dev" 246 | source: hosted 247 | version: "4.1.4" 248 | intl: 249 | dependency: "direct main" 250 | description: 251 | name: intl 252 | sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" 253 | url: "https://pub.dev" 254 | source: hosted 255 | version: "0.18.1" 256 | io: 257 | dependency: transitive 258 | description: 259 | name: io 260 | sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" 261 | url: "https://pub.dev" 262 | source: hosted 263 | version: "1.0.4" 264 | js: 265 | dependency: transitive 266 | description: 267 | name: js 268 | sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 269 | url: "https://pub.dev" 270 | source: hosted 271 | version: "0.6.7" 272 | json_annotation: 273 | dependency: transitive 274 | description: 275 | name: json_annotation 276 | sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 277 | url: "https://pub.dev" 278 | source: hosted 279 | version: "4.8.1" 280 | lints: 281 | dependency: transitive 282 | description: 283 | name: lints 284 | sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 285 | url: "https://pub.dev" 286 | source: hosted 287 | version: "3.0.0" 288 | localstorage: 289 | dependency: "direct main" 290 | description: 291 | name: localstorage 292 | sha256: fdff4f717114e992acfd4045dc4a9ab9b987ca57f020965d63e3eb34089c60d8 293 | url: "https://pub.dev" 294 | source: hosted 295 | version: "4.0.1+4" 296 | logging: 297 | dependency: transitive 298 | description: 299 | name: logging 300 | sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" 301 | url: "https://pub.dev" 302 | source: hosted 303 | version: "1.2.0" 304 | matcher: 305 | dependency: transitive 306 | description: 307 | name: matcher 308 | sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" 309 | url: "https://pub.dev" 310 | source: hosted 311 | version: "0.12.16" 312 | material_color_utilities: 313 | dependency: transitive 314 | description: 315 | name: material_color_utilities 316 | sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" 317 | url: "https://pub.dev" 318 | source: hosted 319 | version: "0.5.0" 320 | meta: 321 | dependency: transitive 322 | description: 323 | name: meta 324 | sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e 325 | url: "https://pub.dev" 326 | source: hosted 327 | version: "1.10.0" 328 | mime: 329 | dependency: transitive 330 | description: 331 | name: mime 332 | sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e 333 | url: "https://pub.dev" 334 | source: hosted 335 | version: "1.0.4" 336 | nested: 337 | dependency: transitive 338 | description: 339 | name: nested 340 | sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" 341 | url: "https://pub.dev" 342 | source: hosted 343 | version: "1.0.0" 344 | node_preamble: 345 | dependency: transitive 346 | description: 347 | name: node_preamble 348 | sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" 349 | url: "https://pub.dev" 350 | source: hosted 351 | version: "2.0.2" 352 | package_config: 353 | dependency: transitive 354 | description: 355 | name: package_config 356 | sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" 357 | url: "https://pub.dev" 358 | source: hosted 359 | version: "2.1.0" 360 | package_info_plus: 361 | dependency: "direct main" 362 | description: 363 | name: package_info_plus 364 | sha256: "88bc797f44a94814f2213db1c9bd5badebafdfb8290ca9f78d4b9ee2a3db4d79" 365 | url: "https://pub.dev" 366 | source: hosted 367 | version: "5.0.1" 368 | package_info_plus_platform_interface: 369 | dependency: transitive 370 | description: 371 | name: package_info_plus_platform_interface 372 | sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" 373 | url: "https://pub.dev" 374 | source: hosted 375 | version: "2.0.1" 376 | path: 377 | dependency: "direct main" 378 | description: 379 | name: path 380 | sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" 381 | url: "https://pub.dev" 382 | source: hosted 383 | version: "1.8.3" 384 | path_provider: 385 | dependency: transitive 386 | description: 387 | name: path_provider 388 | sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b 389 | url: "https://pub.dev" 390 | source: hosted 391 | version: "2.1.2" 392 | path_provider_android: 393 | dependency: transitive 394 | description: 395 | name: path_provider_android 396 | sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" 397 | url: "https://pub.dev" 398 | source: hosted 399 | version: "2.2.2" 400 | path_provider_foundation: 401 | dependency: transitive 402 | description: 403 | name: path_provider_foundation 404 | sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" 405 | url: "https://pub.dev" 406 | source: hosted 407 | version: "2.3.2" 408 | path_provider_linux: 409 | dependency: transitive 410 | description: 411 | name: path_provider_linux 412 | sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 413 | url: "https://pub.dev" 414 | source: hosted 415 | version: "2.2.1" 416 | path_provider_platform_interface: 417 | dependency: transitive 418 | description: 419 | name: path_provider_platform_interface 420 | sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" 421 | url: "https://pub.dev" 422 | source: hosted 423 | version: "2.1.2" 424 | path_provider_windows: 425 | dependency: transitive 426 | description: 427 | name: path_provider_windows 428 | sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" 429 | url: "https://pub.dev" 430 | source: hosted 431 | version: "2.2.1" 432 | petitparser: 433 | dependency: transitive 434 | description: 435 | name: petitparser 436 | sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 437 | url: "https://pub.dev" 438 | source: hosted 439 | version: "6.0.2" 440 | platform: 441 | dependency: transitive 442 | description: 443 | name: platform 444 | sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" 445 | url: "https://pub.dev" 446 | source: hosted 447 | version: "3.1.4" 448 | plugin_platform_interface: 449 | dependency: transitive 450 | description: 451 | name: plugin_platform_interface 452 | sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" 453 | url: "https://pub.dev" 454 | source: hosted 455 | version: "2.1.8" 456 | pointycastle: 457 | dependency: transitive 458 | description: 459 | name: pointycastle 460 | sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" 461 | url: "https://pub.dev" 462 | source: hosted 463 | version: "3.7.4" 464 | pool: 465 | dependency: transitive 466 | description: 467 | name: pool 468 | sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" 469 | url: "https://pub.dev" 470 | source: hosted 471 | version: "1.5.1" 472 | provider: 473 | dependency: "direct main" 474 | description: 475 | name: provider 476 | sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" 477 | url: "https://pub.dev" 478 | source: hosted 479 | version: "6.1.1" 480 | pub_semver: 481 | dependency: transitive 482 | description: 483 | name: pub_semver 484 | sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" 485 | url: "https://pub.dev" 486 | source: hosted 487 | version: "2.1.4" 488 | shelf: 489 | dependency: transitive 490 | description: 491 | name: shelf 492 | sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 493 | url: "https://pub.dev" 494 | source: hosted 495 | version: "1.4.1" 496 | shelf_packages_handler: 497 | dependency: transitive 498 | description: 499 | name: shelf_packages_handler 500 | sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" 501 | url: "https://pub.dev" 502 | source: hosted 503 | version: "3.0.2" 504 | shelf_static: 505 | dependency: transitive 506 | description: 507 | name: shelf_static 508 | sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e 509 | url: "https://pub.dev" 510 | source: hosted 511 | version: "1.1.2" 512 | shelf_web_socket: 513 | dependency: transitive 514 | description: 515 | name: shelf_web_socket 516 | sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" 517 | url: "https://pub.dev" 518 | source: hosted 519 | version: "1.0.4" 520 | sky_engine: 521 | dependency: transitive 522 | description: flutter 523 | source: sdk 524 | version: "0.0.99" 525 | source_map_stack_trace: 526 | dependency: transitive 527 | description: 528 | name: source_map_stack_trace 529 | sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" 530 | url: "https://pub.dev" 531 | source: hosted 532 | version: "2.1.1" 533 | source_maps: 534 | dependency: transitive 535 | description: 536 | name: source_maps 537 | sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" 538 | url: "https://pub.dev" 539 | source: hosted 540 | version: "0.10.12" 541 | source_span: 542 | dependency: transitive 543 | description: 544 | name: source_span 545 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 546 | url: "https://pub.dev" 547 | source: hosted 548 | version: "1.10.0" 549 | sqflite: 550 | dependency: "direct main" 551 | description: 552 | name: sqflite 553 | sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a" 554 | url: "https://pub.dev" 555 | source: hosted 556 | version: "2.3.0" 557 | sqflite_common: 558 | dependency: transitive 559 | description: 560 | name: sqflite_common 561 | sha256: bb4738f15b23352822f4c42a531677e5c6f522e079461fd240ead29d8d8a54a6 562 | url: "https://pub.dev" 563 | source: hosted 564 | version: "2.5.0+2" 565 | stack_trace: 566 | dependency: transitive 567 | description: 568 | name: stack_trace 569 | sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" 570 | url: "https://pub.dev" 571 | source: hosted 572 | version: "1.11.1" 573 | stream_channel: 574 | dependency: transitive 575 | description: 576 | name: stream_channel 577 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 578 | url: "https://pub.dev" 579 | source: hosted 580 | version: "2.1.2" 581 | string_scanner: 582 | dependency: transitive 583 | description: 584 | name: string_scanner 585 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 586 | url: "https://pub.dev" 587 | source: hosted 588 | version: "1.2.0" 589 | synchronized: 590 | dependency: transitive 591 | description: 592 | name: synchronized 593 | sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" 594 | url: "https://pub.dev" 595 | source: hosted 596 | version: "3.1.0+1" 597 | term_glyph: 598 | dependency: transitive 599 | description: 600 | name: term_glyph 601 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 602 | url: "https://pub.dev" 603 | source: hosted 604 | version: "1.2.1" 605 | test: 606 | dependency: "direct dev" 607 | description: 608 | name: test 609 | sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f 610 | url: "https://pub.dev" 611 | source: hosted 612 | version: "1.24.9" 613 | test_api: 614 | dependency: transitive 615 | description: 616 | name: test_api 617 | sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" 618 | url: "https://pub.dev" 619 | source: hosted 620 | version: "0.6.1" 621 | test_core: 622 | dependency: transitive 623 | description: 624 | name: test_core 625 | sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a 626 | url: "https://pub.dev" 627 | source: hosted 628 | version: "0.5.9" 629 | typed_data: 630 | dependency: transitive 631 | description: 632 | name: typed_data 633 | sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c 634 | url: "https://pub.dev" 635 | source: hosted 636 | version: "1.3.2" 637 | url_launcher: 638 | dependency: "direct main" 639 | description: 640 | name: url_launcher 641 | sha256: d25bb0ca00432a5e1ee40e69c36c85863addf7cc45e433769d61bed3fe81fd96 642 | url: "https://pub.dev" 643 | source: hosted 644 | version: "6.2.3" 645 | url_launcher_android: 646 | dependency: transitive 647 | description: 648 | name: url_launcher_android 649 | sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f" 650 | url: "https://pub.dev" 651 | source: hosted 652 | version: "6.2.2" 653 | url_launcher_ios: 654 | dependency: transitive 655 | description: 656 | name: url_launcher_ios 657 | sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" 658 | url: "https://pub.dev" 659 | source: hosted 660 | version: "6.2.4" 661 | url_launcher_linux: 662 | dependency: transitive 663 | description: 664 | name: url_launcher_linux 665 | sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 666 | url: "https://pub.dev" 667 | source: hosted 668 | version: "3.1.1" 669 | url_launcher_macos: 670 | dependency: transitive 671 | description: 672 | name: url_launcher_macos 673 | sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 674 | url: "https://pub.dev" 675 | source: hosted 676 | version: "3.1.0" 677 | url_launcher_platform_interface: 678 | dependency: transitive 679 | description: 680 | name: url_launcher_platform_interface 681 | sha256: a932c3a8082e118f80a475ce692fde89dc20fddb24c57360b96bc56f7035de1f 682 | url: "https://pub.dev" 683 | source: hosted 684 | version: "2.3.1" 685 | url_launcher_web: 686 | dependency: transitive 687 | description: 688 | name: url_launcher_web 689 | sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b 690 | url: "https://pub.dev" 691 | source: hosted 692 | version: "2.2.3" 693 | url_launcher_windows: 694 | dependency: transitive 695 | description: 696 | name: url_launcher_windows 697 | sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 698 | url: "https://pub.dev" 699 | source: hosted 700 | version: "3.1.1" 701 | vector_math: 702 | dependency: transitive 703 | description: 704 | name: vector_math 705 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 706 | url: "https://pub.dev" 707 | source: hosted 708 | version: "2.1.4" 709 | vm_service: 710 | dependency: transitive 711 | description: 712 | name: vm_service 713 | sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 714 | url: "https://pub.dev" 715 | source: hosted 716 | version: "13.0.0" 717 | watcher: 718 | dependency: transitive 719 | description: 720 | name: watcher 721 | sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" 722 | url: "https://pub.dev" 723 | source: hosted 724 | version: "1.1.0" 725 | web: 726 | dependency: transitive 727 | description: 728 | name: web 729 | sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 730 | url: "https://pub.dev" 731 | source: hosted 732 | version: "0.3.0" 733 | web_socket_channel: 734 | dependency: transitive 735 | description: 736 | name: web_socket_channel 737 | sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b 738 | url: "https://pub.dev" 739 | source: hosted 740 | version: "2.4.0" 741 | webkit_inspection_protocol: 742 | dependency: transitive 743 | description: 744 | name: webkit_inspection_protocol 745 | sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" 746 | url: "https://pub.dev" 747 | source: hosted 748 | version: "1.2.1" 749 | win32: 750 | dependency: transitive 751 | description: 752 | name: win32 753 | sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" 754 | url: "https://pub.dev" 755 | source: hosted 756 | version: "5.2.0" 757 | xdg_directories: 758 | dependency: transitive 759 | description: 760 | name: xdg_directories 761 | sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d 762 | url: "https://pub.dev" 763 | source: hosted 764 | version: "1.0.4" 765 | xml: 766 | dependency: transitive 767 | description: 768 | name: xml 769 | sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 770 | url: "https://pub.dev" 771 | source: hosted 772 | version: "6.5.0" 773 | yaml: 774 | dependency: transitive 775 | description: 776 | name: yaml 777 | sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" 778 | url: "https://pub.dev" 779 | source: hosted 780 | version: "3.1.2" 781 | sdks: 782 | dart: ">=3.2.0 <4.0.0" 783 | flutter: ">=3.16.0" 784 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:math'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:flutter/services.dart'; 7 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 8 | import 'package:provider/provider.dart'; 9 | import 'package:package_info_plus/package_info_plus.dart'; 10 | import 'package:everest/game.dart'; 11 | import 'package:everest/storage.dart'; 12 | import 'package:flex_color_scheme/flex_color_scheme.dart'; 13 | import 'package:url_launcher/link.dart'; 14 | import 'package:url_launcher/url_launcher.dart'; 15 | 16 | const appName = 'Everest'; 17 | const String themeModeKey = 'settings:themeMode'; 18 | const String pureBlackKey = 'settings:pureBlack'; 19 | 20 | class StatusIcon extends StatelessWidget { 21 | final QuestionsStatusCovered statusCovered; 22 | const StatusIcon(this.statusCovered, {Key? key}) : super(key: key); 23 | 24 | static Color color(BuildContext context) { 25 | final light = Theme.of(context).brightness == Brightness.light; 26 | if (light) { 27 | return const Color(0xff888888); // default gray 28 | } else { 29 | return const Color(0xffa3a8bf); // default bluish-white 0xffd5dbfa detoned 30 | } 31 | } 32 | 33 | static QuestionsStatusCovered mkStatusCovered(QuestionsStatus statusSingle, {QuestionsStatus? statusCombined}) { 34 | // if statusCombined is null, then this is not a multi question 35 | if (statusSingle == QuestionsStatus.partial) { 36 | return QuestionsStatusCovered.partial; 37 | } else if (statusCombined == QuestionsStatus.correct || statusCombined == null && statusSingle == QuestionsStatus.correct) { 38 | return QuestionsStatusCovered.correct; 39 | } else if (statusCombined == null && statusSingle == QuestionsStatus.wrong) { 40 | return QuestionsStatusCovered.wrong; 41 | } else { // statusCombined != null and statusSingle is covered (wrong or correct) 42 | return QuestionsStatusCovered.covered; 43 | } 44 | } 45 | 46 | @override 47 | Widget build(BuildContext context) { 48 | final light = Theme.of(context).brightness == Brightness.light; 49 | switch (statusCovered) { 50 | case QuestionsStatusCovered.partial: return Icon(Icons.circle_outlined, color: color(context)); 51 | case QuestionsStatusCovered.correct: return Icon(Icons.check_circle, color: Color(light ? 0xff1ca23e : 0xff2fae49)); 52 | case QuestionsStatusCovered.wrong: return Icon(Icons.cancel, color: Color(light ? 0xffd51529 : 0xfff6313a)); 53 | case QuestionsStatusCovered.covered: return Icon(Icons.help, color: color(context)); 54 | } 55 | } 56 | } 57 | 58 | class DampedCurve extends Curve { 59 | // a linearly damped oscillation in reverse 60 | @override double transformInternal(double t) { 61 | return cos(16*(1-t))*t; 62 | } 63 | } 64 | final _dampedCurve = DampedCurve(); 65 | 66 | class RotateCurve extends Curve { // 90 rotation 67 | @override double transformInternal(double t) => t < 0.5 ? 0 : -cos(t*pi); 68 | } 69 | final _rotateCurve = RotateCurve(); 70 | 71 | class RotateCurve2 extends Curve { // 180 rotation 72 | @override double transformInternal(double t) => t < 0.5 ? 0 : cos(2*t*pi); 73 | } 74 | final _rotateCurve2 = RotateCurve2(); 75 | 76 | class DelayedEaseCurve extends Curve { 77 | // This delay is tweaked such that the down-scrolling to show the new exam 78 | // questions (after return to the main screen) looks smooth rather than janky. 79 | static double delay = 0.35; 80 | @override double transformInternal(double t) => t < delay ? 0 : Curves.ease.transformInternal((t - delay) / (1 - delay)); 81 | } 82 | final _delayedEaseCurve = DelayedEaseCurve(); 83 | 84 | class StatusIconConnector extends StatelessWidget { 85 | const StatusIconConnector({Key? key}) : super(key: key); 86 | @override build(context) => Container(width: 2.5, color: StatusIcon.color(context)); 87 | } 88 | 89 | class AnimatedStatusIcon extends StatefulWidget { 90 | final QuestionsStatus statusCombined, statusSingle; 91 | final bool isFirst, isLast, isMulti; 92 | final bool animateStatusWrong; 93 | const AnimatedStatusIcon(this.statusSingle, {required this.statusCombined, required this.animateStatusWrong, Key? key, required this.isFirst, required this.isLast}) : 94 | isMulti = !(isFirst && isLast), 95 | super(key: key); 96 | 97 | @override 98 | State createState() => _AnimatedStatusIconState(); 99 | } 100 | class _AnimatedStatusIconState extends State 101 | with SingleTickerProviderStateMixin { 102 | late final AnimationController _controller = AnimationController( 103 | duration: const Duration(milliseconds: 70), 104 | reverseDuration: const Duration(milliseconds: 420), 105 | vsync: this, 106 | ); 107 | late final Animation _animation = Tween( 108 | begin: Offset.zero, 109 | end: const Offset(-0.225, 0.0), 110 | ).animate(CurvedAnimation( 111 | parent: _controller, 112 | curve: Curves.easeInOutSine, 113 | reverseCurve: _dampedCurve, 114 | )); 115 | 116 | @override 117 | void dispose() { 118 | _controller.dispose(); 119 | super.dispose(); 120 | } 121 | 122 | Future _runAnimation() async { 123 | try { // circumvents intermediate disposal of _controller 124 | _controller.reset(); // stops previous animation if still in progress 125 | await _controller.forward().orCancel; 126 | await _controller.reverse().orCancel; 127 | } on TickerCanceled { /* ignore */ } 128 | } 129 | 130 | @override 131 | Widget build(BuildContext context) { 132 | final innerIcon = StatusIcon(StatusIcon.mkStatusCovered(widget.statusSingle, statusCombined: widget.isMulti ? widget.statusCombined : null)); 133 | final animSwitcher = AnimatedSwitcher( // icon change animation 134 | duration: const Duration(milliseconds: 330), 135 | transitionBuilder: (Widget child, Animation animation) => VerticalScaleTransition(child, animation, horizontal: widget.isMulti), 136 | switchInCurve: _rotateCurve, 137 | switchOutCurve: _rotateCurve, 138 | child: Container( 139 | key: ValueKey(innerIcon.statusCovered), 140 | child: innerIcon, 141 | ), 142 | ); 143 | if (widget.animateStatusWrong && widget.statusCombined == QuestionsStatus.wrong) { 144 | _runAnimation(); 145 | } 146 | Widget iconWithConnector = SlideTransition( // error animation 147 | position: _animation, 148 | child: !widget.isMulti ? 149 | animSwitcher : 150 | Column( 151 | children: [ 152 | Expanded(child: Visibility(visible: !widget.isFirst, child: const StatusIconConnector())), 153 | animSwitcher, 154 | Expanded(child: Visibility(visible: !widget.isLast, child: const StatusIconConnector())), 155 | ], 156 | ), 157 | ); 158 | return iconWithConnector; 159 | } 160 | } 161 | 162 | class VerticalScaleTransition extends StatefulWidget { 163 | final Widget child; 164 | final Animation animation; 165 | final bool horizontal; 166 | const VerticalScaleTransition(this.child, this.animation, {this.horizontal = false, Key? key}) : super(key: key); 167 | @override createState() => _VerticalScaleTransitionState(); 168 | } 169 | class _VerticalScaleTransitionState extends State { 170 | double scaleY = 1; 171 | void _update() => setState(() => scaleY = widget.animation.value); 172 | @override Widget build(BuildContext context) => Transform.scale( 173 | scaleX: widget.horizontal ? scaleY : 1.0, 174 | scaleY: widget.horizontal ? 1.0 : scaleY, 175 | child: widget.child 176 | ); 177 | @override void initState() { 178 | super.initState(); 179 | scaleY = widget.animation.value; // may be 1.0 or 0.0 depending on whether this is initial creation or animated switch, avoids flickering 180 | widget.animation.addListener(_update); 181 | } 182 | @override void didUpdateWidget(oldWidget) { 183 | super.didUpdateWidget(oldWidget); 184 | if (!identical(oldWidget.animation, widget.animation)) { 185 | oldWidget.animation.removeListener(_update); 186 | scaleY = widget.animation.value; 187 | widget.animation.addListener(_update); 188 | } 189 | } 190 | @override void dispose() { 191 | widget.animation.removeListener(_update); 192 | super.dispose(); 193 | } 194 | } 195 | 196 | final _listTileRadius = BorderRadius.circular(20); 197 | final _listTileRounded = RoundedRectangleBorder(borderRadius: _listTileRadius); 198 | 199 | class QuestionsWidget extends StatelessWidget { 200 | final List questions; 201 | final bool isActive; 202 | final int focussedQuestion; 203 | final bool animateStatusWrong; 204 | final ScrollType doScroll; 205 | final void Function() doneScrollAnimation; 206 | final void Function(int) onTap; 207 | final Widget? trailing; 208 | const QuestionsWidget(this.questions, 209 | {required this.isActive, required this.focussedQuestion, required bool animateStatusWrong, 210 | required ScrollType doScroll, required this.doneScrollAnimation, required this.onTap, this.trailing, Key? key}): 211 | animateStatusWrong = animateStatusWrong && isActive, 212 | doScroll = isActive ? doScroll : ScrollType.none, 213 | super(key: key); 214 | 215 | @override 216 | Widget build(BuildContext context) { 217 | final status = jointStatus(questions); // TODO cache this? 218 | final c = Column(children: [ 219 | ...Iterable.generate(questions.length, (i) { 220 | // the following is more direct than expr.str() and works since all variables appear exactly once from left to right 221 | final q = questions[i].inputs.fold(questions[i].q, (q, s) => q.replaceFirst('?', s)); 222 | Widget? t; 223 | if (isActive && i == focussedQuestion) { 224 | var j = q.indexOf('?'); 225 | if (j == -1) { 226 | j = questions[i].q.indexOf('?'); // relies on all replacements being single characters 227 | } 228 | if (j != -1) { 229 | t = Text.rich( 230 | TextSpan(text: q.substring(0, j), style: _biggerFontMath, children: [ 231 | WidgetSpan( // with padding 232 | child: Container( 233 | padding: const EdgeInsets.symmetric(horizontal: 3.0), 234 | decoration: BoxDecoration( 235 | color: Theme.of(context).textSelectionTheme.selectionColor, 236 | borderRadius: const BorderRadius.all(Radius.circular(3.0)), 237 | ), 238 | child: Text(q.substring(j, j+1), 239 | style: _biggerFontMath, 240 | textScaler: TextScaler.noScaling, // important in order to avoid scaling twice with enlarged system font settings 241 | ), 242 | ), 243 | ), 244 | ...(q.length > j+1 ? [TextSpan(text: q.substring(j+1))] : []), 245 | ]), 246 | textScaler: MediaQuery.textScalerOf(context), // for consistent font sizes with enlarged system font settings 247 | ); 248 | } 249 | } 250 | t ??= Text(q, style: _biggerFontMath); 251 | return ListTile( 252 | title: t, 253 | trailing: AnimatedStatusIcon( 254 | questions[i].status(), 255 | statusCombined: status, 256 | animateStatusWrong: animateStatusWrong, 257 | isFirst: i == 0, 258 | isLast: i == questions.length - 1, 259 | ), 260 | shape: _listTileRounded, 261 | onTap: () => onTap(i) 262 | ); 263 | }), 264 | if (trailing != null) trailing!, 265 | ]); 266 | if (doScroll != ScrollType.none) { 267 | WidgetsBinding.instance.addPostFrameCallback((_) { 268 | // for details about scrolling see https://stackoverflow.com/q/49153087 269 | Scrollable.ensureVisible(context, 270 | alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd, 271 | duration: doScroll == ScrollType.jump ? Duration(milliseconds: (800 / (1 - DelayedEaseCurve.delay)).round()) : const Duration(milliseconds: 800), 272 | curve: doScroll == ScrollType.jump ? _delayedEaseCurve : Curves.ease, // formerly this was a `jump`, now it is just a delayed smooth scroll 273 | ); 274 | doneScrollAnimation(); 275 | }); 276 | } 277 | // Here we are careful to keep the widget tree the same regardless of whether widget is active, 278 | // since otherwise the status switch animation does not show. 279 | return Container( 280 | decoration: ShapeDecoration( 281 | color: isActive ? Theme.of(context).highlightColor : null, // makes hover work on non-selected tiles and background color in pure black mode 282 | shape: _listTileRounded, // TODO for pure black, add (side: const BorderSide(color: ...)), 283 | ), 284 | child: c 285 | ); 286 | } 287 | } 288 | 289 | class BouncingWidget extends StatefulWidget { 290 | final Widget child; 291 | const BouncingWidget(this.child, {Key? key}) : super(key: key); 292 | 293 | @override 294 | State createState() => _BouncingWidgetState(); 295 | } 296 | 297 | class _BouncingWidgetState extends State 298 | with SingleTickerProviderStateMixin { 299 | // see https://api.flutter.dev/flutter/widgets/SlideTransition-class.html 300 | late final AnimationController _controller = AnimationController( 301 | duration: const Duration(milliseconds: 400), 302 | reverseDuration: const Duration(milliseconds: 800), 303 | vsync: this, 304 | ); 305 | late final Animation _offsetAnimation = Tween( 306 | begin: Offset.zero, 307 | end: const Offset(-1.0, 0.0), 308 | ).animate(CurvedAnimation( 309 | parent: _controller, 310 | curve: Curves.easeInOutBack, 311 | reverseCurve: Curves.bounceIn, 312 | )); 313 | int _cycleCount = 0; 314 | bool _showInfo = false; // after some cycles, show info icon to draw attention to exercises page 315 | 316 | // instead of a permanent long animation, we use a timer with a short animation to avoid permanent high cpu usage 317 | late final Timer _timer; 318 | _BouncingWidgetState() { 319 | // despite the `late`, defining the timer here in the constructor (in contrast 320 | // to initializing it at declaration) ensures that it is actually started 321 | _timer = Timer.periodic(const Duration(seconds: 8), (timer) async { 322 | await _controller.forward(); 323 | await _controller.reverse(); 324 | _cycleCount++; 325 | if (_cycleCount >= 3 && !_showInfo) { 326 | setState(() => _showInfo = true); 327 | } 328 | }); 329 | } 330 | 331 | @override 332 | void dispose() { 333 | _timer.cancel(); 334 | _controller.dispose(); 335 | super.dispose(); 336 | } 337 | 338 | @override 339 | Widget build(BuildContext context) { 340 | return SlideTransition( 341 | position: _offsetAnimation, 342 | child: Row( 343 | mainAxisSize: MainAxisSize.min, 344 | children: [ 345 | if (_showInfo) const Icon(Icons.info_outline), 346 | widget.child, 347 | ], 348 | ), 349 | ); 350 | } 351 | } 352 | 353 | Iterable interleave(Iterable it, A separator) { 354 | return it.expand((a) => [separator, a]).skip(1); 355 | } 356 | 357 | const listPadding = EdgeInsets.all(8.0); 358 | const _biggerFont = TextStyle(fontSize: 18.0); 359 | final _biggerFontMath = _biggerFont.copyWith(fontFamily: 'NotoSansMath', fontFamilyFallback: ['NotoSans']); 360 | final _biggerFontMathButton = _biggerFontMath.copyWith(fontSize: 20.0); // NotoSansMath appears to be better vertically centered than NotoSans 361 | 362 | class KeyboardButton extends StatelessWidget { 363 | static const _keyIcons = { 364 | 'backspace': Icons.backspace, 365 | }; 366 | final String label; 367 | const KeyboardButton(this.label, {Key? key}) : super(key: key); 368 | 369 | @override 370 | Widget build(BuildContext context) { 371 | return Consumer(builder: (context, game, child) => 372 | Padding( 373 | padding: const EdgeInsets.all(2), 374 | child: SizedBox( 375 | height: 44, 376 | width: 66, 377 | child: OutlinedButton( 378 | child: _keyIcons.containsKey(label) ? Icon(_keyIcons[label], size: 26) : Text(label, style: _biggerFontMathButton), 379 | onPressed: () => game.keyPressed(label), 380 | ), 381 | ), 382 | ), 383 | ); 384 | } 385 | } 386 | 387 | class Keyboard extends StatelessWidget { 388 | const Keyboard({Key? key}) : super(key: key); 389 | 390 | static const _keys = [ 391 | ['1', '4', '7', 'X'], 392 | ['2', '5', '8', '0'], 393 | ['3', '6', '9', 'backspace'], 394 | // ['backspace'], 395 | ]; 396 | 397 | @override 398 | Widget build(BuildContext context) { 399 | return Container( // alternatively use Material(elevation..) 400 | decoration: BoxDecoration( 401 | color: Theme.of(context).bottomAppBarTheme.color, 402 | boxShadow: [ 403 | BoxShadow(color: Theme.of(context).shadowColor.withOpacity(0.4), blurRadius: 4.0, offset: const Offset(0.0, -0.75)), 404 | ], 405 | ), 406 | child: Row( 407 | mainAxisAlignment: MainAxisAlignment.center, 408 | crossAxisAlignment: CrossAxisAlignment.start, 409 | children: _keys.map((col) => 410 | Column( 411 | mainAxisAlignment: MainAxisAlignment.center, 412 | children: col.map((s) => KeyboardButton(s)).toList(), 413 | ) 414 | ).toList(), 415 | ), 416 | ); 417 | } 418 | } 419 | 420 | // setting thickness/color as a workaround for invisible dividers in mobile web browser https://github.com/flutter/flutter/issues/46339 421 | class MyDivider extends StatelessWidget { 422 | const MyDivider({Key? key}) : super(key: key); 423 | @override build(BuildContext context) => Divider( 424 | thickness: 0.5, 425 | color: Theme.of(context).colorScheme.secondary.withOpacity(0.2), 426 | ); 427 | } 428 | 429 | class LevelScreen extends StatelessWidget { 430 | const LevelScreen({ Key? key }) : super(key: key); 431 | 432 | @override 433 | Widget build(BuildContext context) { 434 | final level = Provider.of(context); 435 | return ListView( 436 | padding: listPadding, 437 | children: interleave( 438 | level.exercise.fullQuestions().map((es) => Selector( 439 | // selector ensures that questions widget is only rebuilt when isActive is set or changes 440 | selector: (context, level) => es[0].key <= level.exercise.activeIndex && es.last.key >= level.exercise.activeIndex, // isActive 441 | shouldRebuild: (bool oldIsActive, bool isActive) => oldIsActive || isActive, 442 | builder: (context, isActive, child) { 443 | final game = Provider.of(context); // changes often, so consuming it inside selector avoids triggering rebuilds 444 | return InkWell( 445 | child: QuestionsWidget(es.map((e) => e.value).toList(), 446 | isActive: isActive, 447 | focussedQuestion: level.exercise.activeIndex - es[0].key, // TODO use level and game from different context? 448 | animateStatusWrong: game.doStatusAnimation(), 449 | doScroll: game.doScrollAnimation(), 450 | doneScrollAnimation: game.doneScrollAnimation, 451 | onTap: (j) => game.levelTapped(es[0].key + j, inExam: false), 452 | ), 453 | ); 454 | }, 455 | )), 456 | const MyDivider(), 457 | ).toList(), 458 | ); 459 | } 460 | } 461 | 462 | class ThemeModeLabel extends StatelessWidget { 463 | final ThemeMode _mode; 464 | const ThemeModeLabel(this._mode, { Key? key }) : super(key: key); 465 | @override 466 | Widget build(BuildContext context) { 467 | final String s; 468 | switch (_mode) { 469 | case ThemeMode.system: s = AppLocalizations.of(context)!.themeSystem; break; 470 | case ThemeMode.light: s = AppLocalizations.of(context)!.themeLight; break; 471 | case ThemeMode.dark: s = AppLocalizations.of(context)!.themeDark; break; 472 | } 473 | return Text(s); 474 | } 475 | } 476 | 477 | class MoreInfoMessage extends StatelessWidget { 478 | const MoreInfoMessage({Key? key}) : super(key: key); 479 | @override 480 | Widget build(BuildContext context) { 481 | final ThemeData theme = Theme.of(context); 482 | final TextStyle textStyle = theme.textTheme.bodyMedium!; 483 | final Uri url = Uri.parse('https://mwageringel.github.io/everest/'); 484 | final List textFragments = AppLocalizations.of(context)!.moreInfo('__URL__').split('__URL__'); // fragments before and after the url 485 | return Text.rich( // important for vertical alignment 486 | TextSpan( 487 | children: [ 488 | ...(textFragments.isNotEmpty && textFragments[0].isNotEmpty ? [TextSpan(style: textStyle, text: textFragments[0])] : []), 489 | WidgetSpan( 490 | child: Link( 491 | uri: url, 492 | builder: (context, followLink) => InkWell( 493 | // opens new tab in web (in contrast to `followLink`) and external browser on android 494 | onTap: () => launchUrl(url, mode: LaunchMode.externalApplication), 495 | child: Text(url.toString(), style: textStyle.copyWith(color: theme.colorScheme.primary)), 496 | ), 497 | ), 498 | ), 499 | ...(textFragments.length > 1 && textFragments[1].isNotEmpty ? [TextSpan(style: textStyle, text: textFragments[1])] : []), 500 | ], 501 | ), 502 | ); 503 | } 504 | } 505 | 506 | class SettingsScreen extends StatelessWidget { 507 | const SettingsScreen({ Key? key }) : super(key: key); 508 | 509 | @override 510 | Widget build(BuildContext context) { 511 | return ListView( 512 | padding: listPadding, 513 | children: [ 514 | ListTile( 515 | leading: const Icon(Icons.settings_brightness), 516 | title: Text(AppLocalizations.of(context)!.theme), 517 | ), 518 | Consumer2(builder: (context, world, game, child) => 519 | Column( 520 | children: [ 521 | ...([ThemeMode.light, ThemeMode.system, ThemeMode.dark].map((ThemeMode m) => 522 | RadioListTile( 523 | value: m, 524 | groupValue: world.themeMode, 525 | title: ThemeModeLabel(m), 526 | onChanged: (mode) async { 527 | world.switchTheme(themeMode: m); 528 | return game.db.storeKeyValue(themeModeKey, m.toString()); 529 | }, 530 | ), 531 | )), 532 | SwitchListTile( 533 | title: Text(AppLocalizations.of(context)!.darkThemeBlackBackground), 534 | subtitle: Text(AppLocalizations.of(context)!.darkThemeBlackBackgroundSubtitle), 535 | value: world.pureBlack, 536 | onChanged: (bool value) async { 537 | world.switchTheme(pureBlack: value); 538 | return game.db.storeKeyValue(pureBlackKey, value.toString()); 539 | }, 540 | ), 541 | ], 542 | ), 543 | ), 544 | const MyDivider(), 545 | Consumer2(builder: (context, world, game, child) => 546 | ListTile( 547 | leading: AnimatedSwitcher( 548 | duration: const Duration(milliseconds: 660), 549 | transitionBuilder: (Widget child, Animation animation) => VerticalScaleTransition(child, animation, horizontal: true), 550 | switchInCurve: _rotateCurve2, 551 | switchOutCurve: _rotateCurve2, 552 | child: Container( 553 | key: ObjectKey(game), 554 | child: const Icon(Icons.restore), 555 | ), 556 | ), 557 | title: Text(AppLocalizations.of(context)!.restart), 558 | subtitle: Text(AppLocalizations.of(context)!.restartSubtitle), 559 | onLongPress: () async { 560 | await game.resetProgress(); 561 | world.resetGame(Game.initializedGame(game.db, loadProgress: false)); 562 | }, 563 | ), 564 | ), 565 | const MyDivider(), 566 | AboutListTile( 567 | icon: const Icon(Icons.info_outline), 568 | applicationVersion: AppLocalizations.of(context)!.version(Provider.of(context).appInfo?.version), 569 | aboutBoxChildren: const [ 570 | MoreInfoMessage(), 571 | ], 572 | ), 573 | const MyDivider(), 574 | ], 575 | ); 576 | } 577 | } 578 | 579 | class KeyboardScaffold extends StatelessWidget { 580 | final Widget title; 581 | final Widget child; 582 | final List? actions; 583 | const KeyboardScaffold({required this.title, required this.child, this.actions, Key? key}) : super(key: key); 584 | 585 | @override 586 | Widget build(BuildContext context) { 587 | final game = Provider.of(context); 588 | final cs = [Expanded(child: child), const Hero(tag: 'thekeyboard', child: Keyboard())]; 589 | return FocusScope( 590 | debugLabel: 'keyboard-scaffold', 591 | // skipTraversal: true, // ideally should be skipped: TODO find a way to make sure traversal and initial focus still work (e.g. move focus along with active question) 592 | autofocus: true, // i.e. receives initial input 593 | onKeyEvent: (node, e) { 594 | final String? label = e.character?.toUpperCase(); 595 | // for now we ignore KeyRepeatEvent since UI does not rebuild fast enough or skips rebuilding some question widgets 596 | if (e is KeyDownEvent && e.logicalKey == LogicalKeyboardKey.backspace) { 597 | return game.keyPressed('backspace'); 598 | } else if (label != null && e is KeyDownEvent) { 599 | return game.keyPressed(label); 600 | } else { 601 | return KeyEventResult.ignored; 602 | } 603 | }, 604 | child: Scaffold( 605 | appBar: AppBar( 606 | title: title, 607 | actions: actions, 608 | ), 609 | body: MediaQuery.of(context).orientation == Orientation.landscape ? Row(children: cs) : Column(children: cs), 610 | ), 611 | ); 612 | } 613 | } 614 | 615 | // This widget groups the level title and the three exam questions together 616 | // in order to allow automatic scrolling even if the questions are still hidden. 617 | class ExamWidget extends StatelessWidget { 618 | final int levelIdx; 619 | final Game game; 620 | final Level level; 621 | final bool isActive, unlocked, _showExamQuestions; 622 | 623 | ExamWidget(this.game, {required this.levelIdx, required this.isActive, Key? key}): 624 | level = game.levels[levelIdx], 625 | unlocked = (levelIdx <= game.levelsUnlocked || debugUnlockAll), 626 | _showExamQuestions = (game.examUnlocked(levelIdx) || debugUnlockAll), 627 | super(key: key); 628 | 629 | void _pushExercises(BuildContext context, String label) { 630 | game.pushLevel(levelIdx); 631 | Navigator.of(context).push( 632 | MaterialPageRoute( 633 | builder: (context) { 634 | assert(game.activeLevel == levelIdx); 635 | return KeyboardScaffold( 636 | title: Text(label), 637 | // Rather than obtaining the current level from game.activeLevel, we provide it directly, 638 | // since activeLevel can change on popLevel which would cause some flickering 639 | // (i.e. exercises from a different level getting rendered). 640 | child: Provider.value( 641 | value: level, 642 | child: const LevelScreen(), 643 | ), 644 | ); 645 | }, 646 | ), 647 | ).then((_) { 648 | game.popLevel(); 649 | }); 650 | } 651 | 652 | @override 653 | Widget build(BuildContext context) { 654 | final label = AppLocalizations.of(context)!.levelTitle(levelIdx); 655 | return Column( 656 | children: [ 657 | if (levelIdx > 0) const MyDivider(), 658 | if (levelIdx > 0) ListTile( 659 | title: Text(label, style: _biggerFont), 660 | trailing: levelIdx == game.levelsUnlocked && !game.levels[levelIdx].clicked 661 | ? BouncingWidget(Icon(Icons.adaptive.arrow_forward)) 662 | : Icon(unlocked ? Icons.adaptive.arrow_forward : Icons.lock), 663 | enabled: unlocked, 664 | shape: _listTileRounded, 665 | onTap: () { 666 | if (unlocked) { 667 | _pushExercises(context, label); 668 | } 669 | }, 670 | ), 671 | if (levelIdx > 0 && _showExamQuestions) const MyDivider(), 672 | QuestionsWidget( 673 | _showExamQuestions ? level.exam.questions : [], // we render the widget even with 0 questions in order to support autoscroll when the questions are hidden for the first few levels 674 | isActive: isActive, 675 | focussedQuestion: level.exam.activeIndex, 676 | animateStatusWrong: game.doStatusAnimation(), 677 | doScroll: game.doScrollAnimation(), 678 | doneScrollAnimation: game.doneScrollAnimation, 679 | onTap: (i) => game.levelTapped(i, inExam: true, levelIdx: levelIdx), 680 | trailing: (levelIdx == game.levels.length-1 && game.finished) ? const EndMessage() : null, // added here for autoscroll 681 | ), 682 | ], 683 | ); 684 | } 685 | } 686 | 687 | class ExamsScreen extends StatelessWidget { 688 | const ExamsScreen({ Key? key }) : super(key: key); 689 | 690 | void _pushSettings(BuildContext context) { 691 | Navigator.of(context).push( 692 | MaterialPageRoute( 693 | builder: (context) { 694 | return Scaffold( 695 | appBar: AppBar( 696 | title: Text(AppLocalizations.of(context)!.settings), 697 | ), 698 | body: const SettingsScreen(), 699 | ); 700 | }, 701 | ), 702 | ).then((_) { 703 | Provider.of(context, listen: false).popSettings(); 704 | }); 705 | } 706 | 707 | @override 708 | Widget build(BuildContext context) { 709 | final scaf = KeyboardScaffold( 710 | title: const Text(appName), 711 | actions: [ 712 | IconButton( 713 | icon: const Icon(Icons.menu), 714 | onPressed: () => _pushSettings(context), 715 | tooltip: AppLocalizations.of(context)!.settings, 716 | ), 717 | ], 718 | child: ListView.builder( 719 | padding: listPadding, 720 | itemCount: Provider.of(context, listen: false).levels.length, // as number of levels is constant, not listening avoids unnecessary rebuilds 721 | itemBuilder: (context, levelIdx) => Selector( 722 | selector: (context, game) { 723 | // Rendering exam questions is only relevant when in the exam screen. 724 | // (Flutter only renders the questions that are visible on screen.) 725 | // This triggers full rebuilds whenever exiting a subpage (to account for resets/theme changes/unlocks) 726 | // and selective rebuilds for levels actively changing. 727 | final isRelevant = game.inExamScreen && (levelIdx == game.activeLevel || game.doRedrawEverything()); 728 | return isRelevant; 729 | }, 730 | shouldRebuild: (bool oldIsRelevant, bool newIsRelevant) => oldIsRelevant || newIsRelevant, 731 | builder: (_, isRelevant, child) { // we do not use the inner context since world changes (such as theme) would not trigger a rebuild 732 | final game = Provider.of(context, listen: false); // listening not needed, since selector already does 733 | assert((levelIdx > 0) ^ game.levels[levelIdx].exercise.questions.isEmpty); 734 | final isActive = levelIdx == game.activeLevel && game.inExamScreen; 735 | return Material( // fixes hover artifact near keyboard 736 | color: Theme.of(context).scaffoldBackgroundColor, 737 | child: ExamWidget(game, levelIdx: levelIdx, isActive: isActive), 738 | ); 739 | }, 740 | ), 741 | ), 742 | ); 743 | return Consumer(builder: (context, game, child) => 744 | WillPopScope( 745 | onWillPop: () => ( 746 | // this asks for confirmation at back button press to avoid loss of state, when no database is available on some platform 747 | game.db.isUsable() ? Future.value(true) : showDialog( 748 | context: context, 749 | builder: (context) => AlertDialog( 750 | title: Text(AppLocalizations.of(context)!.exitDialogTitle), 751 | content: Text(AppLocalizations.of(context)!.exitDialogContent), 752 | actions: [ 753 | ElevatedButton(child: Text(AppLocalizations.of(context)!.dialogCancel), onPressed: () => Navigator.of(context).pop(false)), 754 | OutlinedButton(child: Text(AppLocalizations.of(context)!.dialogOk), onPressed: () => Navigator.of(context).pop(true)), 755 | ], 756 | ), 757 | ).then((x) => x ?? false) 758 | ), 759 | child: scaf, 760 | ) 761 | ); 762 | } 763 | } 764 | 765 | class ExtendedMessage extends StatelessWidget { 766 | const ExtendedMessage({Key? key}) : super(key: key); 767 | @override build(BuildContext context) => Text( 768 | utf8.decode(base64.decode(AppLocalizations.of(context)!.extendedMessage)) 769 | ); 770 | } 771 | 772 | class EndMessage extends StatelessWidget { 773 | const EndMessage({Key? key}) : super(key: key); 774 | @override build(BuildContext context) => ListTile( 775 | title: Text(AppLocalizations.of(context)!.endMessage, style: _biggerFont.merge(TextStyle(color: Theme.of(context).colorScheme.primary))), 776 | subtitle: const ExtendedMessage(), 777 | leading: const Icon(Icons.sentiment_very_satisfied), 778 | ); 779 | } 780 | 781 | class MyApp extends StatelessWidget { 782 | final World world0; 783 | final Game _game0; 784 | 785 | const MyApp(this.world0, this._game0, {Key? key}) : super(key: key); 786 | 787 | static ThemeData lightTheme() => FlexThemeData.light( 788 | fontFamily: 'NotoSans', 789 | scheme: FlexScheme.materialBaseline, 790 | primary: Colors.indigo, 791 | surfaceMode: FlexSurfaceMode.highScaffoldLowSurface, 792 | blendLevel: 4, 793 | appBarOpacity: 0.95, 794 | subThemesData: const FlexSubThemesData( 795 | blendOnLevel: 4, 796 | blendOnColors: false, 797 | blendTextTheme: true, // blends theme colors into text 798 | tintedDisabledControls: true, 799 | outlinedButtonOutlineSchemeColor: SchemeColor.primary, 800 | ), 801 | visualDensity: FlexColorScheme.comfortablePlatformDensity, 802 | useMaterial3: false, 803 | ); 804 | 805 | static ThemeData darkTheme(bool pureBlack) => FlexThemeData.dark( 806 | fontFamily: 'NotoSans', 807 | scheme: FlexScheme.materialBaseline, 808 | primary: Colors.indigoAccent, // better contrast against dark background 809 | surfaceMode: FlexSurfaceMode.highScaffoldLowSurface, 810 | blendLevel: 10, 811 | appBarStyle: FlexAppBarStyle.background, 812 | appBarOpacity: 0.90, 813 | subThemesData: const FlexSubThemesData( 814 | blendOnLevel: 10, 815 | blendTextTheme: true, // blends theme colors into text 816 | tintedDisabledControls: true, 817 | outlinedButtonOutlineSchemeColor: SchemeColor.primary, 818 | ), 819 | visualDensity: FlexColorScheme.comfortablePlatformDensity, 820 | useMaterial3: false, 821 | darkIsTrueBlack: pureBlack, 822 | ); 823 | 824 | @override 825 | Widget build(BuildContext context) { 826 | return ChangeNotifierProvider.value( 827 | value: world0, 828 | child: Consumer(builder: (context, world, child) { 829 | return FutureProvider.value( 830 | value: world.gameFuture, // differs from game0 once resetGame is called 831 | initialData: _game0, // After game has been reset, we should avoid showing this old game progress, but since reset happens on another page, it is likely never visible 832 | child: Consumer(builder: (context, game, child) { 833 | return ChangeNotifierProvider.value( 834 | value: game, 835 | child: MaterialApp( 836 | title: appName, 837 | themeMode: world.themeMode, 838 | theme: lightTheme(), 839 | darkTheme: darkTheme(world.pureBlack), 840 | localizationsDelegates: AppLocalizations.localizationsDelegates, 841 | supportedLocales: AppLocalizations.supportedLocales, 842 | home: const ExamsScreen(), 843 | ), 844 | ); 845 | }), 846 | ); 847 | }), 848 | ); 849 | } 850 | } 851 | 852 | // wrapper around game state in order to be able to reset the progress 853 | class World with ChangeNotifier { 854 | ThemeMode themeMode; 855 | bool pureBlack; 856 | final PackageInfo? appInfo; 857 | Future gameFuture; 858 | World(this.appInfo, this.themeMode, this.pureBlack, this.gameFuture); 859 | 860 | void resetGame(Future newGame) { 861 | gameFuture = newGame; 862 | notifyListeners(); 863 | } 864 | 865 | void switchTheme({ThemeMode? themeMode, bool? pureBlack}) { 866 | this.themeMode = themeMode ?? this.themeMode; 867 | this.pureBlack = pureBlack ?? this.pureBlack; 868 | notifyListeners(); 869 | } 870 | } 871 | 872 | 873 | void main() async { 874 | WidgetsFlutterBinding.ensureInitialized(); // Avoid errors caused by flutter upgrade. 875 | DatabaseWrapper db = await DatabaseWrapper.create(); 876 | 877 | LicenseRegistry.addLicense(() async* { 878 | final license = await rootBundle.loadString('fonts/OFL.txt'); 879 | yield LicenseEntryWithLineBreaks(['NotoSansMath', 'NotoSans'], license); 880 | }); 881 | 882 | Future loadThemeMode(Game game) async { 883 | String? mode = await game.db.loadKeyValue(themeModeKey); 884 | return ThemeMode.values.firstWhere((m) => m.toString() == mode, orElse: () => ThemeMode.system); 885 | } 886 | 887 | // loading the game from the database is quick, so we do it synchronously here, so that the initial render can already show the actual progress 888 | final game0 = await Game.initializedGame(db, loadProgress: true); 889 | final themeMode0 = await loadThemeMode(game0); 890 | final pureBlack0 = (await game0.db.loadKeyValue(pureBlackKey)) == true.toString(); // false by default 891 | final appInfo = await PackageInfo.fromPlatform(); 892 | final world0 = World(appInfo, themeMode0, pureBlack0, Future.value(game0)); 893 | runApp(MyApp(world0, game0)); 894 | } 895 | --------------------------------------------------------------------------------