├── .github ├── ISSUE_TEMPLATE │ ├── fout-rapporteren.md │ └── suggestie.md └── workflows │ └── build-release.yml ├── .gitignore ├── .idx └── dev.nix ├── .metadata ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ ├── google-services.json │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── dev │ │ │ │ └── harrydekat │ │ │ │ └── silvio │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-hdpi │ │ │ └── notification.png │ │ │ ├── drawable-mdpi │ │ │ └── notification.png │ │ │ ├── drawable-xhdpi │ │ │ └── notification.png │ │ │ ├── drawable-xxhdpi │ │ │ └── notification.png │ │ │ ├── drawable-xxxhdpi │ │ │ └── notification.png │ │ │ ├── drawable │ │ │ ├── ic_launcher_background.xml │ │ │ └── launch_background.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ └── ic_launcher.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_monochrome.png │ │ │ ├── play_store_512.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ ├── colors.xml │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets └── font_package │ └── fonts │ └── Gemairo.ttf ├── crowdin.yml ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── PrivacyInfo.xcprivacy ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── AppIcon-20@2x.png │ │ │ ├── AppIcon-20@2x~ipad.png │ │ │ ├── AppIcon-20@3x.png │ │ │ ├── AppIcon-20~ipad.png │ │ │ ├── AppIcon-29.png │ │ │ ├── AppIcon-29@2x.png │ │ │ ├── AppIcon-29@2x~ipad.png │ │ │ ├── AppIcon-29@3x.png │ │ │ ├── AppIcon-29~ipad.png │ │ │ ├── AppIcon-40@2x.png │ │ │ ├── AppIcon-40@2x~ipad.png │ │ │ ├── AppIcon-40@3x.png │ │ │ ├── AppIcon-40~ipad.png │ │ │ ├── AppIcon-60@2x~car.png │ │ │ ├── AppIcon-60@3x~car.png │ │ │ ├── AppIcon-83.5@2x~ipad.png │ │ │ ├── AppIcon@2x.png │ │ │ ├── AppIcon@2x~ipad.png │ │ │ ├── AppIcon@3x.png │ │ │ ├── AppIcon~ios-marketing.png │ │ │ ├── AppIcon~ipad.png │ │ │ └── Contents.json │ │ ├── Contents.json │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── README.md │ │ │ ├── ic_launcher_foreground 1.png │ │ │ ├── ic_launcher_foreground 2.png │ │ │ └── ic_launcher_foreground.png │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── ExportOptions.plist │ ├── GoogleService-Info.plist │ ├── Info.plist │ ├── Launch Screen.storyboard │ ├── Runner-Bridging-Header.h │ ├── Runner.entitlements │ └── RunnerRelease.entitlements ├── build │ ├── .last_build_id │ └── XCBuildData │ │ └── 276c23d118cf162e1ac0a7687825d510.xcbuilddata │ │ ├── build-request.json │ │ ├── description.msgpack │ │ ├── manifest.json │ │ ├── target-graph.txt │ │ └── task-store.msgpack └── firebase_app_id_file.json ├── ios_release.sh ├── l10n.yaml ├── lib ├── apis │ ├── abstact_api.dart │ ├── account_manager.dart │ ├── ads.dart │ ├── local_file.dart │ ├── magister.dart │ ├── magister │ │ ├── api.dart │ │ ├── screens │ │ │ ├── config.dart │ │ │ ├── login.dart │ │ │ └── terms.dart │ │ └── translate.dart │ ├── random.dart │ ├── saaf.dart │ ├── somtoday.dart │ └── somtoday │ │ ├── api.dart │ │ ├── screens │ │ ├── config.dart │ │ ├── login.dart │ │ ├── school_picker.dart │ │ └── terms.dart │ │ └── translate.dart ├── background_tasks.dart ├── firebase_options.dart ├── hive │ ├── adapters.dart │ ├── adapters.g.dart │ └── extentions.dart ├── l10n │ ├── app_en.arb │ └── app_nl.arb ├── main.dart ├── screens │ ├── career.dart │ ├── login.dart │ ├── post_login.dart │ ├── search.dart │ ├── settings.dart │ ├── subject.dart │ ├── subjects.dart │ └── year.dart └── widgets │ ├── ads.dart │ ├── animations.dart │ ├── announcements.dart │ ├── appbar.dart │ ├── avatars.dart │ ├── bottom_sheet.dart │ ├── card.dart │ ├── cards │ ├── grade_calculations.dart │ ├── list_grade.dart │ ├── list_schoolyear.dart │ └── list_test.dart │ ├── charts │ ├── barchart_frequency.dart │ ├── barchart_subjects_average.dart │ ├── barchart_subjects_min_max.dart │ ├── barchart_subjects_weight.dart │ ├── linechart_grades.dart │ └── linechart_monthly_average.dart │ ├── facts_header.dart │ ├── filter.dart │ ├── global │ └── skeletons.dart │ ├── navigation.dart │ └── ratelimit.dart ├── pubspec.lock ├── pubspec.yaml └── test └── widget_test.dart /.github/ISSUE_TEMPLATE/fout-rapporteren.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Fout rapporteren 3 | about: Help Gemairo door fouten in de applicatie door te geven 4 | title: "Bug - " 5 | labels: bug 6 | assignees: "" 7 | --- 8 | 9 | **Beschrijf de fout** 10 | Een duidelijke beschijving van de fout. 11 | 12 | **Fout reproduceren** 13 | Stappen om het gedrag te reproduceren: 14 | 15 | 1. Ga naar '...' 16 | 2. Klik op '....' 17 | 3. etc. 18 | 19 | **Verwacht gedrag** 20 | Een duidelijke beschrijving van wat u verwachtte dat er zou gebeuren. 21 | 22 | **Schermafbeeldingen** 23 | Voeg indien van toepassing schermafbeeldingen toe van de fout 24 | 25 | **Smartphone (gelieve de volgende gegevens in te vullen):** 26 | 27 | - Toestel: [bijv. Iphone 6] 28 | - Besturingssysteem: [bijv. iOS8.1] 29 | - Versie [bijv. 1.0.1] (te zien bij licenties in de instellingen) 30 | 31 | **Aanvullende context** 32 | Voeg hier een andere context over het probleem toe. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/suggestie.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Suggestie 3 | about: Maak een suggestie 4 | title: 'Suggestie - ' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is uw suggestie verbonden aan een probleem? zo ja, beschrijf het probleem** 11 | Een duidelijke beschrijving van wat het probleem is. b.v.b. Ik ben altijd gefrustreerd wanneer [...] 12 | 13 | **Beschrijf welke toevoeging u wilt zien** 14 | Een duidelijke beschrijving van wat u wilt dat er gaat komen. 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | *.jks 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | migrate_working_dir/ 13 | 14 | # IntelliJ related 15 | *.iml 16 | *.ipr 17 | *.iws 18 | .idea/ 19 | 20 | # The .vscode folder contains launch configuration and tasks you configure in 21 | # VS Code which you may wish to be included in version control, so this line 22 | # is commented out by default. 23 | #.vscode/ 24 | 25 | # Flutter/Dart/Pub related 26 | **/doc/api/ 27 | **/ios/Flutter/.last_build_id 28 | .dart_tool/ 29 | .flutter-plugins 30 | .flutter-plugins-dependencies 31 | .packages 32 | .pub-cache/ 33 | .pub/ 34 | /build/ 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Obfuscation related 40 | app.*.map.json 41 | 42 | # Android Studio will place build artifacts here 43 | /android/app/debug 44 | /android/app/profile 45 | /android/app/release 46 | -------------------------------------------------------------------------------- /.idx/dev.nix: -------------------------------------------------------------------------------- 1 | {pkgs}: { 2 | channel = "stable-23.11"; 3 | packages = [ 4 | pkgs.nodePackages.firebase-tools 5 | pkgs.jdk17 6 | pkgs.unzip 7 | ]; 8 | idx.extensions = [ 9 | "Dart-Code.dart-code" 10 | "Dart-Code.flutter"]; 11 | idx.previews = { 12 | previews = { 13 | web = { 14 | command = [ 15 | "flutter" 16 | "run" 17 | "--machine" 18 | "-d" 19 | "web-server" 20 | "--web-hostname" 21 | "0.0.0.0" 22 | "--web-port" 23 | "$PORT" 24 | ]; 25 | manager = "flutter"; 26 | }; 27 | android = { 28 | command = [ 29 | "flutter" 30 | "run" 31 | "--machine" 32 | "-d" 33 | "android" 34 | "-d" 35 | "emulator-5554" 36 | ]; 37 | manager = "flutter"; 38 | }; 39 | }; 40 | }; 41 | } -------------------------------------------------------------------------------- /.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. 5 | 6 | version: 7 | revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf 17 | base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf 18 | - platform: android 19 | create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf 20 | base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf 21 | - platform: ios 22 | create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf 23 | base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf 24 | 25 | # User provided section 26 | 27 | # List of Local paths (relative to this file) that should be 28 | # ignored by the migrate tool. 29 | # 30 | # Files that are not part of the templates will be ignored by default. 31 | unmanaged_files: 32 | - 'lib/main.dart' 33 | - 'ios/Runner.xcodeproj/project.pbxproj' 34 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Gemairo", 9 | "request": "launch", 10 | "type": "dart" 11 | }, 12 | { 13 | "name": "Gemairo (profile mode)", 14 | "request": "launch", 15 | "type": "dart", 16 | "flutterMode": "profile" 17 | }, 18 | { 19 | "name": "Gemairo (release mode)", 20 | "request": "launch", 21 | "type": "dart", 22 | "flutterMode": "release" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | Logo 4 | 5 | 6 |

Gemairo Statistiek voor Magister

7 | 8 |

9 | Krijg beter inzicht in je cijfers 10 |

11 | 12 | 13 | 14 | 15 | 16 |

17 |
18 | 19 | > **Note** 20 | > Gemairo is niet verbonden met, of onderdeel van Iddink Group. 21 | 22 | ## Wat is dit? 23 | 24 | Gemairo is een manier om je cijfers uit het [leerlingvolgsysteem Magister](https://magister.nl) te bekijken. Hun eigen app mist veel functionaliteiten, dus heb ik een poging gedaan om in ieder geval het bekijken van je cijfers te verbeteren. 25 | 26 | ## Is dit project verbonden met Magister of haar moeder bedrijf? 27 | 28 | **Nee**, Gemairo is een **privé-initiatief** en heeft geen elke connectie met Magister of haar moeder bedrijf. Alle code voor Gemairo is openbaar beschikbaar, dus het is mogelijk om zelf te controleren welke en hoe Gemairo je gegevens gebruikt. 29 | 30 | ![screenshots](https://www.harrydekat.dev/Silvio/screenshot.png) 31 | 32 | ## Contributie 33 | 34 | Alle contributies **zijn erg welkom**, omdat ik dit project niet voor eeuwig zelf kan ondersteunen. Je kan een contributies maken door een pull-request te openen, maar probeer wel een **goede beschijving** van je veranderingen/toevoegingen te maken. Verder, mocht je een UI toevoeging maken, probeer dan waar mogelijk [material 3](https://m3.material.io/) aan te houden. 35 | 36 | ## Contact 37 | 38 | 39 | 40 | Project Link: [https://github.com/Gemairo/app](https://github.com/Gemairo/app) 41 | 42 | 45 | 46 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | def keystoreProperties = new Properties() 25 | def keystorePropertiesFile = rootProject.file('key.properties') 26 | if (keystorePropertiesFile.exists()) { 27 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 28 | } 29 | 30 | 31 | apply plugin: 'com.android.application' 32 | // START: FlutterFire Configuration 33 | apply plugin: 'com.google.gms.google-services' 34 | // END: FlutterFire Configuration 35 | apply plugin: 'kotlin-android' 36 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 37 | 38 | android { 39 | compileSdkVersion 34 //rootProject.ext.compileSdkVersion 40 | ndkVersion flutter.ndkVersion 41 | 42 | compileOptions { 43 | coreLibraryDesugaringEnabled true 44 | sourceCompatibility JavaVersion.VERSION_1_8 45 | targetCompatibility JavaVersion.VERSION_1_8 46 | } 47 | 48 | kotlinOptions { 49 | jvmTarget = '1.8' 50 | } 51 | 52 | sourceSets { 53 | main.java.srcDirs += 'src/main/kotlin' 54 | } 55 | 56 | defaultConfig { 57 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 58 | applicationId "app.netlob.magiscore" 59 | // You can update the following values to match your application needs. 60 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 61 | minSdkVersion 21 62 | // targetSdkVersion rootProject.ext.targetSdkVersion 63 | targetSdkVersion 34 64 | versionCode flutterVersionCode.toInteger() 65 | versionName flutterVersionName 66 | multiDexEnabled true 67 | // ndk { 68 | // abiFilters "arm64-v8a", "armeabi-v7a", "x86", "x86_64" 69 | // } 70 | } 71 | 72 | signingConfigs { 73 | release { 74 | keyAlias keystoreProperties['keyAlias'] 75 | keyPassword keystoreProperties['keyPassword'] 76 | storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null 77 | storePassword keystoreProperties['storePassword'] 78 | } 79 | } 80 | buildTypes { 81 | release { 82 | signingConfig signingConfigs.release 83 | } 84 | } 85 | } 86 | 87 | flutter { 88 | source '../..' 89 | } 90 | 91 | dependencies { 92 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 93 | implementation "androidx.activity:activity:1.6.1" 94 | implementation 'androidx.window:window:1.0.0' 95 | implementation 'androidx.window:window-java:1.0.0' 96 | coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' 97 | implementation 'com.google.android.gms:play-services-ads:22.0.0' 98 | } 99 | -------------------------------------------------------------------------------- /android/app/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "978543096660", 4 | "project_id": "gemairo-6c835", 5 | "storage_bucket": "gemairo-6c835.appspot.com" 6 | }, 7 | "client": [ 8 | { 9 | "client_info": { 10 | "mobilesdk_app_id": "1:978543096660:android:2c5453859e4f9f31940ec1", 11 | "android_client_info": { 12 | "package_name": "app.netlob.magiscore" 13 | } 14 | }, 15 | "oauth_client": [ 16 | { 17 | "client_id": "978543096660-1b7drodio3mma413rikba22oifa3t09a.apps.googleusercontent.com", 18 | "client_type": 3 19 | } 20 | ], 21 | "api_key": [ 22 | { 23 | "current_key": "AIzaSyA-D4yWJYnLhI5idhJz2whEkKekI6STXqo" 24 | } 25 | ], 26 | "services": { 27 | "appinvite_service": { 28 | "other_platform_oauth_client": [ 29 | { 30 | "client_id": "978543096660-1b7drodio3mma413rikba22oifa3t09a.apps.googleusercontent.com", 31 | "client_type": 3 32 | }, 33 | { 34 | "client_id": "978543096660-1rg9mgl5nqa6fkn91c6cfg3a7808rqvj.apps.googleusercontent.com", 35 | "client_type": 2, 36 | "ios_info": { 37 | "bundle_id": "app.netlob.magiscore" 38 | } 39 | } 40 | ] 41 | } 42 | }, 43 | "admob_app_id": "ca-app-pub-9170931639371270~4644085927" 44 | } 45 | ], 46 | "configuration_version": "1" 47 | } -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 9 | 17 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 40 | 43 | 45 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/dev/harrydekat/silvio/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package app.netlob.magiscore 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/android/app/src/main/res/drawable-hdpi/notification.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/android/app/src/main/res/drawable-mdpi/notification.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/android/app/src/main/res/drawable-xhdpi/notification.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/android/app/src/main/res/drawable-xxhdpi/notification.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/android/app/src/main/res/drawable-xxxhdpi/notification.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/play_store_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/android/app/src/main/res/play_store_512.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #4f46e5 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.8.22' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.2.0' 10 | // START: FlutterFire Configuration 11 | classpath 'com.google.gms:google-services:4.3.10' 12 | // END: FlutterFire Configuration 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | } 15 | ext { 16 | compileSdkVersion = 33 17 | targetSdkVersion = 33 18 | appCompatVersion = "1.4.2" 19 | } 20 | } 21 | 22 | allprojects { 23 | repositories { 24 | google() 25 | mavenCentral() 26 | maven { 27 | url "${project(':background_fetch').projectDir}/libs" 28 | } 29 | } 30 | } 31 | 32 | rootProject.buildDir = '../build' 33 | subprojects { 34 | project.buildDir = "${rootProject.buildDir}/${project.name}" 35 | } 36 | subprojects { 37 | project.evaluationDependsOn(':app') 38 | } 39 | 40 | tasks.register("clean", Delete) { 41 | delete rootProject.buildDir 42 | } 43 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 6 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /assets/font_package/fonts/Gemairo.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/assets/font_package/fonts/Gemairo.ttf -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - source: /lib/l10n/app_nl.arb 3 | translation: /lib/l10n/app_%two_letters_code%.arb 4 | targets: 5 | - name: Arbs 6 | sources: 7 | - /lib/l10n/app_nl.arb 8 | file: /lib/l10n/app_%two_letters_code%.arb 9 | format: arb-export 10 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '15.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | # Start of the permission_handler configuration 41 | target.build_configurations.each do |config| 42 | xcconfig_path = config.base_configuration_reference.real_path 43 | xcconfig = File.read(xcconfig_path) 44 | xcconfig_mod = xcconfig.gsub(/DT_TOOLCHAIN_DIR/, "TOOLCHAIN_DIR") 45 | File.open(xcconfig_path, "w") { |file| file << xcconfig_mod } 46 | 47 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0' 48 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ 49 | '$(inherited)', 50 | 51 | ## dart: PermissionGroup.camera 52 | 'PERMISSION_CAMERA=1', 53 | 54 | ## dart: PermissionGroup.notification 55 | 'PERMISSION_NOTIFICATIONS=1', 56 | ] 57 | 58 | config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64' 59 | config.build_settings['CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES'] = 'YES' 60 | 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /ios/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyAccessedAPITypes 6 | 7 | 8 | NSPrivacyAccessedAPIType 9 | NSPrivacyAccessedAPICategoryUserDefaults 10 | NSPrivacyAccessedAPITypeReasons 11 | 12 | CA92.1 13 | 14 | 15 | 16 | NSPrivacyAccessedAPIType 17 | NSPrivacyAccessedAPICategoryFileTimestamp 18 | NSPrivacyAccessedAPITypeReasons 19 | 20 | C617.1 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import AppTrackingTransparency 3 | import flutter_local_notifications 4 | import Flutter 5 | import GoogleMobileAds 6 | 7 | @main 8 | @objc class AppDelegate: FlutterAppDelegate { 9 | override func application( 10 | _ application: UIApplication, 11 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 12 | ) -> Bool { 13 | // This is required to make any communication available in the action isolate. 14 | FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { (registry) in 15 | GeneratedPluginRegistrant.register(with: registry) 16 | } 17 | 18 | if #available(iOS 10.0, *) { 19 | UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate 20 | } 21 | 22 | func applicationDidBecomeActive(_ application: UIApplication) { 23 | if #available(iOS 15.0, *) { 24 | ATTrackingManager.requestTrackingAuthorization(completionHandler: { status in 25 | 26 | }) 27 | } 28 | } 29 | 30 | GeneratedPluginRegistrant.register(with: self) 31 | GADMobileAds.sharedInstance().requestConfiguration.testDeviceIdentifiers = [ "bae27ae297f1ea60743b27eb5351b744" ] 32 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "filename": "AppIcon@2x.png", 5 | "idiom": "iphone", 6 | "scale": "2x", 7 | "size": "60x60" 8 | }, 9 | { 10 | "filename": "AppIcon@3x.png", 11 | "idiom": "iphone", 12 | "scale": "3x", 13 | "size": "60x60" 14 | }, 15 | { 16 | "filename": "AppIcon~ipad.png", 17 | "idiom": "ipad", 18 | "scale": "1x", 19 | "size": "76x76" 20 | }, 21 | { 22 | "filename": "AppIcon@2x~ipad.png", 23 | "idiom": "ipad", 24 | "scale": "2x", 25 | "size": "76x76" 26 | }, 27 | { 28 | "filename": "AppIcon-83.5@2x~ipad.png", 29 | "idiom": "ipad", 30 | "scale": "2x", 31 | "size": "83.5x83.5" 32 | }, 33 | { 34 | "filename": "AppIcon-40@2x.png", 35 | "idiom": "iphone", 36 | "scale": "2x", 37 | "size": "40x40" 38 | }, 39 | { 40 | "filename": "AppIcon-40@3x.png", 41 | "idiom": "iphone", 42 | "scale": "3x", 43 | "size": "40x40" 44 | }, 45 | { 46 | "filename": "AppIcon-40~ipad.png", 47 | "idiom": "ipad", 48 | "scale": "1x", 49 | "size": "40x40" 50 | }, 51 | { 52 | "filename": "AppIcon-40@2x~ipad.png", 53 | "idiom": "ipad", 54 | "scale": "2x", 55 | "size": "40x40" 56 | }, 57 | { 58 | "filename": "AppIcon-20@2x.png", 59 | "idiom": "iphone", 60 | "scale": "2x", 61 | "size": "20x20" 62 | }, 63 | { 64 | "filename": "AppIcon-20@3x.png", 65 | "idiom": "iphone", 66 | "scale": "3x", 67 | "size": "20x20" 68 | }, 69 | { 70 | "filename": "AppIcon-20~ipad.png", 71 | "idiom": "ipad", 72 | "scale": "1x", 73 | "size": "20x20" 74 | }, 75 | { 76 | "filename": "AppIcon-20@2x~ipad.png", 77 | "idiom": "ipad", 78 | "scale": "2x", 79 | "size": "20x20" 80 | }, 81 | { 82 | "filename": "AppIcon-29.png", 83 | "idiom": "iphone", 84 | "scale": "1x", 85 | "size": "29x29" 86 | }, 87 | { 88 | "filename": "AppIcon-29@2x.png", 89 | "idiom": "iphone", 90 | "scale": "2x", 91 | "size": "29x29" 92 | }, 93 | { 94 | "filename": "AppIcon-29@3x.png", 95 | "idiom": "iphone", 96 | "scale": "3x", 97 | "size": "29x29" 98 | }, 99 | { 100 | "filename": "AppIcon-29~ipad.png", 101 | "idiom": "ipad", 102 | "scale": "1x", 103 | "size": "29x29" 104 | }, 105 | { 106 | "filename": "AppIcon-29@2x~ipad.png", 107 | "idiom": "ipad", 108 | "scale": "2x", 109 | "size": "29x29" 110 | }, 111 | { 112 | "filename": "AppIcon-60@2x~car.png", 113 | "idiom": "car", 114 | "scale": "2x", 115 | "size": "60x60" 116 | }, 117 | { 118 | "filename": "AppIcon-60@3x~car.png", 119 | "idiom": "car", 120 | "scale": "3x", 121 | "size": "60x60" 122 | }, 123 | { 124 | "filename": "AppIcon~ios-marketing.png", 125 | "idiom": "ios-marketing", 126 | "scale": "1x", 127 | "size": "1024x1024" 128 | } 129 | ], 130 | "info": { 131 | "author": "iconkitchen", 132 | "version": 1 133 | } 134 | } -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ic_launcher_foreground.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "ic_launcher_foreground 1.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "ic_launcher_foreground 2.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/ic_launcher_foreground 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/ios/Runner/Assets.xcassets/LaunchImage.imageset/ic_launcher_foreground 1.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/ic_launcher_foreground 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/ios/Runner/Assets.xcassets/LaunchImage.imageset/ic_launcher_foreground 2.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/ios/Runner/Assets.xcassets/LaunchImage.imageset/ic_launcher_foreground.png -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /ios/Runner/ExportOptions.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | destination 6 | export 7 | manageAppVersionAndBuildNumber 8 | 9 | method 10 | app-store 11 | provisioningProfiles 12 | 13 | app.netlob.magiscore 14 | Gemairo 15 | 16 | signingCertificate 17 | 7F82AD4C6C02266F4743AE9FCE9B08CD7BEE285D 18 | signingStyle 19 | manual 20 | stripSwiftSymbols 21 | 22 | teamID 23 | FCB4S3W235 24 | uploadSymbols 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CLIENT_ID 6 | 978543096660-1rg9mgl5nqa6fkn91c6cfg3a7808rqvj.apps.googleusercontent.com 7 | REVERSED_CLIENT_ID 8 | com.googleusercontent.apps.978543096660-1rg9mgl5nqa6fkn91c6cfg3a7808rqvj 9 | API_KEY 10 | AIzaSyDcG2eDIzbFUVk67U3hZ8uWcbr9Pr7ZI1s 11 | GCM_SENDER_ID 12 | 978543096660 13 | PLIST_VERSION 14 | 1 15 | BUNDLE_ID 16 | app.netlob.magiscore 17 | PROJECT_ID 18 | gemairo-6c835 19 | STORAGE_BUCKET 20 | gemairo-6c835.appspot.com 21 | IS_ADS_ENABLED 22 | 23 | IS_ANALYTICS_ENABLED 24 | 25 | IS_APPINVITE_ENABLED 26 | 27 | IS_GCM_ENABLED 28 | 29 | IS_SIGNIN_ENABLED 30 | 31 | GOOGLE_APP_ID 32 | 1:978543096660:ios:52724800456f3c21940ec1 33 | ADMOB_APP_ID 34 | ca-app-pub-9170931639371270~8990525040 35 | 36 | -------------------------------------------------------------------------------- /ios/Runner/Launch Screen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /ios/Runner/Runner.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/RunnerRelease.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | com.apple.developer.aps-environment 8 | development 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/build/.last_build_id: -------------------------------------------------------------------------------- 1 | 3c6bcbae52418748a4180941ea3e39b9 -------------------------------------------------------------------------------- /ios/build/XCBuildData/276c23d118cf162e1ac0a7687825d510.xcbuilddata/build-request.json: -------------------------------------------------------------------------------- 1 | { 2 | "buildCommand" : { 3 | "command" : "build", 4 | "skipDependencies" : false, 5 | "style" : "buildOnly" 6 | }, 7 | "configuredTargets" : [ 8 | 9 | ], 10 | "continueBuildingAfterErrors" : false, 11 | "dependencyScope" : "workspace", 12 | "enableIndexBuildArena" : false, 13 | "hideShellScriptEnvironment" : false, 14 | "parameters" : { 15 | "action" : "build", 16 | "overrides" : { 17 | 18 | } 19 | }, 20 | "qos" : "utility", 21 | "schemeCommand" : "launch", 22 | "showNonLoggedProgress" : true, 23 | "useDryRun" : false, 24 | "useImplicitDependencies" : false, 25 | "useLegacyBuildLocations" : false, 26 | "useParallelTargets" : true 27 | } -------------------------------------------------------------------------------- /ios/build/XCBuildData/276c23d118cf162e1ac0a7687825d510.xcbuilddata/description.msgpack: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/ios/build/XCBuildData/276c23d118cf162e1ac0a7687825d510.xcbuilddata/description.msgpack -------------------------------------------------------------------------------- /ios/build/XCBuildData/276c23d118cf162e1ac0a7687825d510.xcbuilddata/manifest.json: -------------------------------------------------------------------------------- 1 | {"client":{"name":"basic","version":0,"file-system":"device-agnostic","perform-ownership-analysis":"no"},"targets":{"":[""]},"commands":{"":{"tool":"phony","inputs":[""],"outputs":[""]},"P0:::Gate WorkspaceHeaderMapVFSFilesWritten":{"tool":"phony","inputs":[],"outputs":[""]}}} -------------------------------------------------------------------------------- /ios/build/XCBuildData/276c23d118cf162e1ac0a7687825d510.xcbuilddata/target-graph.txt: -------------------------------------------------------------------------------- 1 | Target dependency graph (0 target) -------------------------------------------------------------------------------- /ios/build/XCBuildData/276c23d118cf162e1ac0a7687825d510.xcbuilddata/task-store.msgpack: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gemairo/app/a1cef27bb3f8287ecf159a5544fdd144b9c13a23/ios/build/XCBuildData/276c23d118cf162e1ac0a7687825d510.xcbuilddata/task-store.msgpack -------------------------------------------------------------------------------- /ios/firebase_app_id_file.json: -------------------------------------------------------------------------------- 1 | { 2 | "file_generated_by": "FlutterFire CLI", 3 | "purpose": "FirebaseAppID & ProjectID for this Firebase app in this directory", 4 | "GOOGLE_APP_ID": "1:978543096660:ios:52724800456f3c21940ec1", 5 | "FIREBASE_PROJECT_ID": "gemairo-6c835", 6 | "GCM_SENDER_ID": "978543096660" 7 | } -------------------------------------------------------------------------------- /ios_release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | flutter clean 3 | rm -Rf ios/Pods 4 | rm -Rf ios/.symlinks 5 | rm -Rf ios/Flutter/Flutter.framework 6 | rm -Rf ios/Flutter/Flutter.podspec 7 | rm -Rf ios/Podfile.lock 8 | flutter pub get 9 | flutter packages get 10 | flutter packages pub run build_runner build --delete-conflicting-outputs 11 | flutter pub get 12 | cd ios/ 13 | # arch -x86_64 pod repo update 14 | # arch -x86_64 pod install 15 | pod repo update 16 | pod install 17 | cd ../ 18 | # flutter packages pub run flutter_launcher_name:main 19 | # flutter pub pub run flutter_native_splash:create 20 | # flutter build ios --release -------------------------------------------------------------------------------- /l10n.yaml: -------------------------------------------------------------------------------- 1 | arb-dir: lib/l10n 2 | template-arb-file: app_en.arb 3 | output-localization-file: app_localizations.dart -------------------------------------------------------------------------------- /lib/apis/abstact_api.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gemairo/hive/adapters.dart'; 3 | 4 | abstract class Api { 5 | Account account; 6 | Api(this.account); 7 | late bool isOnline; 8 | 9 | Future refreshProfilePicture(Person person) async {} 10 | Future refreshCalendarEvents(Person person) async {} 11 | Future refreshGrade(Person person, Grade grade) async {} 12 | Future refreshSchoolYear(Person person, SchoolYear schoolYear, 13 | void Function(int completed, int total) progress) async {} 14 | Future refreshAll(Person person) async {} 15 | Future logout() async {} 16 | 17 | Widget buildLogin(BuildContext context); 18 | Widget? buildConfig(BuildContext context, {required Person person}); 19 | } 20 | -------------------------------------------------------------------------------- /lib/apis/account_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gemairo/apis/saaf.dart'; 3 | import 'package:gemairo/hive/adapters.dart'; 4 | import 'package:gemairo/hive/extentions.dart'; 5 | import 'package:hive/hive.dart'; 6 | 7 | class AccountManager { 8 | late List accountsList = 9 | Hive.box('accountList').values.toList().unique((x) => x.uuid); 10 | late List personList = accountsList 11 | .map((e) => e.profiles) 12 | .expand((x) => x) 13 | .toList() 14 | .unique((x) => x.uuid); 15 | 16 | Account getActive() { 17 | if (accountsList.isNotEmpty) { 18 | List accountListWithActiveProfile = accountsList 19 | .where((account) => account.profiles 20 | .map((profile) => profile.uuid) 21 | .contains(config.activeProfileId)) 22 | .toList(); 23 | if (accountListWithActiveProfile.isNotEmpty) { 24 | return accountListWithActiveProfile.first; 25 | } else { 26 | //No active account has been set, setting one... 27 | config.activeProfileId = accountsList.first.profiles.first.uuid; 28 | config.save(); 29 | return accountsList.first; 30 | } 31 | } else { 32 | return Account(); 33 | } 34 | } 35 | 36 | bool alreadyExists(Account account, {bool unsaved = false}) => 37 | accountsList.map((e) => e.uuid).contains(account.uuid) || 38 | personList.map((e) => unsaved ? e.id : e.uuid).any((uuid) => 39 | account.profiles.map((e) => unsaved ? e.id : e.uuid).contains(uuid)); 40 | 41 | void addAccount(Account account) { 42 | if (!alreadyExists(account)) { 43 | Hive.box('accountList').add(account); 44 | } 45 | } 46 | } 47 | 48 | class AccountProvider extends ChangeNotifier { 49 | Account get account => AccountManager().getActive(); 50 | 51 | Person get person => AccountManager().getActive().activeProfile!; 52 | 53 | SchoolYear get schoolYear => 54 | AccountManager().getActive().activeProfile!.activeSchoolYear; 55 | 56 | List get _activeFilters => 57 | AccountManager().getActive().activeProfile!.activeFilters; 58 | List activeFilters({bool isGlobal = false}) => isGlobal 59 | ? _activeFilters 60 | : _activeFilters.where((f) => !f.isGlobal).toList(); 61 | 62 | void changeAccount(int? newid) { 63 | config.activeProfileId = newid ?? config.activeProfileId; 64 | config.save(); 65 | notifyListeners(); 66 | Saaf.instance?.setAdRequest(force: true); 67 | } 68 | 69 | void changeSchoolYear(int newid) { 70 | person.config.activeSchoolYearId = newid; 71 | person.activeFilters.clear(); 72 | account.save(); 73 | notifyListeners(); 74 | } 75 | 76 | void addToFilter(Filter filter, {bool isGlobal = false}) { 77 | _activeFilters.add(filter..isGlobal = isGlobal); 78 | notifyListeners(); 79 | } 80 | 81 | void removeFromFilterWhere(bool Function(Filter) test) { 82 | _activeFilters.removeWhere(test); 83 | notifyListeners(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/apis/local_file.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:cr_file_saver/file_saver.dart'; 4 | import 'package:file_picker/file_picker.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:gemairo/apis/abstact_api.dart'; 7 | import 'package:gemairo/hive/adapters.dart'; 8 | import 'package:gemairo/main.dart'; 9 | import 'package:hive/hive.dart'; 10 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 11 | 12 | class LocalFile implements Api { 13 | @override 14 | Account account; 15 | LocalFile(this.account); 16 | 17 | @override 18 | late bool isOnline = false; 19 | 20 | @override 21 | Widget? buildConfig(BuildContext context, {required Person person}) { 22 | return null; 23 | } 24 | 25 | @override 26 | Widget buildLogin(BuildContext context) { 27 | return Scaffold( 28 | body: FutureBuilder( 29 | future: restoreHiveBox(), 30 | builder: (context, snapshot) { 31 | if (snapshot.hasData) { 32 | WidgetsBinding.instance.addPostFrameCallback((_) { 33 | Navigator.of(context).popUntil((r) => r.isFirst); 34 | Navigator.of(context).pushReplacement(MaterialPageRoute( 35 | builder: (context) => const Start(), 36 | )); 37 | }); 38 | } 39 | if (snapshot.hasError) { 40 | print(snapshot.error); 41 | return Center( 42 | child: AlertDialog( 43 | actionsAlignment: MainAxisAlignment.start, 44 | title: Text(AppLocalizations.of(context)!.somethingWentWrong), 45 | content: 46 | Text(AppLocalizations.of(context)!.localfileFailedWarning), 47 | actions: [ 48 | FilledButton.icon( 49 | onPressed: () => Navigator.pop(context), 50 | icon: const Icon(Icons.navigate_before), 51 | label: Text(AppLocalizations.of(context)!.back)) 52 | ], 53 | ), 54 | ); 55 | } 56 | return const Center(child: CircularProgressIndicator()); 57 | }, 58 | ), 59 | ); 60 | } 61 | 62 | @override 63 | Future refreshAll(Person person) async {} 64 | 65 | @override 66 | Future refreshCalendarEvents(Person person) async {} 67 | 68 | @override 69 | Future refreshGrade(Person person, Grade grade) async {} 70 | 71 | @override 72 | Future refreshProfilePicture(Person person) async {} 73 | 74 | @override 75 | Future refreshSchoolYear(Person person, SchoolYear schoolYear, 76 | void Function(int completed, int total) progress) async {} 77 | 78 | Future restoreHiveBox() async { 79 | FilePickerResult? result = await FilePicker.platform.pickFiles(); 80 | 81 | if (result != null) { 82 | final box = await Hive.openBox('tmpAccountlistBox'); 83 | final boxPath = box.path; 84 | await box.close(); 85 | 86 | //Get acccounts from imported box 87 | await File(result.files.single.path!).copy(boxPath!); 88 | await Hive.openBox('tmpAccountlistBox'); 89 | List importedAccounts = Hive.box('tmpAccountlistBox') 90 | .values 91 | .toList() 92 | .map((e) => e.copy) 93 | .toList(); 94 | Hive.deleteBoxFromDisk('tmpAccountlistBox'); 95 | //Remove accounts that already exist 96 | importedAccounts.removeWhere((element) => Hive.box('accountList') 97 | .values 98 | .map((e) => e.uuid) 99 | .contains(element.uuid)); 100 | //Change the API type, add all the imported accounts & change the active account ID 101 | for (Account importedAccount in importedAccounts) { 102 | importedAccount.apiType = AccountAPITypes.localFile; 103 | } 104 | Hive.box('accountList').addAll(importedAccounts); 105 | return Future.value(true); 106 | } else { 107 | throw 'No file selected'; 108 | } 109 | } 110 | 111 | @override 112 | Future logout() async {} 113 | } 114 | 115 | Future backupHiveBox( 116 | {required String boxName, BuildContext? context}) async { 117 | String? selectedDirectory = 118 | Platform.isIOS ? await FilePicker.platform.getDirectoryPath() : ''; 119 | 120 | if (selectedDirectory != null) { 121 | final box = Hive.box(boxName); 122 | final boxPath = box.path; 123 | await box.close(); 124 | 125 | try { 126 | if (Platform.isAndroid) { 127 | CRFileSaver.saveFileWithDialog(SaveFileDialogParams( 128 | sourceFilePath: boxPath!, 129 | destinationFileName: 130 | "Accounts-${DateTime.now().millisecondsSinceEpoch}.Gemairo")); 131 | } else { 132 | File(boxPath!).copy( 133 | "$selectedDirectory/Accounts-${DateTime.now().millisecondsSinceEpoch}.Gemairo"); 134 | } 135 | } catch (e) { 136 | if (context != null) { 137 | WidgetsBinding.instance.addPostFrameCallback((_) => 138 | ScaffoldMessenger.of(context).showSnackBar(SnackBar( 139 | content: Text( 140 | AppLocalizations.of(context)?.somethingWentWrong ?? "Error"), 141 | showCloseIcon: true, 142 | ))); 143 | } 144 | } finally { 145 | await Hive.openBox(boxName); 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /lib/apis/magister/screens/config.dart: -------------------------------------------------------------------------------- 1 | part of 'package:gemairo/apis/magister.dart'; 2 | -------------------------------------------------------------------------------- /lib/apis/magister/screens/terms.dart: -------------------------------------------------------------------------------- 1 | part of 'package:gemairo/apis/magister.dart'; 2 | 3 | class Terms extends StatefulWidget { 4 | const Terms({super.key}); 5 | 6 | @override 7 | State createState() => _Terms(); 8 | } 9 | 10 | class _Terms extends State { 11 | bool accepted = false; 12 | @override 13 | Widget build(BuildContext context) { 14 | String company = "Iddink Group"; 15 | return Scaffold( 16 | appBar: AppBar( 17 | title: Text(AppLocalizations.of(context)!.terms), 18 | ), 19 | body: Center( 20 | child: ConstrainedBox( 21 | constraints: const BoxConstraints(maxWidth: 640), 22 | child: Column( 23 | mainAxisAlignment: MainAxisAlignment.spaceAround, 24 | children: [ 25 | Padding( 26 | padding: const EdgeInsets.symmetric(horizontal: 20), 27 | child: Text.rich( 28 | TextSpan( 29 | style: Theme.of(context).textTheme.headlineSmall, 30 | children: [ 31 | TextSpan( 32 | text: AppLocalizations.of(context)! 33 | .termsContent(company), 34 | style: Theme.of(context).textTheme.bodyMedium) 35 | ]), 36 | )), 37 | Row( 38 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 39 | children: [ 40 | InkWell( 41 | onTap: () => setState(() { 42 | accepted = !accepted; 43 | }), 44 | child: Row( 45 | mainAxisAlignment: MainAxisAlignment.center, 46 | children: [ 47 | Checkbox( 48 | value: accepted, 49 | onChanged: (value) => setState(() { 50 | accepted = value!; 51 | }), 52 | ), 53 | Text( 54 | AppLocalizations.of(context)!.agree, 55 | ), 56 | ])), 57 | FilledButton.icon( 58 | icon: const Icon(Icons.navigate_next), 59 | onPressed: accepted 60 | ? () => Navigator.push( 61 | context, 62 | MaterialPageRoute( 63 | builder: (context) => const SignIn(), 64 | )) 65 | : null, 66 | label: 67 | Text(AppLocalizations.of(context)!.gContinue)), 68 | ]) 69 | ]), 70 | ), 71 | )); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/apis/random.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:gemairo/apis/abstact_api.dart'; 4 | import 'package:gemairo/hive/adapters.dart'; 5 | import 'package:gemairo/main.dart'; 6 | import 'package:hive/hive.dart'; 7 | 8 | Random random = Random(); 9 | 10 | String _generateRandomString(length) { 11 | var r = Random.secure(); 12 | var chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; 13 | return Iterable.generate(length, (_) => chars[r.nextInt(chars.length)]) 14 | .join(); 15 | } 16 | 17 | class RandomAccount implements Api { 18 | @override 19 | Account account; 20 | RandomAccount(this.account); 21 | 22 | @override 23 | late bool isOnline = false; 24 | 25 | @override 26 | Future refreshAll(Person person) async {} 27 | 28 | @override 29 | Future refreshGrade(Person person, Grade grade) async { 30 | grade = randomGrade(grade.subject); 31 | } 32 | 33 | @override 34 | Widget? buildConfig(BuildContext context, {required Person person}) { 35 | return null; 36 | } 37 | 38 | @override 39 | Widget buildLogin(BuildContext context) { 40 | account.apiType = AccountAPITypes.random; 41 | account.accountType = AccountTypes.other; 42 | account.apiStorage = null; 43 | account.id = random.nextInt(99999); 44 | account.profiles = List.generate(2, (index) { 45 | List subjects = 46 | List.generate(random.nextInt(5) + 5, (index) => randomSubject()); 47 | return Person( 48 | id: random.nextInt(99999), 49 | firstName: "Random", 50 | lastName: index.toString()) 51 | ..config = (PersonConfig()..activeSchoolYearId = 0) 52 | ..rawSchoolYears = List.generate( 53 | 5, 54 | (index) => SchoolYear( 55 | start: DateTime.now().add(Duration(days: 365 * index)), 56 | end: DateTime.now().add(Duration(days: 365 * (index + 1))), 57 | groupCode: index.toString(), 58 | groupName: "Klas $index", 59 | id: index, 60 | studyCode: "studyCode") 61 | ..grades = List.generate( 62 | 100, 63 | (index) => 64 | randomGrade(subjects[random.nextInt(subjects.length)]))) 65 | ..calendarEvents = List.generate( 66 | 5, 67 | (index) => CalendarEvent( 68 | start: DateTime.now().add(Duration( 69 | days: random.nextInt(12), hours: random.nextInt(24))), 70 | locations: ["12G"], 71 | end: DateTime.now().add(Duration( 72 | days: random.nextInt(12), hours: random.nextInt(24))), 73 | endHour: 0, 74 | id: random.nextInt(999), 75 | isFinished: random.nextBool(), 76 | startHour: 0, 77 | subjectsNames: subjects 78 | .map((e) => e.name) 79 | .take(random.nextInt(2) + 1) 80 | .toList(), 81 | teacherNames: ["Wod"], 82 | type: CalendarEventTypes.values[random.nextInt(6)])); 83 | }); 84 | Hive.box('accountList').add(account); 85 | WidgetsBinding.instance.addPostFrameCallback((_) { 86 | Navigator.of(context).popUntil((r) => r.isFirst); 87 | Navigator.of(context).pushReplacement(MaterialPageRoute( 88 | builder: (context) => const Start(), 89 | )); 90 | }); 91 | return const Center(child: CircularProgressIndicator()); 92 | } 93 | 94 | @override 95 | Future refreshCalendarEvents(Person person) async {} 96 | 97 | @override 98 | Future refreshProfilePicture(Person person) async {} 99 | 100 | @override 101 | Future refreshSchoolYear(Person person, SchoolYear schoolYear, 102 | void Function(int completed, int total) progress) async {} 103 | 104 | @override 105 | Future logout() async {} 106 | } 107 | 108 | Subject randomSubject() { 109 | return Subject( 110 | rawCode: _generateRandomString(3), 111 | rawName: _generateRandomString(6), 112 | id: random.nextInt(99999)); 113 | } 114 | 115 | Grade randomGrade(Subject subject) { 116 | return Grade( 117 | gradeString: ((random.nextInt(90) + 10) / 10).toString(), 118 | weight: random.nextInt(20).toDouble() + 1, 119 | subject: subject, 120 | description: "Description for a ${subject.name} grade", 121 | addedDate: DateTime.now().subtract(Duration(days: random.nextInt(250))), 122 | counts: true, 123 | id: random.nextInt(9999), 124 | type: GradeType.grade, 125 | teacherCode: subject.name, 126 | schoolQuarter: SchoolQuarter( 127 | shortname: "SE", 128 | end: DateTime.now(), 129 | id: 0, 130 | name: "School examen", 131 | start: DateTime.now()), 132 | isPTA: random.nextBool(), 133 | sufficient: random.nextBool()); 134 | } 135 | -------------------------------------------------------------------------------- /lib/apis/somtoday.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:math'; 3 | 4 | import 'package:dio/dio.dart'; 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:gemairo/apis/account_manager.dart'; 8 | import 'package:gemairo/hive/adapters.dart'; 9 | import 'package:gemairo/hive/extentions.dart'; 10 | import 'package:gemairo/widgets/card.dart'; 11 | import 'package:url_launcher/url_launcher.dart'; 12 | import 'package:webview_flutter/webview_flutter.dart'; 13 | import 'abstact_api.dart'; 14 | import 'package:pointycastle/export.dart' as castle; 15 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 16 | 17 | part 'package:gemairo/apis/somtoday/api.dart'; 18 | part 'package:gemairo/apis/somtoday/screens/config.dart'; 19 | part 'package:gemairo/apis/somtoday/screens/login.dart'; 20 | part 'package:gemairo/apis/somtoday/screens/school_picker.dart'; 21 | part 'package:gemairo/apis/somtoday/screens/terms.dart'; 22 | part 'package:gemairo/apis/somtoday/translate.dart'; 23 | 24 | class SomToDay implements Api { 25 | @override 26 | Account account; 27 | SomToDay(this.account); 28 | 29 | late SomToDayApi api = SomToDayApi(account); 30 | 31 | @override 32 | late bool isOnline = true; 33 | 34 | @override 35 | Widget? buildConfig(BuildContext context, {required Person person}) { 36 | return null; 37 | } 38 | 39 | @override 40 | Widget buildLogin(BuildContext context) { 41 | return Terms(account); 42 | } 43 | 44 | @override 45 | Future refreshAll(Person person) async {} 46 | 47 | @override 48 | Future refreshCalendarEvents(Person person) async { 49 | // TODO: implement refreshCalendarEvents 50 | } 51 | 52 | @override 53 | Future refreshGrade(Person person, Grade grade) async { 54 | // TODO: implement refreshGrade 55 | } 56 | 57 | @override 58 | Future refreshProfilePicture(Person person) async { 59 | var img = (await api.dio.get( 60 | "/rest/v1/leerlingen?additional=pasfoto", 61 | options: Options( 62 | responseType: ResponseType.bytes, 63 | validateStatus: (status) => [200, 404].contains(status), 64 | ), 65 | )); 66 | 67 | person.profilePicture = img.data["items"].where((item) => 68 | item["links"] 69 | .where((link) => link["type"] == "leerling.RLeerling")["id"] == 70 | person.id)["additionalObjects"]["pasfoto"]["datauri"]; 71 | if (account.isInBox) account.save(); 72 | } 73 | 74 | @override 75 | Future refreshSchoolYear(Person person, SchoolYear schoolYear, 76 | void Function(int completed, int total) progress) async { 77 | // TODO: implement refreshSchoolYear 78 | } 79 | 80 | @override 81 | Future logout() async { 82 | // TODO: implement logout 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/apis/somtoday/api.dart: -------------------------------------------------------------------------------- 1 | part of 'package:gemairo/apis/somtoday.dart'; 2 | 3 | class SomToDayApi extends SomToDay { 4 | SomToDayApi(super.account); 5 | 6 | late Dio dio = Dio( 7 | BaseOptions( 8 | baseUrl: account.apiStorage?.baseUrl ?? "", 9 | headers: {"authorization": "Bearer ${account.apiStorage?.accessToken}"}, 10 | connectTimeout: const Duration(seconds: 15)), 11 | )..interceptors.addAll([ 12 | InterceptorsWrapper( 13 | onError: (e, handler) async { 14 | if (e.response?.data != null && 15 | e.response?.data["error"] == "invalid_grant") { 16 | return handler.reject(DioException( 17 | requestOptions: e.requestOptions, 18 | error: 19 | "Dit account is uitgelogd, verwijder je account en log opnieuw in. (Spijt me zeer hier is nog geen automatische support voor)", 20 | response: e.response, 21 | )); 22 | } 23 | handler.next(e); 24 | }, 25 | ), 26 | QueuedInterceptorsWrapper(onRequest: (options, handler) async { 27 | if (account.apiStorage!.accessToken == null || 28 | DateTime.now().millisecondsSinceEpoch > 29 | account.apiStorage!.expiry!) { 30 | debugPrint("Accestoken expired"); 31 | await refreshToken().onError((e, stack) { 32 | handler.reject(e as DioException); 33 | return; 34 | }); 35 | } 36 | 37 | options.baseUrl = account.apiStorage!.baseUrl; 38 | 39 | options.headers["Authorization"] = 40 | "Bearer ${account.apiStorage!.accessToken}"; 41 | 42 | return handler.next(options); 43 | }, onError: (e, handler) async { 44 | var options = e.requestOptions; 45 | 46 | Future retry() => dio.fetch(options).then( 47 | (r) => handler.resolve(r), 48 | onError: (e) => handler.reject(e), 49 | ); 50 | 51 | if (e.response?.data == "SecurityToken Expired") { 52 | debugPrint("Request failed, token is invalid"); 53 | 54 | if (options.headers["Authorization"] != 55 | "Bearer ${account.apiStorage!.accessToken}") { 56 | options.headers["Authorization"] = 57 | "Bearer ${account.apiStorage!.accessToken}"; 58 | 59 | return await retry(); 60 | } 61 | 62 | return await refreshToken().then((_) => retry()).onError( 63 | (e, stack) => handler.reject(e as DioException), 64 | ); 65 | } 66 | }) 67 | ]); 68 | 69 | Future setAccountDetails() async { 70 | Map res = (await dio.get("/rest/v1/account/")).data; 71 | account.accountType = AccountTypes.student; 72 | account.id = res["items"]["persoon"]["links"].first["id"]; 73 | 74 | Future initPerson(Person person) async { 75 | await Future.wait([ 76 | refreshProfilePicture(person), 77 | refreshCalendarEvents(person), 78 | setSchoolYears(person) 79 | ]); 80 | 81 | person.config.activeSchoolYearId = person.schoolYears.first.id; 82 | account.profiles.add(person); 83 | config.save(); 84 | } 85 | 86 | for (var item in res["items"]) { 87 | await initPerson(Person( 88 | id: item["persoon"]["links"].first["id"], 89 | firstName: item["persoon"]["roepnaam"] ?? "", 90 | lastName: item["persoon"]["achternaam"] ?? "")); 91 | } 92 | 93 | if (account.isInBox) account.save(); 94 | } 95 | 96 | Future refreshToken() async { 97 | await Dio(BaseOptions( 98 | contentType: Headers.formUrlEncodedContentType, 99 | )) 100 | .post( 101 | "https://somtoday.nl/oauth2/token", 102 | data: 103 | "refresh_token=${account.apiStorage!.refreshToken}&client_id=D50E0C06-32D1-4B41-A137-A9A850C892C2&grant_type=refresh_token", 104 | ) 105 | .then((res) async { 106 | saveTokens(res.data!); 107 | if (account.isInBox) account.save(); 108 | }).catchError((err) { 109 | throw err; 110 | }); 111 | } 112 | 113 | void saveTokens(tokenSet) { 114 | account.apiStorage!.accessToken = tokenSet["access_token"]; 115 | account.apiStorage!.refreshToken = tokenSet["refresh_token"]; 116 | account.apiStorage!.expiry = 117 | DateTime.now().add(const Duration(hours: 1)).millisecondsSinceEpoch; 118 | account.apiStorage!.baseUrl = tokenSet["somtoday_api_url"]; 119 | } 120 | 121 | Future setSchoolYears(Person person) async { 122 | if (account.isInBox) account.save(); 123 | } 124 | } 125 | 126 | Future getTokenSet(String url) async { 127 | if (url.startsWith("refreshtoken")) { 128 | Account tempAccount = Account(); 129 | 130 | tempAccount.apiStorage!.refreshToken = 131 | url.replaceFirst("refreshtoken=", ""); 132 | await SomToDay(tempAccount).api.refreshToken(); 133 | 134 | return { 135 | "access_token": tempAccount.apiStorage!.accessToken, 136 | "refresh_token": tempAccount.apiStorage!.refreshToken, 137 | }; 138 | } else { 139 | String? code = Uri.parse(url).queryParameters["code"]; 140 | 141 | Response res = await Dio().post( 142 | "https://somtoday.nl/oauth2/token", 143 | options: Options( 144 | contentType: "application/x-www-form-urlencoded", 145 | ), 146 | data: 147 | "code=$code&redirect_uri=somtodayleerling://oauth/callback&client_id=D50E0C06-32D1-4B41-A137-A9A850C892C2&grant_type=authorization_code&code_verifier=$codeVerifier", 148 | ); 149 | return res.data; 150 | } 151 | } 152 | 153 | String _generateRandomString() { 154 | var r = Random.secure(); 155 | var chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; 156 | return Iterable.generate(32, (_) => chars[r.nextInt(chars.length)]).join(); 157 | } 158 | 159 | String codeVerifier = _generateRandomString(); 160 | 161 | String createURL(String uuid) { 162 | String codeChallenge = base64Url 163 | .encode(castle.SHA256Digest() 164 | .process(Uint8List.fromList(codeVerifier.codeUnits))) 165 | .replaceAll('=', ''); 166 | return "https://somtoday.nl/oauth2/authorize?redirect_uri=somtodayleerling://oauth/callback&client_id=D50E0C06-32D1-4B41-A137-A9A850C892C2&response_type=code&prompt=login&scope=openid&code_challenge=$codeChallenge&code_challenge_method=S256&tenant_uuid=$uuid"; 167 | } 168 | -------------------------------------------------------------------------------- /lib/apis/somtoday/screens/config.dart: -------------------------------------------------------------------------------- 1 | part of 'package:gemairo/apis/somtoday.dart'; 2 | -------------------------------------------------------------------------------- /lib/apis/somtoday/screens/login.dart: -------------------------------------------------------------------------------- 1 | part of 'package:gemairo/apis/somtoday.dart'; 2 | 3 | enum _LoginOptions { 4 | refresh, 5 | browser, 6 | // token, 7 | } 8 | 9 | class _SignIn extends StatelessWidget { 10 | const _SignIn(this.account, this.schoolUUID); 11 | final Account account; 12 | final String schoolUUID; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | final ValueNotifier redirectUrl = ValueNotifier(""); 17 | WebViewController webViewController = WebViewController() 18 | ..setJavaScriptMode(JavaScriptMode.unrestricted) 19 | ..setNavigationDelegate( 20 | NavigationDelegate( 21 | onProgress: (int progress) { 22 | // Update loading bar. 23 | }, 24 | onPageStarted: (String url) {}, 25 | onPageFinished: (String url) {}, 26 | onWebResourceError: (WebResourceError error) {}, 27 | onNavigationRequest: (NavigationRequest request) { 28 | if (request.url.contains('?code')) { 29 | redirectUrl.value = request.url; 30 | return NavigationDecision.prevent; 31 | } 32 | return NavigationDecision.navigate; 33 | }, 34 | ), 35 | ); 36 | 37 | return Scaffold( 38 | appBar: AppBar( 39 | title: Text(AppLocalizations.of(context)!.loginWith("SomToDay")), 40 | actions: [ 41 | ValueListenableBuilder( 42 | valueListenable: redirectUrl, 43 | builder: (context, _, __) { 44 | if (redirectUrl.value != "") { 45 | return Container(); 46 | } 47 | 48 | return PopupMenuButton<_LoginOptions>( 49 | onSelected: (value) async { 50 | switch (value) { 51 | case _LoginOptions.refresh: 52 | webViewController 53 | .loadRequest(Uri.parse(createURL(schoolUUID))); 54 | 55 | break; 56 | case _LoginOptions.browser: 57 | await launchUrl(Uri.parse(createURL(schoolUUID)), 58 | mode: LaunchMode.externalNonBrowserApplication, 59 | webViewConfiguration: const WebViewConfiguration( 60 | enableDomStorage: false)); 61 | // redirectUrl.value = (await linkStream.first)!; 62 | break; 63 | } 64 | }, 65 | itemBuilder: (_) => [ 66 | PopupMenuItem( 67 | value: _LoginOptions.refresh, 68 | child: Text(AppLocalizations.of(context)!.reload), 69 | ), 70 | PopupMenuItem( 71 | value: _LoginOptions.browser, 72 | child: Text(AppLocalizations.of(context)!.openInBrowser), 73 | ), 74 | ], 75 | ); 76 | }, 77 | ) 78 | ], 79 | ), 80 | body: ValueListenableBuilder( 81 | valueListenable: redirectUrl, 82 | builder: (context, _, __) { 83 | if (redirectUrl.value == "") { 84 | WebViewCookieManager().clearCookies(); 85 | 86 | return WebViewWidget( 87 | controller: webViewController 88 | ..loadRequest(Uri.parse(createURL(schoolUUID)))); 89 | } 90 | return _FetchGrades(redirectUrl: redirectUrl, account: account); 91 | }, 92 | )); 93 | } 94 | } 95 | 96 | class _FetchGrades extends StatefulWidget { 97 | const _FetchGrades({required this.redirectUrl, required this.account}); 98 | 99 | final Account account; 100 | final ValueNotifier redirectUrl; 101 | 102 | @override 103 | State createState() => _FetchGradesState(); 104 | } 105 | 106 | class _FetchGradesState extends State<_FetchGrades> { 107 | @override 108 | Widget build(BuildContext context) { 109 | return FutureBuilder( 110 | future: Future(() async { 111 | SomToDay somtoday = SomToDay(widget.account); 112 | widget.account.apiType = AccountAPITypes.somToDay; 113 | somtoday.api.saveTokens(await getTokenSet(widget.redirectUrl.value)); 114 | await somtoday.api.setAccountDetails(); 115 | if (!AccountManager().alreadyExists(widget.account)) { 116 | return Future.value(widget.account); 117 | } else { 118 | throw "Account already exists"; 119 | } 120 | }), 121 | builder: (context, snapshot) { 122 | if (snapshot.hasData) { 123 | } else if (snapshot.hasError) { 124 | if (snapshot.error == "Account already exists") { 125 | //TODO: Share menu? 126 | } 127 | return Center( 128 | child: ListTile( 129 | title: Text("${snapshot.error}"), 130 | subtitle: Text("${snapshot.stackTrace}"), 131 | ), 132 | ); 133 | } 134 | return Center( 135 | child: CircularProgressIndicator( 136 | semanticsLabel: 137 | AppLocalizations.of(context)!.whileAccountInformationFetched), 138 | ); 139 | }, 140 | ); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /lib/apis/somtoday/screens/school_picker.dart: -------------------------------------------------------------------------------- 1 | part of 'package:gemairo/apis/somtoday.dart'; 2 | 3 | Future> getSchools() async { 4 | var test = (await Dio().get('https://servers.somtoday.nl/organisaties.json')) 5 | .data 6 | .first["instellingen"]; 7 | return Future.value(List.from(test.map((e) => SomToDaySchool(e)))); 8 | } 9 | 10 | class SomToDaySchool { 11 | String location = ""; 12 | String name = ""; 13 | String uuid = ""; 14 | 15 | SomToDaySchool(Map map) { 16 | location = map["plaats"] != "" 17 | ? map["plaats"].toString().toLowerCase().capitalize() 18 | : "Onbekend"; 19 | name = map["naam"]; 20 | uuid = map["uuid"]; 21 | } 22 | } 23 | 24 | class SchoolPicker extends StatelessWidget { 25 | const SchoolPicker(this.account, {super.key}); 26 | final Account account; 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return Scaffold( 31 | appBar: AppBar( 32 | title: Text(AppLocalizations.of(context)!.chooseaSchool), 33 | ), 34 | body: FutureBuilder( 35 | future: getSchools(), 36 | builder: (context, snapshot) { 37 | if (snapshot.hasData) { 38 | TextEditingController controller = TextEditingController(); 39 | ValueNotifier valueListenable = ValueNotifier(""); 40 | return ValueListenableBuilder( 41 | valueListenable: valueListenable, 42 | builder: (context, value, _) { 43 | return ListView( 44 | children: [ 45 | Padding( 46 | padding: const EdgeInsets.symmetric( 47 | horizontal: 16, vertical: 10), 48 | child: GemairoCard( 49 | child: Padding( 50 | padding: const EdgeInsets.all(4), 51 | child: TextField( 52 | controller: controller, 53 | onTapOutside: (event) => FocusScope.of(context) 54 | .requestFocus(FocusNode()), 55 | onChanged: (value) => 56 | valueListenable.value = value, 57 | decoration: InputDecoration( 58 | prefixIcon: const Icon(Icons.search), 59 | border: InputBorder.none, 60 | hintText: AppLocalizations.of(context)! 61 | .searchaSchool, 62 | filled: false), 63 | ), 64 | ), 65 | ), 66 | ), 67 | ...snapshot.data! 68 | .where((school) => 69 | school.location.toLowerCase().contains( 70 | valueListenable.value.toLowerCase()) || 71 | school.name.toLowerCase().contains( 72 | valueListenable.value.toLowerCase())) 73 | .map((e) => ListTile( 74 | title: Text(e.name), 75 | leading: const CircleAvatar( 76 | child: Icon(Icons.school), 77 | ), 78 | trailing: const Icon(Icons.navigate_next), 79 | subtitle: Text(e.location), 80 | onTap: () => Navigator.push( 81 | context, 82 | MaterialPageRoute( 83 | builder: (context) => 84 | _SignIn(account, e.uuid), 85 | )), 86 | )) 87 | ], 88 | ); 89 | }); 90 | } 91 | if (snapshot.hasError) { 92 | return const Center(child: Icon(Icons.warning_amber)); 93 | } 94 | return const Center( 95 | child: CircularProgressIndicator(), 96 | ); 97 | }, 98 | )); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/apis/somtoday/screens/terms.dart: -------------------------------------------------------------------------------- 1 | part of 'package:gemairo/apis/somtoday.dart'; 2 | 3 | class Terms extends StatefulWidget { 4 | const Terms(this.account, {super.key}); 5 | final Account account; 6 | 7 | @override 8 | State createState() => _Terms(); 9 | } 10 | 11 | class _Terms extends State { 12 | bool accepted = false; 13 | @override 14 | Widget build(BuildContext context) { 15 | String company = "Topicus"; 16 | return Scaffold( 17 | appBar: AppBar( 18 | title: Text(AppLocalizations.of(context)!.terms), 19 | ), 20 | body: 21 | Column(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ 22 | Padding( 23 | padding: const EdgeInsets.symmetric(horizontal: 20), 24 | child: Text.rich(TextSpan( 25 | style: Theme.of(context).textTheme.headlineSmall, 26 | children: [ 27 | TextSpan( 28 | text: 29 | AppLocalizations.of(context)!.termsContent(company), 30 | style: Theme.of(context).textTheme.bodyMedium), 31 | ]))), 32 | Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ 33 | InkWell( 34 | onTap: () => setState(() { 35 | accepted = !accepted; 36 | }), 37 | child: 38 | Row(mainAxisAlignment: MainAxisAlignment.center, children: [ 39 | Checkbox( 40 | value: accepted, 41 | onChanged: (value) => setState(() { 42 | accepted = value!; 43 | }), 44 | ), 45 | Text( 46 | AppLocalizations.of(context)!.agree, 47 | ), 48 | ])), 49 | FilledButton.icon( 50 | icon: const Icon(Icons.navigate_next), 51 | onPressed: accepted 52 | ? () => Navigator.push( 53 | context, 54 | MaterialPageRoute( 55 | builder: (context) => SchoolPicker(widget.account), 56 | )) 57 | : null, 58 | label: Text(AppLocalizations.of(context)!.gContinue)), 59 | ]) 60 | ])); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/apis/somtoday/translate.dart: -------------------------------------------------------------------------------- 1 | part of 'package:gemairo/apis/somtoday.dart'; 2 | 3 | //TODO: SomToDay translate 4 | 5 | // Grade? somtodayGrade([Map? gotGrade]) { 6 | // if (gotGrade != null) { 7 | // return Grade(); 8 | // } 9 | // return null; 10 | // } 11 | 12 | // Subject? somtodaySubject([Map? subject]) { 13 | // if (subject != null) { 14 | // return Subject( 15 | // code: subject["afkorting"], 16 | // id: subject["links"].first["id"], 17 | // name: subject["naam"].toString().capitalize()); 18 | // } 19 | // return null; 20 | // } 21 | 22 | // SchoolQuarter? somtodaySchoolQuarter(int? per) { 23 | // if (per != null) { 24 | // return SchoolQuarter(); 25 | // } 26 | // return null; 27 | // } 28 | 29 | // SchoolYear? somtodaySchoolYear([Map? year]) { 30 | // if (year != null) { 31 | // return SchoolYear(); 32 | // } 33 | // return null; 34 | // } 35 | 36 | // CalendarEvent? somdayCalendarEvent([Map? event]) { 37 | // if (event != null) { 38 | // return CalendarEvent(); 39 | // } 40 | // return null; 41 | // } 42 | -------------------------------------------------------------------------------- /lib/background_tasks.dart: -------------------------------------------------------------------------------- 1 | part of 'main.dart'; 2 | 3 | @pragma('vm:entry-point') 4 | void backgroundFetchHeadlessTask(HeadlessTask task) async { 5 | String taskId = task.taskId; 6 | bool isTimeout = task.timeout; 7 | if (isTimeout) { 8 | print("[BackgroundFetch] Headless task timed-out: $taskId"); 9 | BackgroundFetch.finish(taskId); 10 | Hive.close(); 11 | return; 12 | } 13 | print('[BackgroundFetch] Headless event received.'); 14 | await initHive(); 15 | await Hive.openBox('config'); 16 | await Hive.openBox('accountList'); 17 | await backgroundCheck(); 18 | BackgroundFetch.finish(taskId); 19 | // Hive.close(); 20 | } 21 | 22 | @pragma('vm:entry-point') 23 | void notificationTapBackground(NotificationResponse notificationResponse) { 24 | // ignore: avoid_print 25 | print('notification(${notificationResponse.id}) action tapped: ' 26 | '${notificationResponse.actionId} with' 27 | ' payload: ${notificationResponse.payload}'); 28 | if (notificationResponse.input?.isNotEmpty ?? false) { 29 | // ignore: avoid_print 30 | print( 31 | 'notification action tapped with input: ${notificationResponse.input}'); 32 | } 33 | } 34 | 35 | Future backgroundCheck() async { 36 | if (config.enableNotifications != true) return; 37 | List accountsToCheck = AccountManager().accountsList; 38 | 39 | await Future.forEach(accountsToCheck, (account) async { 40 | Api api = account.api; 41 | 42 | await Future.forEach( 43 | account.profiles.where((person) => person.config.useForGradeCheck), 44 | (person) async { 45 | //Save current grade list 46 | List beforeFetchLatestGrades = 47 | List.of(person.schoolYears.allGrades.useable).toList(); 48 | //Refresh grade list 49 | await Future.wait(person.schoolYears.where((g) => g.grades.isNotEmpty).map( 50 | (SchoolYear sY) => api.refreshSchoolYear(person, sY, (i, t) {}))); 51 | //Save new grade list 52 | List afterFetchLatestGrades = 53 | List.of(person.schoolYears.allGrades.useable); 54 | List difference = afterFetchLatestGrades 55 | ..removeWhere((grade) => 56 | beforeFetchLatestGrades.map((e) => e.id).contains(grade.id)); 57 | if (difference.isNotEmpty) { 58 | print( 59 | "Er zijn ${difference.length} nieuwe cijfers voor ${person.firstName}"); 60 | FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = 61 | FlutterLocalNotificationsPlugin(); 62 | bool showPersonName = (AccountManager() 63 | .personList 64 | .where((person) => person.config.useForGradeCheck) 65 | .toList() 66 | .length > 67 | 1); 68 | await flutterLocalNotificationsPlugin.show( 69 | person.uuid, 70 | 'Nieuwe cijfers in Gemairo', 71 | difference.length == 1 || 72 | difference.subjects.every( 73 | (subject) => subject.id == difference.first.subject.id) 74 | ? 'Er is een nieuw cijfer van ${difference.first.subject.name} beschikbaar${showPersonName ? " voor ${person.firstName}" : ""} ' 75 | : 'Er zijn ${difference.length} nieuwe cijfers beschikbaar${showPersonName ? " voor ${person.firstName}" : ""}', 76 | const NotificationDetails( 77 | android: AndroidNotificationDetails( 78 | "GemairoGrades", "Nieuwe cijfers", 79 | channelDescription: 80 | "Ontvang een bericht zodra er een nieuw cijfer beschikbaar is!", 81 | importance: Importance.high, 82 | priority: Priority.high, 83 | groupKey: "GemairoGrades", 84 | icon: 'notification'), 85 | iOS: DarwinNotificationDetails(threadIdentifier: "GemairoGrades"), 86 | ), 87 | payload: "${person.uuid}-grade"); 88 | } 89 | }); 90 | 91 | await Future.forEach( 92 | account.profiles.where((person) => person.config.useForTestCheck), 93 | (person) async { 94 | //Save current test list 95 | List beforeFetchTests = 96 | List.of(person.calendarEvents.tests).toList(); 97 | //Refresh tests list 98 | await api.refreshCalendarEvents(person); 99 | //Save new test list 100 | List afterFetchTests = 101 | List.of(person.calendarEvents.tests).toList(); 102 | List difference = afterFetchTests 103 | ..removeWhere( 104 | (event) => beforeFetchTests.map((e) => e.id).contains(event.id)); 105 | if (difference.isNotEmpty) { 106 | FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = 107 | FlutterLocalNotificationsPlugin(); 108 | bool showPersonName = (AccountManager() 109 | .personList 110 | .where((person) => person.config.useForTestCheck) 111 | .toList() 112 | .length > 113 | 1); 114 | await flutterLocalNotificationsPlugin.show( 115 | person.uuid, 116 | 'Gemairo heeft nieuwe toetsen gevonden', 117 | difference.length == 1 || 118 | difference.map((e) => e.subjectsNames).every( 119 | (subject) => subject == difference.first.subjectsNames) 120 | ? 'Er is een nieuw toets van ${difference.first.subjectsNames.join(" ")} beschikbaar${showPersonName ? " voor ${person.firstName}" : ""} ' 121 | : 'Er zijn ${difference.length} nieuwe toetsen beschikbaar${showPersonName ? " voor ${person.firstName}" : ""}', 122 | const NotificationDetails( 123 | android: AndroidNotificationDetails( 124 | "GemairoTests", "Nieuwe toetsen", 125 | channelDescription: 126 | "Ontvang een bericht zodra er een nieuwe toets gevonden is!", 127 | importance: Importance.high, 128 | priority: Priority.high, 129 | groupKey: "GemairoTests", 130 | icon: 'notification'), 131 | iOS: DarwinNotificationDetails(threadIdentifier: "GemairoTests"), 132 | ), 133 | payload: "${person.uuid}-test"); 134 | } 135 | }); 136 | }); 137 | } 138 | 139 | Future initPlatformState() async { 140 | // Configure BackgroundFetch. 141 | int status = await BackgroundFetch.configure( 142 | BackgroundFetchConfig( 143 | minimumFetchInterval: 30, 144 | stopOnTerminate: false, 145 | enableHeadless: true, 146 | startOnBoot: true, 147 | requiresBatteryNotLow: false, 148 | requiresCharging: false, 149 | requiresStorageNotLow: false, 150 | requiresDeviceIdle: false, 151 | requiredNetworkType: NetworkType.ANY), (String taskId) async { 152 | print("[BackgroundFetch] Event received $taskId"); 153 | await backgroundCheck(); 154 | BackgroundFetch.finish(taskId); 155 | }, (String taskId) async { 156 | print("[BackgroundFetch] TASK TIMEOUT taskId: $taskId"); 157 | BackgroundFetch.finish(taskId); 158 | }); 159 | print('[BackgroundFetch] configure success: $status'); 160 | //Load latestdata on opening 161 | backgroundCheck(); 162 | } 163 | 164 | void loadLatestData() { 165 | List accountsToCheck = AccountManager().accountsList; 166 | Future.forEach(accountsToCheck, (account) async { 167 | Api api = account.api; 168 | Future.forEach(account.profiles, (person) async { 169 | await Future.wait(person.schoolYears.where((g) => g.grades.isNotEmpty).map( 170 | (SchoolYear sY) => api.refreshSchoolYear(person, sY, (i, t) {}))); 171 | api.refreshCalendarEvents(person); 172 | }); 173 | }); 174 | } 175 | -------------------------------------------------------------------------------- /lib/firebase_options.dart: -------------------------------------------------------------------------------- 1 | // File generated by FlutterFire CLI. 2 | // ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members 3 | import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; 4 | import 'package:flutter/foundation.dart' 5 | show defaultTargetPlatform, kIsWeb, TargetPlatform; 6 | 7 | /// Default [FirebaseOptions] for use with your Firebase apps. 8 | /// 9 | /// Example: 10 | /// ```dart 11 | /// import 'firebase_options.dart'; 12 | /// // ... 13 | /// await Firebase.initializeApp( 14 | /// options: DefaultFirebaseOptions.currentPlatform, 15 | /// ); 16 | /// ``` 17 | class DefaultFirebaseOptions { 18 | static FirebaseOptions get currentPlatform { 19 | if (kIsWeb) { 20 | throw UnsupportedError( 21 | 'DefaultFirebaseOptions have not been configured for web - ' 22 | 'you can reconfigure this by running the FlutterFire CLI again.', 23 | ); 24 | } 25 | switch (defaultTargetPlatform) { 26 | case TargetPlatform.android: 27 | return android; 28 | case TargetPlatform.iOS: 29 | return ios; 30 | case TargetPlatform.macOS: 31 | throw UnsupportedError( 32 | 'DefaultFirebaseOptions have not been configured for macos - ' 33 | 'you can reconfigure this by running the FlutterFire CLI again.', 34 | ); 35 | case TargetPlatform.windows: 36 | throw UnsupportedError( 37 | 'DefaultFirebaseOptions have not been configured for windows - ' 38 | 'you can reconfigure this by running the FlutterFire CLI again.', 39 | ); 40 | case TargetPlatform.linux: 41 | throw UnsupportedError( 42 | 'DefaultFirebaseOptions have not been configured for linux - ' 43 | 'you can reconfigure this by running the FlutterFire CLI again.', 44 | ); 45 | default: 46 | throw UnsupportedError( 47 | 'DefaultFirebaseOptions are not supported for this platform.', 48 | ); 49 | } 50 | } 51 | 52 | static const FirebaseOptions android = FirebaseOptions( 53 | apiKey: 'AIzaSyA-D4yWJYnLhI5idhJz2whEkKekI6STXqo', 54 | appId: '1:978543096660:android:2c5453859e4f9f31940ec1', 55 | messagingSenderId: '978543096660', 56 | projectId: 'gemairo-6c835', 57 | storageBucket: 'gemairo-6c835.appspot.com', 58 | ); 59 | 60 | static const FirebaseOptions ios = FirebaseOptions( 61 | apiKey: 'AIzaSyDcG2eDIzbFUVk67U3hZ8uWcbr9Pr7ZI1s', 62 | appId: '1:978543096660:ios:52724800456f3c21940ec1', 63 | messagingSenderId: '978543096660', 64 | projectId: 'gemairo-6c835', 65 | storageBucket: 'gemairo-6c835.appspot.com', 66 | iosClientId: '978543096660-1rg9mgl5nqa6fkn91c6cfg3a7808rqvj.apps.googleusercontent.com', 67 | iosBundleId: 'app.netlob.magiscore', 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /lib/screens/career.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; 3 | import 'package:gemairo/widgets/bottom_sheet.dart'; 4 | import 'package:gemairo/widgets/global/skeletons.dart'; 5 | import 'package:intl/intl.dart'; 6 | import 'package:gemairo/widgets/appbar.dart'; 7 | import 'package:provider/provider.dart'; 8 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 9 | 10 | import 'package:gemairo/apis/account_manager.dart'; 11 | import 'package:gemairo/hive/adapters.dart'; 12 | import 'package:gemairo/hive/extentions.dart'; 13 | 14 | import 'package:gemairo/widgets/card.dart'; 15 | import 'package:gemairo/widgets/charts/linechart_monthly_average.dart'; 16 | import 'package:gemairo/widgets/facts_header.dart'; 17 | import 'package:gemairo/widgets/filter.dart'; 18 | import 'package:gemairo/widgets/charts/barchart_frequency.dart'; 19 | import 'package:gemairo/widgets/charts/linechart_grades.dart'; 20 | import 'package:gemairo/widgets/cards/list_grade.dart'; 21 | 22 | class CareerOverview extends StatefulWidget { 23 | const CareerOverview({super.key}); 24 | 25 | @override 26 | State createState() => _CareerOverview(); 27 | } 28 | 29 | class _CareerOverview extends State { 30 | void addOrRemoveBadge(bool value, GradeListBadges badge) { 31 | if (value == true) { 32 | config.activeBadges.add(badge); 33 | } else { 34 | config.activeBadges.remove(badge); 35 | } 36 | setState(() {}); 37 | } 38 | 39 | @override 40 | Widget build(BuildContext context) { 41 | final AccountProvider acP = Provider.of(context); 42 | List allGrades = acP.person.allGrades 43 | ..sort((Grade a, Grade b) => b.addedDate.millisecondsSinceEpoch 44 | .compareTo(a.addedDate.millisecondsSinceEpoch)); 45 | List grades = allGrades.onlyFilterd(acP.person.activeFilters); 46 | 47 | List widgets = [ 48 | if (grades.isNotEmpty) ...[ 49 | if (grades.numericalGrades.length > 1) 50 | StaggeredGridTile.fit( 51 | crossAxisCellCount: 2, 52 | child: GemairoCard( 53 | child: Padding( 54 | padding: const EdgeInsets.all(8.0), 55 | child: LineChartGrades( 56 | grades: grades, 57 | showAverage: true, 58 | )), 59 | )), 60 | if (grades.numericalGrades.isNotEmpty) 61 | StaggeredGridTile.fit( 62 | crossAxisCellCount: 2, 63 | child: GemairoCard( 64 | title: Text(AppLocalizations.of(context)!.histogram), 65 | child: Padding( 66 | padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), 67 | child: BarChartFrequency( 68 | grades: grades, 69 | ), 70 | ))), 71 | StaggeredGridTile.extent( 72 | mainAxisExtent: 100, 73 | crossAxisCellCount: 2, 74 | child: FactCard( 75 | title: AppLocalizations.of(context)! 76 | .percentSufficient 77 | .capitalize(), 78 | value: 79 | "${grades.where((grade) => grade.isSufficient).length}/${grades.length}", 80 | extra: FactCardProgress( 81 | value: grades.getPresentageSufficient() / 100, 82 | ))), 83 | ...grades.useable 84 | .generateFactsList(context, 85 | Provider.of(context, listen: false).person) 86 | .skip(2) 87 | .map((e) => StaggeredGridTile.extent( 88 | mainAxisExtent: 100, 89 | crossAxisCellCount: 1, 90 | child: FactCard( 91 | title: e.title.capitalize(), 92 | value: e.value, 93 | onTap: e.onTap))), 94 | if (grades.numericalGrades.isNotEmpty && 95 | grades 96 | .map((g) => DateTime.parse( 97 | DateFormat('yyyy-MM-01').format(g.addedDate))) 98 | .toList() 99 | .unique() 100 | .length > 101 | 1) 102 | StaggeredGridTile.fit( 103 | crossAxisCellCount: 2, 104 | child: GemairoCard( 105 | title: Text(AppLocalizations.of(context)!.monthlyAverage), 106 | child: Padding( 107 | padding: const EdgeInsets.fromLTRB(8, 0, 8, 8), 108 | child: MonthlyLineChartGrades( 109 | grades: grades, 110 | showAverage: true, 111 | ), 112 | ))), 113 | ], 114 | ]; 115 | 116 | return ScaffoldSkeleton( 117 | appBar: GemairoAppBar( 118 | enableYearSwitcher: false, 119 | title: Text(AppLocalizations.of(context)!.searchStatistics)), 120 | onRefresh: () async { 121 | await acP.account.api.refreshAll(acP.person); 122 | acP.changeAccount(null); 123 | }, 124 | children: [ 125 | Padding( 126 | padding: const EdgeInsets.symmetric(horizontal: 16), 127 | child: FactsHeader( 128 | grades: grades.useable, 129 | )), 130 | Padding( 131 | padding: const EdgeInsets.symmetric(vertical: 8), 132 | child: FilterChips( 133 | isGlobal: true, 134 | grades: allGrades, 135 | )), 136 | GemairoCardList( 137 | maxCrossAxisExtent: 250, 138 | children: widgets, 139 | ), 140 | ...grades.sortByDate((e) => e.addedDate, doNotSort: true).entries.map( 141 | (e) => Padding( 142 | padding: const EdgeInsets.symmetric(horizontal: 8.0), 143 | child: Column(children: [ 144 | ListTile( 145 | title: Text(e.key, 146 | style: Theme.of(context) 147 | .textTheme 148 | .titleMedium 149 | ?.copyWith( 150 | color: 151 | Theme.of(context).colorScheme.primary)), 152 | dense: true, 153 | ), 154 | ...e.value.map((e) => GradeTile( 155 | grade: e, 156 | grades: grades, 157 | onTap: () => showGemairoModalBottomSheet(children: [ 158 | GradeInformation( 159 | context: context, 160 | grade: e, 161 | grades: grades, 162 | showGradeCalculate: true, 163 | ) 164 | ], context: context), 165 | )) 166 | ]), 167 | ), 168 | ) 169 | ]); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /lib/screens/login.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 5 | 6 | import 'package:gemairo/hive/adapters.dart'; 7 | 8 | import 'package:gemairo/apis/local_file.dart'; 9 | import 'package:gemairo/apis/random.dart'; 10 | import 'package:gemairo/apis/somtoday.dart'; 11 | import 'package:gemairo/apis/magister.dart'; 12 | 13 | class LoginView extends StatefulWidget { 14 | const LoginView({super.key}); 15 | 16 | @override 17 | State createState() => _LoginView(); 18 | } 19 | 20 | class _LoginView extends State { 21 | final List backgroundIcons = [ 22 | Icons.calculate_outlined, 23 | Icons.school_outlined, 24 | Icons.book_outlined, 25 | Icons.work_outline, 26 | Icons.history_edu_outlined, 27 | Icons.bar_chart_outlined, 28 | Icons.architecture_outlined, 29 | Icons.functions_outlined, 30 | Icons.sports_basketball_outlined 31 | ]; 32 | late final List icons; 33 | 34 | @override 35 | void initState() { 36 | super.initState(); 37 | icons = List.generate( 38 | 2000, 39 | (int index) => 40 | backgroundIcons[Random().nextInt(backgroundIcons.length)]); 41 | } 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | return Scaffold( 46 | extendBodyBehindAppBar: true, 47 | appBar: AppBar( 48 | backgroundColor: 49 | Theme.of(context).colorScheme.surface.withOpacity(0.2), 50 | ), 51 | body: Stack(children: [ 52 | Container( 53 | width: MediaQuery.of(context).size.width, 54 | height: MediaQuery.of(context).size.height, 55 | color: Theme.of(context).colorScheme.surface, 56 | child: Wrap( 57 | alignment: WrapAlignment.spaceEvenly, 58 | spacing: 5, 59 | runSpacing: 5, 60 | children: icons 61 | .map((e) => Icon( 62 | e, 63 | color: Theme.of(context).colorScheme.surfaceContainerHighest, 64 | )) 65 | .toList(), 66 | ), 67 | ), 68 | SafeArea( 69 | child: Padding( 70 | padding: const EdgeInsets.all(32.0), 71 | child: Column( 72 | crossAxisAlignment: CrossAxisAlignment.stretch, 73 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 74 | children: [ 75 | Expanded( 76 | child: Center( 77 | child: Row( 78 | mainAxisAlignment: MainAxisAlignment.center, 79 | children: [ 80 | InkWell( 81 | onLongPress: () => Navigator.push( 82 | context, 83 | MaterialPageRoute( 84 | builder: (context) => RandomAccount(Account()) 85 | .buildLogin(context), 86 | )), 87 | child: Icon( 88 | const IconData(0xf201, fontFamily: "Gemairo"), 89 | size: Theme.of(context) 90 | .textTheme 91 | .headlineLarge! 92 | .fontSize! * 93 | 1.50, 94 | ), 95 | ), 96 | Text( 97 | " Gemairo", 98 | style: Theme.of(context) 99 | .textTheme 100 | .headlineLarge 101 | ?.copyWith( 102 | fontWeight: FontWeight.bold, 103 | fontSize: 40, 104 | ), 105 | ) 106 | ], 107 | ), 108 | ), 109 | ), 110 | Wrap( 111 | alignment: WrapAlignment.spaceBetween, 112 | children: [ 113 | FilledButton.tonal( 114 | onPressed: () => otherLoginDialog(context), 115 | child: Text(AppLocalizations.of(context)!.other)), 116 | FilledButton.icon( 117 | onPressed: () => Navigator.push( 118 | context, 119 | MaterialPageRoute( 120 | builder: (context) => 121 | Magister(Account()).buildLogin(context), 122 | )), 123 | icon: const Icon(Icons.login), 124 | label: Text(AppLocalizations.of(context)! 125 | .loginWith("Magister"))), 126 | ], 127 | ), 128 | ], 129 | ), 130 | ), 131 | ) 132 | ])); 133 | } 134 | 135 | Future otherLoginDialog(BuildContext context) { 136 | return showDialog( 137 | context: context, 138 | builder: (BuildContext context) { 139 | return AlertDialog( 140 | title: const Text('Login op een andere manier'), 141 | content: Column( 142 | mainAxisSize: MainAxisSize.min, 143 | children: [ 144 | ...otherLoginMethods.map( 145 | (e) => ListTile( 146 | leading: Icon(e.icon), 147 | title: 148 | Text(AppLocalizations.of(context)!.loginWith(e.name)), 149 | onTap: () => Navigator.of(context).push( 150 | MaterialPageRoute(builder: e.buildLogin), 151 | )), 152 | ), 153 | ] 154 | .map((e) => Card( 155 | elevation: 0, 156 | child: e, 157 | )) 158 | .toList(), 159 | ), 160 | ); 161 | }, 162 | ); 163 | } 164 | } 165 | 166 | List otherLoginMethods = [ 167 | // LoginMethod( 168 | // name: 'Magister', 169 | // buildLogin: Magister(Account()).buildLogin, 170 | // ), 171 | if (!kReleaseMode) 172 | LoginMethod( 173 | name: 'Somtoday', 174 | buildLogin: SomToDay(Account()).buildLogin, 175 | ), 176 | if (!kReleaseMode) 177 | LoginMethod( 178 | name: 'Random', 179 | buildLogin: RandomAccount(Account()).buildLogin, 180 | icon: Icons.developer_mode), 181 | LoginMethod( 182 | name: 'Import', 183 | buildLogin: LocalFile(Account()).buildLogin, 184 | icon: Icons.upload_file) 185 | ]; 186 | 187 | class LoginMethod { 188 | String name; 189 | Widget Function(BuildContext) buildLogin; 190 | IconData icon; 191 | 192 | LoginMethod( 193 | {required this.name, required this.buildLogin, this.icon = Icons.login}); 194 | } 195 | -------------------------------------------------------------------------------- /lib/screens/post_login.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:firebase_messaging/firebase_messaging.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 6 | import 'package:flutter_local_notifications/flutter_local_notifications.dart'; 7 | import 'package:gemairo/apis/ads.dart'; 8 | import 'package:permission_handler/permission_handler.dart'; 9 | import 'package:gemairo/hive/adapters.dart' hide PersonConfig; 10 | import 'package:gemairo/main.dart'; 11 | import 'package:gemairo/screens/settings.dart'; 12 | 13 | class SettingsReminder extends StatefulWidget { 14 | const SettingsReminder({super.key, required this.account}); 15 | 16 | final Account account; 17 | 18 | @override 19 | State createState() => _SettingsReminder(); 20 | } 21 | 22 | class _SettingsReminder extends State { 23 | @override 24 | void dispose() { 25 | super.dispose(); 26 | Ads.instance?.checkGDPRConsent(); 27 | } 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | return Scaffold( 32 | appBar: AppBar( 33 | title: Text(AppLocalizations.of(context)!.settings), 34 | ), 35 | body: Center( 36 | child: SafeArea( 37 | child: ConstrainedBox( 38 | constraints: const BoxConstraints(maxWidth: 640), 39 | child: SingleChildScrollView( 40 | padding: const EdgeInsets.only(bottom: 8), 41 | child: Column(children: [ 42 | SwitchListTile( 43 | title: Text(AppLocalizations.of(context)!.notifications), 44 | secondary: const Icon(Icons.notifications_active), 45 | subtitle: 46 | Text(AppLocalizations.of(context)!.notificationsExpl), 47 | value: config.enableNotifications, 48 | onChanged: (bool value) async { 49 | config.enableNotifications = value; 50 | config.save(); 51 | }, 52 | ), 53 | PersonConfigCarousel( 54 | profiles: widget.account.profiles, 55 | simpleView: true, 56 | widgetsNextToIndicator: [ 57 | FilledButton.icon( 58 | icon: const Icon(Icons.navigate_next), 59 | onPressed: () async { 60 | FlutterLocalNotificationsPlugin 61 | flutterLocalNotificationsPlugin = 62 | FlutterLocalNotificationsPlugin(); 63 | final bool? androidResult = 64 | await flutterLocalNotificationsPlugin 65 | .resolvePlatformSpecificImplementation< 66 | AndroidFlutterLocalNotificationsPlugin>() 67 | ?.requestNotificationsPermission(); 68 | final bool? iOSResult = 69 | await flutterLocalNotificationsPlugin 70 | .resolvePlatformSpecificImplementation< 71 | IOSFlutterLocalNotificationsPlugin>() 72 | ?.requestPermissions( 73 | alert: true, 74 | badge: true, 75 | sound: true, 76 | ); 77 | if (androidResult == true || iOSResult == true) { 78 | if (!(await Permission 79 | .ignoreBatteryOptimizations.isGranted)) { 80 | try { 81 | Permission.ignoreBatteryOptimizations 82 | .request(); 83 | } catch (e) { 84 | print(e); 85 | } 86 | } 87 | setState(() {}); 88 | WidgetsBinding.instance.addPostFrameCallback((_) { 89 | Gemairo.of(context).update(); 90 | }); 91 | } 92 | 93 | if (Platform.isAndroid || Platform.isIOS) { 94 | await FirebaseMessaging.instance 95 | .requestPermission( 96 | alert: true, 97 | announcement: false, 98 | badge: true, 99 | carPlay: false, 100 | criticalAlert: false, 101 | provisional: false, 102 | sound: true, 103 | ); 104 | } 105 | 106 | if (!context.mounted) return; 107 | Navigator.pushReplacement( 108 | context, 109 | MaterialPageRoute( 110 | builder: (context) => const Start(), 111 | ), 112 | ); 113 | }, 114 | label: Text(AppLocalizations.of(context)!.gContinue)), 115 | ], 116 | ), 117 | ])), 118 | ), 119 | ), 120 | ), 121 | ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /lib/screens/search.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gemairo/apis/ads.dart'; 3 | import 'package:gemairo/screens/career.dart'; 4 | import 'package:gemairo/widgets/bottom_sheet.dart'; 5 | import 'package:gemairo/widgets/card.dart'; 6 | import 'package:gemairo/widgets/filter.dart'; 7 | import 'package:gemairo/widgets/global/skeletons.dart'; 8 | import 'package:provider/provider.dart'; 9 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 10 | 11 | import 'package:gemairo/apis/account_manager.dart'; 12 | import 'package:gemairo/hive/adapters.dart'; 13 | import 'package:gemairo/hive/extentions.dart'; 14 | 15 | import 'package:gemairo/widgets/cards/list_grade.dart'; 16 | 17 | class SearchView extends StatefulWidget { 18 | const SearchView({super.key}); 19 | 20 | @override 21 | State createState() => _SearchView(); 22 | } 23 | 24 | class _SearchView extends State { 25 | final controller = TextEditingController(); 26 | 27 | @override 28 | void initState() { 29 | super.initState(); 30 | Ads.instance?.handleNavigate('search'); 31 | } 32 | 33 | void addOrRemoveBadge(bool value, GradeListBadges badge) { 34 | if (value == true) { 35 | config.activeBadges.add(badge); 36 | } else { 37 | config.activeBadges.remove(badge); 38 | } 39 | setState(() {}); 40 | } 41 | 42 | @override 43 | Widget build(BuildContext context) { 44 | final AccountProvider acP = Provider.of(context); 45 | List useable = acP.person.allGrades.onlyFilterd([ 46 | if (controller.text != "" || acP.person.activeFilters.isEmpty) 47 | Filter( 48 | name: "SearchValue", 49 | type: FilterTypes.inputString, 50 | filter: controller.text), 51 | ...acP.activeFilters(isGlobal: true) 52 | ]); 53 | 54 | void textfieldToFilter() => setState(() { 55 | if (controller.text != "") { 56 | acP.addToFilter( 57 | Filter( 58 | name: "SearchValue", 59 | type: FilterTypes.inputString, 60 | filter: controller.text), 61 | isGlobal: true); 62 | } 63 | controller.clear(); 64 | }); 65 | 66 | return ScaffoldSkeleton( 67 | injectOverlap: true, 68 | onRefresh: () async { 69 | AccountProvider acP = 70 | Provider.of(context, listen: false); 71 | await acP.account.api.refreshAll(acP.person); 72 | acP.changeAccount(null); 73 | }, 74 | children: [ 75 | Padding( 76 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), 77 | child: GemairoCard( 78 | child: Padding( 79 | padding: const EdgeInsets.all(4), 80 | child: TextField( 81 | controller: controller, 82 | onChanged: (value) => setState(() {}), 83 | onTapOutside: (event) => 84 | FocusScope.of(context).requestFocus(FocusNode()), 85 | onSubmitted: (value) => 86 | value != "" ? textfieldToFilter() : null, 87 | decoration: InputDecoration( 88 | prefixIcon: const Icon(Icons.search), 89 | suffixIcon: Wrap(children: [ 90 | if (controller.text.isNotEmpty) 91 | IconButton( 92 | onPressed: () { 93 | setState(() { 94 | controller.clear(); 95 | }); 96 | }, 97 | icon: const Icon(Icons.clear), 98 | ), 99 | GradeListOptions( 100 | addOrRemoveBadge: addOrRemoveBadge, 101 | ) 102 | ]), 103 | border: InputBorder.none, 104 | hintText: AppLocalizations.of(context)! 105 | .searchForGradesdescriptionsAndTeachers, 106 | filled: false), 107 | ), 108 | ), 109 | ), 110 | ), 111 | FilterChips( 112 | isGlobal: true, 113 | grades: acP.person.allGrades, 114 | extraButtons: [ 115 | if (controller.text != "") 116 | FilterChip( 117 | label: Text(AppLocalizations.of(context)!.add), 118 | avatar: Icon(Icons.add, 119 | color: Theme.of(context).colorScheme.primary), 120 | onSelected: (value) => textfieldToFilter(), 121 | selected: false), 122 | FilterChip( 123 | label: Text(AppLocalizations.of(context)!.seeStatistics), 124 | avatar: Icon( 125 | Icons.bar_chart, 126 | color: Theme.of(context).colorScheme.primary, 127 | ), 128 | onSelected: (value) { 129 | textfieldToFilter(); 130 | Navigator.push( 131 | context, 132 | MaterialPageRoute( 133 | builder: (context) => const CareerOverview(), 134 | )); 135 | }, 136 | selected: false) 137 | ], 138 | ), 139 | const SizedBox(height: 10), 140 | ...useable 141 | .sortByDate((e) => e.addedDate, doNotSort: true) 142 | .entries 143 | .map( 144 | (e) => Column(children: [ 145 | ListTile( 146 | title: Text(e.key, 147 | style: Theme.of(context).textTheme.titleMedium?.copyWith( 148 | color: Theme.of(context).colorScheme.primary)), 149 | dense: true, 150 | ), 151 | ...e.value.map((e) => GradeTile( 152 | grade: e, 153 | grades: useable, 154 | onTap: () => showGemairoModalBottomSheet(children: [ 155 | GradeInformation( 156 | context: context, 157 | grade: e, 158 | grades: useable, 159 | showGradeCalculate: true, 160 | ) 161 | ], context: context), 162 | )) 163 | ]), 164 | ) 165 | .toList(), 166 | ], 167 | ); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /lib/widgets/ads.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:firebase_remote_config/firebase_remote_config.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/scheduler.dart'; 6 | import 'package:gemairo/apis/ads.dart'; 7 | import 'package:google_mobile_ads/google_mobile_ads.dart'; 8 | import 'package:gemairo/hive/adapters.dart'; 9 | 10 | class Advertisement extends StatefulWidget { 11 | const Advertisement( 12 | {super.key, this.size = AdSize.fluid, this.type = 'banner'}); 13 | 14 | final AdSize size; 15 | final String type; 16 | 17 | @override 18 | State createState() => _Advertisement(); 19 | } 20 | 21 | class _Advertisement extends State { 22 | NativeAd? nativeAd; 23 | BannerAd? bannerAd; 24 | bool _nativeAdIsLoaded = false; 25 | 26 | /// Loads a native ad. 27 | void loadAd({AdSize size = AdSize.fluid, String type = 'banner'}) async { 28 | final String unitId = Ads.instance?.getAdmobUnitId(type) ?? ""; 29 | 30 | if (unitId.isEmpty) return; 31 | 32 | bannerAd = BannerAd( 33 | size: size, 34 | adUnitId: unitId, 35 | listener: BannerAdListener( 36 | onAdLoaded: (ad) { 37 | debugPrint('$BannerAd loaded.'); 38 | setState(() { 39 | _nativeAdIsLoaded = true; 40 | }); 41 | }, 42 | onAdFailedToLoad: (ad, error) { 43 | debugPrint('$BannerAd failed to load: $error'); 44 | ad.dispose(); 45 | }, 46 | ), 47 | request: Ads.request, 48 | )..load(); 49 | } 50 | 51 | @override 52 | void initState() { 53 | super.initState(); 54 | SchedulerBinding.instance.addPostFrameCallback((_) { 55 | if (Ads.instance != null) loadAd(size: widget.size, type: widget.type); 56 | }); 57 | } 58 | 59 | @override 60 | Widget build(BuildContext context) { 61 | return bannerAd != null && _nativeAdIsLoaded && !config.noAds 62 | ? ConstrainedBox( 63 | constraints: BoxConstraints( 64 | minWidth: 320, 65 | maxHeight: 66 | widget.size.height.isInfinite || widget.size.height.isNegative 67 | ? 60 68 | : widget.size.height.toDouble(), 69 | ), 70 | child: AdWidget(ad: bannerAd!)) 71 | : Container(); 72 | } 73 | } 74 | 75 | class BottomBanner extends StatelessWidget { 76 | const BottomBanner({ 77 | super.key, 78 | required this.child, 79 | required this.placement, 80 | // this.isEnabled, 81 | }); 82 | 83 | final Widget child; 84 | // final bool? isEnabled; 85 | final String placement; 86 | 87 | @override 88 | Widget build(BuildContext context) { 89 | bool showAd = !(Platform.isIOS || Platform.isAndroid) 90 | ? false 91 | : FirebaseRemoteConfig.instance.getBool('ads_bottom'); 92 | String bannerSize = !(Platform.isIOS || Platform.isAndroid) 93 | ? "" 94 | : FirebaseRemoteConfig.instance.getString('ads_bottom_size'); 95 | 96 | // if (isEnabled != null) { 97 | // showAd = isEnabled!; 98 | // } 99 | 100 | return Column(children: [ 101 | Expanded(child: child), 102 | if (showAd && placement == 'main') 103 | Container( 104 | decoration: BoxDecoration( 105 | color: Theme.of(context).navigationBarTheme.backgroundColor), 106 | child: Advertisement( 107 | size: bannerSize == 'large' ? AdSize.largeBanner : AdSize.banner, 108 | type: 'static_banner_$placement', 109 | ), 110 | ), 111 | if (showAd && placement == 'subject') 112 | SafeArea( 113 | child: Container( 114 | decoration: BoxDecoration( 115 | color: Theme.of(context).navigationBarTheme.backgroundColor), 116 | child: Advertisement( 117 | size: bannerSize == 'large' ? AdSize.largeBanner : AdSize.banner, 118 | type: 'static_banner_$placement', 119 | ), 120 | ), 121 | ), 122 | ]); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /lib/widgets/animations.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ElasticAnimation extends StatelessWidget { 4 | const ElasticAnimation({super.key, required this.child}); 5 | 6 | final Widget child; 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return AnimatedSwitcher( 11 | duration: const Duration(milliseconds: 500), 12 | switchInCurve: Curves.elasticOut, 13 | reverseDuration: const Duration(milliseconds: 10), 14 | transitionBuilder: (Widget child, Animation animation) { 15 | final offsetAnimation = Tween( 16 | begin: const Offset(0.0, .2), end: const Offset(0.0, 0.0)) 17 | .animate(animation); 18 | return FadeTransition( 19 | opacity: animation, 20 | child: SlideTransition( 21 | position: offsetAnimation, 22 | child: child, 23 | )); 24 | }, 25 | layoutBuilder: (currentChild, previousChildren) { 26 | return Stack( 27 | alignment: Alignment.bottomRight, 28 | children: [ 29 | ...previousChildren, 30 | if (currentChild != null) currentChild, 31 | ], 32 | ); 33 | }, 34 | child: child); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/widgets/announcements.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:firebase_remote_config/firebase_remote_config.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:gemairo/widgets/card.dart'; 7 | import 'package:url_launcher/url_launcher.dart'; 8 | 9 | class Announcement { 10 | String title; 11 | String body; 12 | bool urgent; 13 | List> buttons; 14 | 15 | Announcement( 16 | {required this.title, 17 | required this.body, 18 | required this.urgent, 19 | this.buttons = const []}); 20 | } 21 | 22 | List getAnnouncements() { 23 | if (!(Platform.isIOS || Platform.isAndroid)) return []; 24 | var data = 25 | jsonDecode(FirebaseRemoteConfig.instance.getString('announcements')); 26 | return data 27 | .map((data) => Announcement( 28 | title: data["title"], 29 | body: data["body"], 30 | urgent: data["urgent"], 31 | buttons: data["buttons"] 32 | .map>( 33 | (e) => {e["title"].toString(): Uri.parse(e["url"])}) 34 | .toList())) 35 | .toList(); 36 | } 37 | 38 | class AnnouncementCard extends StatelessWidget { 39 | const AnnouncementCard({super.key, required this.announcement}); 40 | 41 | final Announcement announcement; 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | return GemairoCard( 46 | isFilled: announcement.urgent, 47 | title: Text(announcement.title), 48 | child: Padding( 49 | padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), 50 | child: Column( 51 | crossAxisAlignment: CrossAxisAlignment.stretch, 52 | children: [ 53 | Text(announcement.body), 54 | Row( 55 | mainAxisAlignment: MainAxisAlignment.end, 56 | children: announcement.buttons 57 | .map( 58 | (e) => Padding( 59 | padding: const EdgeInsets.only(left: 5.0), 60 | child: FilledButton( 61 | onPressed: () => launchUrl( 62 | e.entries.toList().first.value, 63 | mode: LaunchMode.externalApplication, 64 | ), 65 | child: Text(e.entries.toList().first.key), 66 | ), 67 | ), 68 | ) 69 | .toList()), 70 | ], 71 | ), 72 | )); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/widgets/avatars.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gemairo/hive/adapters.dart'; 3 | import 'package:gemairo/hive/extentions.dart'; 4 | import 'package:gemairo/widgets/animations.dart'; 5 | 6 | class GradeAvatar extends StatelessWidget { 7 | const GradeAvatar( 8 | {super.key, 9 | required this.gradeString, 10 | this.isSufficient, 11 | this.decimalDigits}); 12 | 13 | final String gradeString; 14 | final bool? isSufficient; 15 | final int? decimalDigits; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | double? _grade = double.tryParse(gradeString.replaceAll(',', '.')); 20 | bool _isSufficient = _grade == null 21 | ? (isSufficient ?? true) 22 | : num.parse(_grade.toStringAsFixed(decimalDigits ?? 2)) >= 23 | config.sufficientFrom; 24 | 25 | String displayedGrade = 26 | _grade?.displayNumber(decimalDigits: decimalDigits) ?? gradeString; 27 | 28 | return ElasticAnimation( 29 | child: CircleAvatar( 30 | key: ValueKey(displayedGrade), 31 | backgroundColor: !_isSufficient 32 | ? Theme.of(context).colorScheme.errorContainer 33 | : null, 34 | radius: 25, 35 | child: Text(displayedGrade, 36 | style: !_isSufficient 37 | ? TextStyle( 38 | color: Theme.of(context).colorScheme.onErrorContainer) 39 | : null)), 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/widgets/bottom_sheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | showGemairoModalBottomSheet( 4 | {List children = const [], required context}) { 5 | return showModalBottomSheet( 6 | context: context, 7 | isScrollControlled: true, 8 | showDragHandle: true, 9 | isDismissible: true, 10 | useSafeArea: true, 11 | constraints: const BoxConstraints(maxWidth: 640), 12 | shape: Theme.of(context).bottomSheetTheme.shape, 13 | builder: (context) => DraggableScrollableSheet( 14 | initialChildSize: 0.6, 15 | minChildSize: 0.2, 16 | maxChildSize: 1, 17 | snap: true, 18 | snapSizes: const [.6, 1], 19 | expand: false, 20 | builder: (_, controller) => ListView( 21 | controller: controller, 22 | children: children, 23 | ), 24 | ), 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /lib/widgets/cards/list_schoolyear.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 4 | 5 | import 'package:gemairo/apis/account_manager.dart'; 6 | import 'package:gemairo/hive/adapters.dart'; 7 | import 'package:gemairo/hive/extentions.dart'; 8 | 9 | import 'package:gemairo/widgets/avatars.dart'; 10 | import 'package:gemairo/widgets/card.dart'; 11 | import 'package:gemairo/widgets/navigation.dart'; 12 | 13 | class RelatedSchoolYearsCard extends StatelessWidget { 14 | const RelatedSchoolYearsCard({super.key, required this.subject}); 15 | 16 | final Subject subject; 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | final AccountProvider acP = Provider.of(context); 21 | List schoolyears = acP.person.schoolYears 22 | .where((sY) => sY.grades.relatedSubjectGrades(subject).isNotEmpty) 23 | .toList(); 24 | return CarouselCard( 25 | title: AppLocalizations.of(context)!.otherSchoolyears, 26 | children: schoolyears.length > 1 27 | ? schoolyears.map((sY) { 28 | List useableGrades = 29 | sY.grades.relatedSubjectGrades(subject).toList(); 30 | return Padding( 31 | padding: const EdgeInsets.symmetric(horizontal: 16.0), 32 | child: InkWell( 33 | borderRadius: const BorderRadius.all(Radius.circular(32)), 34 | onTap: () => changeSchoolYear(context, newid: sY.id), 35 | child: Column( 36 | mainAxisAlignment: MainAxisAlignment.start, 37 | crossAxisAlignment: CrossAxisAlignment.stretch, 38 | children: [ 39 | Align( 40 | alignment: Alignment.topLeft, 41 | child: Padding( 42 | padding: const EdgeInsets.only(bottom: 8.0), 43 | child: GradeAvatar( 44 | gradeString: useableGrades.average.isNaN 45 | ? "-" 46 | : useableGrades.average 47 | .displayNumber(decimalDigits: 2)), 48 | )), 49 | Text( 50 | sY.groupName, 51 | maxLines: 2, 52 | overflow: TextOverflow.ellipsis, 53 | style: Theme.of(context).textTheme.titleMedium, 54 | ), 55 | Text( 56 | useableGrades.first.subject.name, 57 | style: TextStyle( 58 | color: Theme.of(context).colorScheme.primary), 59 | ) 60 | ], 61 | )), 62 | ); 63 | }).toList() 64 | : [ 65 | Padding( 66 | padding: const EdgeInsets.symmetric(horizontal: 16.0), 67 | child: Column( 68 | crossAxisAlignment: CrossAxisAlignment.center, 69 | mainAxisAlignment: MainAxisAlignment.center, 70 | children: [ 71 | Padding( 72 | padding: const EdgeInsets.all(8.0), 73 | child: Icon( 74 | Icons.not_interested, 75 | color: Theme.of(context).colorScheme.outline, 76 | size: 32, 77 | ), 78 | ), 79 | Text( 80 | AppLocalizations.of(context)!.noOtherSchoolyears, 81 | maxLines: 2, 82 | textAlign: TextAlign.center, 83 | overflow: TextOverflow.ellipsis, 84 | style: Theme.of(context).textTheme.titleMedium, 85 | ), 86 | ], 87 | ), 88 | ) 89 | ], 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/widgets/charts/barchart_frequency.dart: -------------------------------------------------------------------------------- 1 | import 'package:fl_chart/fl_chart.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:gemairo/hive/adapters.dart'; 4 | import 'package:gemairo/hive/extentions.dart'; 5 | 6 | class BarChartFrequency extends StatefulWidget { 7 | const BarChartFrequency({super.key, required this.grades}); 8 | 9 | final List grades; 10 | 11 | @override 12 | State createState() => _BarChartFrequency(); 13 | } 14 | 15 | class _BarChartFrequency extends State { 16 | bool showPattern = false; 17 | @override 18 | Widget build(BuildContext context) { 19 | List barData = []; 20 | for (var i in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) { 21 | barData.add( 22 | BarChartGroupData( 23 | x: i, 24 | barRods: [ 25 | BarChartRodData( 26 | borderRadius: showPattern 27 | ? const BorderRadius.only( 28 | topLeft: Radius.circular(4), topRight: Radius.circular(4)) 29 | : null, 30 | width: showPattern ? 15 : 8, 31 | toY: widget.grades.getGradeFrequency()[i] ?? 0, 32 | color: Theme.of(context).colorScheme.primary, 33 | rodStackItems: List.generate( 34 | widget.grades 35 | .getGradeFrequency() 36 | .values 37 | .reduce((curr, next) => curr > next ? curr : next) 38 | .toInt(), 39 | (index) => BarChartRodStackItem( 40 | index.toDouble(), 41 | index + 1, 42 | index.isOdd && showPattern 43 | ? (i > config.sufficientFrom) 44 | ? Theme.of(context).colorScheme.inversePrimary 45 | : Theme.of(context).colorScheme.errorContainer 46 | : i > config.sufficientFrom 47 | ? Theme.of(context).colorScheme.primary 48 | : Theme.of(context).colorScheme.error), 49 | ), 50 | backDrawRodData: showPattern 51 | ? null 52 | : BackgroundBarChartRodData( 53 | show: true, 54 | toY: widget.grades 55 | .getGradeFrequency() 56 | .values 57 | .reduce((curr, next) => curr > next ? curr : next), 58 | color: Theme.of(context).colorScheme.surfaceContainerHigh, 59 | ), 60 | ) 61 | ], 62 | showingTooltipIndicators: showPattern ? [0] : [], 63 | ), 64 | ); 65 | } 66 | return SizedBox( 67 | height: 250 - 64, 68 | child: Listener( 69 | behavior: HitTestBehavior.translucent, 70 | onPointerDown: (value) => setState(() { 71 | showPattern = true; 72 | }), 73 | onPointerUp: (value) => setState(() { 74 | showPattern = false; 75 | }), 76 | child: BarChart( 77 | swapAnimationDuration: const Duration(milliseconds: 150), 78 | swapAnimationCurve: Curves.linear, 79 | BarChartData( 80 | barTouchData: BarTouchData( 81 | touchTooltipData: BarTouchTooltipData( 82 | tooltipHorizontalAlignment: showPattern 83 | ? FLHorizontalAlignment.center 84 | : FLHorizontalAlignment.right, 85 | tooltipMargin: showPattern ? 5 : -10, 86 | fitInsideHorizontally: true, 87 | getTooltipItem: (group, groupIndex, rod, rodIndex) { 88 | return rod.toY > 0 89 | ? BarTooltipItem( 90 | (rod.toY).toInt().toString(), 91 | TextStyle( 92 | color: Theme.of(context).colorScheme.onSurface, 93 | ), 94 | ) 95 | : null; 96 | }, 97 | tooltipPadding: showPattern 98 | ? const EdgeInsets.symmetric(horizontal: 4, vertical: 0) 99 | : const EdgeInsets.symmetric(horizontal: 8, vertical: 4), 100 | tooltipRoundedRadius: 4, 101 | tooltipBorder: BorderSide( 102 | color: Theme.of(context).colorScheme.outline, width: 1), 103 | getTooltipColor: (line) => 104 | Theme.of(context).colorScheme.surface, 105 | ), 106 | ), 107 | titlesData: FlTitlesData( 108 | show: true, 109 | rightTitles: const AxisTitles( 110 | sideTitles: SideTitles(showTitles: false), 111 | ), 112 | topTitles: const AxisTitles( 113 | sideTitles: SideTitles(showTitles: false), 114 | ), 115 | bottomTitles: AxisTitles( 116 | sideTitles: SideTitles( 117 | showTitles: true, 118 | getTitlesWidget: (double value, TitleMeta meta) { 119 | return SideTitleWidget( 120 | axisSide: meta.axisSide, 121 | space: 4, 122 | child: Text(value.toInt().toString()), 123 | ); 124 | }, 125 | reservedSize: 20, 126 | ), 127 | ), 128 | leftTitles: AxisTitles( 129 | sideTitles: SideTitles( 130 | showTitles: showPattern, 131 | reservedSize: 25, 132 | getTitlesWidget: (value, meta) { 133 | return SideTitleWidget( 134 | axisSide: meta.axisSide, 135 | space: 4, 136 | child: Text( 137 | value.toInt().toString(), 138 | maxLines: 1, 139 | ), 140 | ); 141 | }, 142 | ), 143 | ), 144 | ), 145 | borderData: FlBorderData( 146 | show: false, 147 | ), 148 | barGroups: barData, 149 | gridData: FlGridData(show: showPattern), 150 | ), 151 | ), 152 | ), 153 | ); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /lib/widgets/charts/barchart_subjects_average.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:fl_chart/fl_chart.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:gemairo/hive/adapters.dart'; 6 | import 'package:gemairo/hive/extentions.dart'; 7 | 8 | class BarChartSubjectsAverage extends StatelessWidget { 9 | const BarChartSubjectsAverage( 10 | {super.key, required this.subjects, this.rounded = false}); 11 | 12 | final List subjects; 13 | final bool rounded; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | List barData = []; 18 | subjects.asMap().forEach((index, Subject subject) { 19 | double average = rounded 20 | ? subject.grades.average.round().toDouble() 21 | : subject.roundOnDecimals != null 22 | ? ((subject.grades.average * pow(10, subject.roundOnDecimals!)) 23 | .truncate() / 24 | pow(10, subject.roundOnDecimals!)) 25 | : subject.grades.average; 26 | barData.add(BarChartGroupData( 27 | x: index, 28 | barRods: [ 29 | BarChartRodData( 30 | toY: average, 31 | borderRadius: const BorderRadius.only( 32 | topLeft: Radius.circular(4), topRight: Radius.circular(4)), 33 | width: 16, 34 | color: (average < config.sufficientFrom) 35 | ? Theme.of(context).colorScheme.error 36 | : Theme.of(context).colorScheme.primary, 37 | rodStackItems: [ 38 | ...List.generate( 39 | 10, 40 | (index) => BarChartRodStackItem( 41 | index.toDouble(), 42 | index + 1, 43 | index.isOdd 44 | ? (average < config.sufficientFrom) 45 | ? Theme.of(context).colorScheme.errorContainer 46 | : Theme.of(context).colorScheme.inversePrimary 47 | : (average < config.sufficientFrom) 48 | ? Theme.of(context).colorScheme.error 49 | : Theme.of(context).colorScheme.primary)), 50 | ]) 51 | ], 52 | showingTooltipIndicators: [], 53 | )); 54 | }); 55 | return SizedBox( 56 | height: 175, 57 | child: BarChart( 58 | swapAnimationDuration: const Duration(milliseconds: 150), 59 | swapAnimationCurve: Curves.linear, 60 | BarChartData( 61 | maxY: 10, 62 | minY: 1, 63 | extraLinesData: ExtraLinesData(horizontalLines: [ 64 | HorizontalLine( 65 | y: config.sufficientFrom, 66 | color: Theme.of(context).colorScheme.error, 67 | strokeWidth: 3, 68 | dashArray: [20, 10]) 69 | ]), 70 | barTouchData: BarTouchData( 71 | touchCallback: (p0, p1) {}, 72 | touchTooltipData: BarTouchTooltipData( 73 | tooltipHorizontalAlignment: FLHorizontalAlignment.center, 74 | tooltipMargin: 5, 75 | fitInsideHorizontally: true, 76 | // fitInsideVertically: true, 77 | getTooltipItem: (group, groupIndex, rod, rodIndex) { 78 | return BarTooltipItem( 79 | '${subjects[group.x].name}: ', 80 | TextStyle( 81 | color: Theme.of(context).colorScheme.onSurface, 82 | ), 83 | children: [ 84 | TextSpan(text: rod.toY.displayNumber(decimalDigits: 2)), 85 | ], 86 | ); 87 | }, 88 | tooltipPadding: 89 | const EdgeInsets.symmetric(horizontal: 4, vertical: 2), 90 | tooltipRoundedRadius: 4, 91 | tooltipBorder: BorderSide( 92 | color: Theme.of(context).colorScheme.outline, width: 1), 93 | getTooltipColor: (line) => Theme.of(context).colorScheme.surface, 94 | ), 95 | ), 96 | titlesData: FlTitlesData( 97 | show: true, 98 | rightTitles: const AxisTitles( 99 | sideTitles: SideTitles(showTitles: false), 100 | ), 101 | topTitles: const AxisTitles( 102 | sideTitles: SideTitles(showTitles: false), 103 | ), 104 | bottomTitles: AxisTitles( 105 | sideTitles: SideTitles( 106 | showTitles: true, 107 | getTitlesWidget: (double value, TitleMeta meta) { 108 | return SideTitleWidget( 109 | axisSide: meta.axisSide, 110 | space: 4, 111 | child: Text(subjects[value.toInt()].code), 112 | ); 113 | }, 114 | reservedSize: 24, 115 | ), 116 | ), 117 | leftTitles: AxisTitles( 118 | sideTitles: SideTitles( 119 | showTitles: true, 120 | reservedSize: 25, 121 | getTitlesWidget: (value, meta) { 122 | return SideTitleWidget( 123 | axisSide: meta.axisSide, 124 | space: 4, 125 | child: Text( 126 | value.toInt().toString(), 127 | maxLines: 1, 128 | ), 129 | ); 130 | }, 131 | ), 132 | ), 133 | ), 134 | borderData: FlBorderData( 135 | show: false, 136 | ), 137 | barGroups: barData, 138 | gridData: const FlGridData( 139 | show: true, 140 | horizontalInterval: 2, 141 | verticalInterval: 1, 142 | ), 143 | ), 144 | ), 145 | ); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /lib/widgets/charts/barchart_subjects_min_max.dart: -------------------------------------------------------------------------------- 1 | import 'package:fl_chart/fl_chart.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:gemairo/hive/adapters.dart'; 4 | import 'package:gemairo/hive/extentions.dart'; 5 | 6 | class BarChartSubjectsMinMax extends StatelessWidget { 7 | const BarChartSubjectsMinMax({super.key, required this.subjects}); 8 | 9 | final List subjects; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | List barData = []; 14 | List useableSubjects = subjects 15 | .where((subject) => 16 | subject.grades.getHighest() != null && 17 | subject.grades.getHighest() != subject.grades.getLowest()) 18 | .toList(); 19 | 20 | useableSubjects.asMap().forEach((index, Subject subject) { 21 | barData.add(BarChartGroupData( 22 | x: index, 23 | barRods: [ 24 | BarChartRodData( 25 | toY: subject.grades.getHighest()!.grade, 26 | fromY: subject.grades.getLowest()!.grade, 27 | borderRadius: const BorderRadius.all(Radius.circular(4)), 28 | width: 16, 29 | color: Theme.of(context).colorScheme.primary, 30 | rodStackItems: [ 31 | BarChartRodStackItem( 32 | subject.grades.median - .1, 33 | subject.grades.median + .1, 34 | Theme.of(context).colorScheme.inversePrimary), 35 | ]) 36 | ], 37 | showingTooltipIndicators: [], 38 | )); 39 | }); 40 | return SizedBox( 41 | height: 175, 42 | child: BarChart( 43 | swapAnimationDuration: const Duration(milliseconds: 150), 44 | swapAnimationCurve: Curves.linear, 45 | BarChartData( 46 | maxY: 10, 47 | minY: (useableSubjects 48 | .expand((subject) => subject.grades) 49 | .toList() 50 | .getLowest() 51 | ?.grade ?? 52 | 2) - 53 | 1, 54 | extraLinesData: ExtraLinesData(horizontalLines: [ 55 | HorizontalLine( 56 | y: config.sufficientFrom, 57 | color: Theme.of(context).colorScheme.error, 58 | strokeWidth: 3, 59 | dashArray: [20, 10]) 60 | ]), 61 | barTouchData: BarTouchData( 62 | touchCallback: (p0, p1) {}, 63 | touchTooltipData: BarTouchTooltipData( 64 | tooltipHorizontalAlignment: FLHorizontalAlignment.left, 65 | tooltipMargin: 5, 66 | fitInsideHorizontally: true, 67 | fitInsideVertically: true, 68 | getTooltipItem: (group, groupIndex, rod, rodIndex) { 69 | TextStyle itemTextStyle = 70 | const TextStyle(fontWeight: FontWeight.normal); 71 | return BarTooltipItem( 72 | textAlign: TextAlign.left, 73 | '${useableSubjects[group.x].name}\n', 74 | TextStyle( 75 | color: Theme.of(context).colorScheme.onSurface, 76 | fontWeight: FontWeight.bold), 77 | children: [ 78 | TextSpan( 79 | text: 80 | "max: ${rod.toY.displayNumber(decimalDigits: 2)}\n", 81 | style: itemTextStyle), 82 | TextSpan( 83 | text: 84 | "med: ${useableSubjects[group.x].grades.median.displayNumber(decimalDigits: 2)}\n", 85 | style: itemTextStyle), 86 | TextSpan( 87 | text: 88 | "min: ${rod.fromY.displayNumber(decimalDigits: 2)}", 89 | style: itemTextStyle), 90 | ], 91 | ); 92 | }, 93 | tooltipPadding: 94 | const EdgeInsets.symmetric(horizontal: 4, vertical: 2), 95 | tooltipRoundedRadius: 4, 96 | tooltipBorder: BorderSide( 97 | color: Theme.of(context).colorScheme.outline, width: 1), 98 | getTooltipColor: (line) => 99 | Theme.of(context).colorScheme.surface, 100 | ), 101 | ), 102 | titlesData: FlTitlesData( 103 | show: true, 104 | rightTitles: const AxisTitles( 105 | sideTitles: SideTitles(showTitles: false), 106 | ), 107 | topTitles: const AxisTitles( 108 | sideTitles: SideTitles(showTitles: false), 109 | ), 110 | bottomTitles: AxisTitles( 111 | sideTitles: SideTitles( 112 | showTitles: true, 113 | getTitlesWidget: (double value, TitleMeta meta) { 114 | return SideTitleWidget( 115 | axisSide: meta.axisSide, 116 | space: 4, 117 | child: Text(useableSubjects[value.toInt()].code), 118 | ); 119 | }, 120 | reservedSize: 24, 121 | ), 122 | ), 123 | leftTitles: AxisTitles( 124 | sideTitles: SideTitles( 125 | showTitles: true, 126 | reservedSize: 25, 127 | getTitlesWidget: (value, meta) { 128 | return SideTitleWidget( 129 | axisSide: meta.axisSide, 130 | space: 4, 131 | child: Text( 132 | value.toInt().toString(), 133 | maxLines: 1, 134 | )); 135 | }, 136 | ), 137 | ), 138 | ), 139 | borderData: FlBorderData( 140 | show: false, 141 | ), 142 | barGroups: barData, 143 | gridData: const FlGridData( 144 | show: true, horizontalInterval: 1, verticalInterval: 1), 145 | ))); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /lib/widgets/charts/barchart_subjects_weight.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | import 'package:fl_chart/fl_chart.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:gemairo/hive/adapters.dart'; 5 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 6 | 7 | class BarChartSubjectsWeight extends StatelessWidget { 8 | const BarChartSubjectsWeight({super.key, required this.subjects}); 9 | 10 | final List subjects; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | List barData = []; 15 | subjects.asMap().forEach((index, Subject subject) { 16 | barData.add(BarChartGroupData( 17 | x: index, 18 | barRods: [ 19 | BarChartRodData( 20 | toY: subject.grades.map((grade) => grade.weight).sum / 21 | subject.grades.length, 22 | borderRadius: const BorderRadius.only( 23 | topLeft: Radius.circular(4), topRight: Radius.circular(4)), 24 | width: 16, 25 | color: Theme.of(context).colorScheme.primary, 26 | ) 27 | ], 28 | showingTooltipIndicators: [], 29 | )); 30 | }); 31 | return SizedBox( 32 | height: 175, 33 | child: BarChart( 34 | swapAnimationDuration: const Duration(milliseconds: 150), 35 | swapAnimationCurve: Curves.linear, 36 | BarChartData( 37 | barTouchData: BarTouchData( 38 | touchCallback: (p0, p1) {}, 39 | touchTooltipData: BarTouchTooltipData( 40 | tooltipHorizontalAlignment: FLHorizontalAlignment.center, 41 | tooltipMargin: 5, 42 | fitInsideHorizontally: true, 43 | fitInsideVertically: true, 44 | getTooltipItem: (group, groupIndex, rod, rodIndex) { 45 | TextStyle itemTextStyle = 46 | const TextStyle(fontWeight: FontWeight.normal); 47 | return BarTooltipItem( 48 | textAlign: TextAlign.left, 49 | '${subjects[group.x].name}\n', 50 | TextStyle( 51 | color: Theme.of(context).colorScheme.onSurface, 52 | fontWeight: FontWeight.bold), 53 | children: [ 54 | TextSpan( 55 | text: AppLocalizations.of(context)! 56 | .totalWeight(rod.toY), 57 | style: itemTextStyle), 58 | ], 59 | ); 60 | }, 61 | tooltipPadding: 62 | const EdgeInsets.symmetric(horizontal: 4, vertical: 2), 63 | tooltipRoundedRadius: 4, 64 | tooltipBorder: BorderSide( 65 | color: Theme.of(context).colorScheme.outline, width: 1), 66 | getTooltipColor: (line) => 67 | Theme.of(context).colorScheme.surface, 68 | ), 69 | ), 70 | titlesData: FlTitlesData( 71 | show: true, 72 | rightTitles: const AxisTitles( 73 | sideTitles: SideTitles(showTitles: false), 74 | ), 75 | topTitles: const AxisTitles( 76 | sideTitles: SideTitles(showTitles: false), 77 | ), 78 | bottomTitles: AxisTitles( 79 | sideTitles: SideTitles( 80 | showTitles: true, 81 | getTitlesWidget: (double value, TitleMeta meta) { 82 | return SideTitleWidget( 83 | axisSide: meta.axisSide, 84 | space: 4, 85 | child: Text(subjects[value.toInt()].code), 86 | ); 87 | }, 88 | reservedSize: 24, 89 | ), 90 | ), 91 | leftTitles: AxisTitles( 92 | sideTitles: SideTitles( 93 | showTitles: true, 94 | reservedSize: 25, 95 | getTitlesWidget: (value, meta) { 96 | return SideTitleWidget( 97 | axisSide: meta.axisSide, 98 | space: 4, 99 | child: Text( 100 | value.toInt().toString(), 101 | maxLines: 1, 102 | )); 103 | }, 104 | ), 105 | ), 106 | ), 107 | borderData: FlBorderData( 108 | show: false, 109 | ), 110 | barGroups: barData, 111 | gridData: const FlGridData(show: true, verticalInterval: 1), 112 | ))); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /lib/widgets/charts/linechart_monthly_average.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gemairo/hive/adapters.dart'; 3 | import 'package:fl_chart/fl_chart.dart'; 4 | import 'package:gemairo/hive/extentions.dart'; 5 | import 'package:intl/intl.dart'; 6 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 7 | 8 | class MonthlyLineChartGrades extends StatelessWidget { 9 | const MonthlyLineChartGrades( 10 | {super.key, required this.grades, this.showAverage = false}); 11 | 12 | final List grades; 13 | final bool showAverage; 14 | @override 15 | Widget build(BuildContext context) { 16 | List useablegrades = grades.numericalGrades.reversed.toList(); 17 | 18 | List months = useablegrades 19 | .map( 20 | (g) => DateTime.parse(DateFormat('yyyy-MM-01').format(g.addedDate))) 21 | .toList() 22 | .unique(); 23 | 24 | Map> gradesPerMonth = { 25 | for (var month in months) 26 | DateTimeRange( 27 | start: month, 28 | end: 29 | DateTime(month.year, month.month + 1, month.day)): useablegrades 30 | .where((g) => 31 | g.addedDate.isAfter(month) && 32 | g.addedDate 33 | .isBefore(DateTime(month.year, month.month + 1, month.day))) 34 | .toList() 35 | }; 36 | 37 | Map averagePerMonth = { 38 | for (var month in months) 39 | DateTimeRange( 40 | start: month, 41 | end: DateTime(month.year, month.month + 1, month.day)): 42 | useablegrades 43 | .where((g) => g.addedDate 44 | .isBefore(DateTime(month.year, month.month + 1, month.day))) 45 | .toList() 46 | .average 47 | }; 48 | 49 | final List gradeData = 50 | List.generate(gradesPerMonth.length, (index) { 51 | return FlSpot( 52 | index.toDouble(), gradesPerMonth.values.toList()[index].average); 53 | }); 54 | final List averageGradeData = showAverage 55 | ? List.generate(averagePerMonth.length, (index) { 56 | return FlSpot( 57 | index.toDouble(), averagePerMonth.values.toList()[index]); 58 | }) 59 | : []; 60 | return SizedBox( 61 | height: 250 - 56, 62 | child: LineChart( 63 | duration: const Duration(milliseconds: 150), 64 | curve: Curves.linear, 65 | LineChartData( 66 | borderData: FlBorderData( 67 | show: false, 68 | border: Border.all(color: const Color(0xff37434d)), 69 | ), 70 | maxY: 10, 71 | minY: 1, 72 | gridData: const FlGridData(drawVerticalLine: false), 73 | clipData: const FlClipData.none(), 74 | lineBarsData: [ 75 | LineChartBarData( 76 | spots: gradeData, 77 | isCurved: true, 78 | isStrokeCapRound: true, 79 | barWidth: 3, 80 | preventCurveOverShooting: true, 81 | color: Theme.of(context).colorScheme.primary, 82 | belowBarData: BarAreaData( 83 | show: true, 84 | color: 85 | Theme.of(context).colorScheme.primary.withOpacity(0.20), 86 | ), 87 | ), 88 | if (showAverage) 89 | LineChartBarData( 90 | spots: averageGradeData, 91 | isCurved: true, 92 | isStrokeCapRound: true, 93 | barWidth: 3, 94 | dotData: const FlDotData( 95 | show: false, 96 | ), 97 | color: Theme.of(context).colorScheme.inversePrimary, 98 | belowBarData: BarAreaData( 99 | show: true, 100 | color: Theme.of(context) 101 | .colorScheme 102 | .inversePrimary 103 | .withOpacity(0.20), 104 | ), 105 | ), 106 | ], 107 | lineTouchData: LineTouchData( 108 | touchTooltipData: LineTouchTooltipData( 109 | getTooltipItems: (value) { 110 | value.sort( 111 | (a, b) => a.bar.dotData.show 112 | .toString() 113 | .length 114 | .compareTo(b.bar.dotData.show.toString().length), 115 | ); 116 | return [ 117 | LineTooltipItem( 118 | textAlign: TextAlign.start, 119 | "${DateFormat.yMMMM('nl').format(gradesPerMonth.keys.toList()[value.first.x.toInt()].start.toLocal())} (${gradesPerMonth.values.toList()[value.first.x.toInt()].length})", 120 | TextStyle( 121 | color: 122 | Theme.of(context).colorScheme.onSurface, 123 | fontWeight: FontWeight.bold, 124 | height: 1.5), 125 | children: value 126 | .map((e) => TextSpan( 127 | text: 128 | "${!e.bar.dotData.show ? '\n${AppLocalizations.of(context)!.average}:' : '\n${AppLocalizations.of(context)!.grade}:'} ${e.y.displayNumber(decimalDigits: 2)}", 129 | style: const TextStyle( 130 | fontWeight: FontWeight.normal, 131 | height: 1.25))) 132 | .toList()), 133 | if (showAverage) 134 | const LineTooltipItem("", TextStyle(height: 0)) 135 | ]; 136 | }, 137 | fitInsideHorizontally: true, 138 | tooltipPadding: 139 | const EdgeInsets.symmetric(horizontal: 8, vertical: 4), 140 | tooltipRoundedRadius: 4, 141 | tooltipBorder: BorderSide( 142 | color: Theme.of(context).colorScheme.outline, width: 1), 143 | getTooltipColor: (line) => Theme.of(context) 144 | .colorScheme 145 | .surface 146 | .withOpacity(1.00))), 147 | extraLinesData: ExtraLinesData( 148 | horizontalLines: [ 149 | HorizontalLine( 150 | y: config.sufficientFrom, 151 | color: Theme.of(context).colorScheme.error, 152 | strokeWidth: 3, 153 | dashArray: [20, 10], 154 | ), 155 | ], 156 | ), 157 | titlesData: FlTitlesData( 158 | bottomTitles: 159 | const AxisTitles(sideTitles: SideTitles(showTitles: false)), 160 | leftTitles: AxisTitles( 161 | sideTitles: SideTitles( 162 | showTitles: true, 163 | reservedSize: 26, 164 | interval: 1, 165 | getTitlesWidget: (value, meta) => Text( 166 | !(value > 9 || value < 2) ? value.toInt().toString() : "", 167 | textAlign: TextAlign.center, 168 | ), 169 | ), 170 | ), 171 | rightTitles: 172 | const AxisTitles(sideTitles: SideTitles(showTitles: false)), 173 | topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), 174 | ), 175 | ), 176 | )); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /lib/widgets/global/skeletons.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gemairo/widgets/ads.dart'; 3 | 4 | class IntroductionSkeleton extends StatelessWidget { 5 | const IntroductionSkeleton({ 6 | super.key, 7 | required this.title, 8 | required this.subTitle, 9 | this.content, 10 | this.actions = const [], 11 | this.icon = const IconData(0xf201, fontFamily: "Gemairo"), 12 | }); 13 | 14 | final String title; 15 | final IconData icon; 16 | final String subTitle; 17 | final Widget? content; 18 | final List actions; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return Scaffold( 23 | appBar: AppBar(scrolledUnderElevation: 0), 24 | body: SafeArea( 25 | child: Padding( 26 | padding: const EdgeInsets.all(32.0), 27 | child: Column( 28 | crossAxisAlignment: CrossAxisAlignment.stretch, 29 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 30 | children: [ 31 | Padding( 32 | padding: 33 | EdgeInsets.symmetric(vertical: (content != null) ? 16 : 32), 34 | child: Column( 35 | crossAxisAlignment: CrossAxisAlignment.start, 36 | mainAxisSize: MainAxisSize.min, 37 | children: [ 38 | Padding( 39 | padding: const EdgeInsets.symmetric(vertical: 8.0), 40 | child: Icon( 41 | icon, 42 | size: 64, 43 | color: Theme.of(context).colorScheme.primary, 44 | ), 45 | ), 46 | Padding( 47 | padding: const EdgeInsets.only(bottom: 8), 48 | child: Text(title, 49 | style: Theme.of(context).textTheme.headlineMedium 50 | ?..copyWith(fontWeight: FontWeight.bold)), 51 | ), 52 | Text(subTitle, style: Theme.of(context).textTheme.bodyLarge), 53 | ], 54 | ), 55 | ), 56 | if (content != null) 57 | Expanded( 58 | child: SingleChildScrollView( 59 | child: content, 60 | )), 61 | Padding( 62 | padding: EdgeInsets.only(top: content != null ? 16.0 : 0), 63 | child: Wrap( 64 | alignment: actions.length > 1 65 | ? WrapAlignment.spaceBetween 66 | : WrapAlignment.end, 67 | children: actions, 68 | ), 69 | ) 70 | ], 71 | ), 72 | )), 73 | ); 74 | } 75 | } 76 | 77 | class ScaffoldSkeleton extends StatelessWidget { 78 | const ScaffoldSkeleton( 79 | {super.key, 80 | this.appBar, 81 | this.children = const [], 82 | this.onRefresh, 83 | this.backgroundColor, 84 | this.sliverAppBar, 85 | this.bottomNavigationBar, 86 | this.injectOverlap = false}); 87 | 88 | final SliverAppBar? sliverAppBar; 89 | final PreferredSizeWidget? appBar; 90 | final List children; 91 | final Future Function()? onRefresh; 92 | final Color? backgroundColor; 93 | // final bool showAds = false; 94 | final Widget? bottomNavigationBar; 95 | final bool injectOverlap; 96 | 97 | @override 98 | Widget build(BuildContext context) { 99 | return Scaffold( 100 | primary: appBar != null, 101 | appBar: appBar, 102 | bottomNavigationBar: bottomNavigationBar, 103 | backgroundColor: backgroundColor, 104 | body: RefreshIndicator.adaptive( 105 | edgeOffset: (injectOverlap 106 | ? NestedScrollView.sliverOverlapAbsorberHandleFor(context) 107 | .layoutExtent 108 | : 0) ?? 109 | 0, 110 | onRefresh: onRefresh ?? () => Future(() {}), 111 | notificationPredicate: (notificationPredicate) => 112 | onRefresh != null ? true : false, 113 | child: CustomScrollView( 114 | slivers: [ 115 | if (injectOverlap) 116 | SliverOverlapInjector( 117 | handle: 118 | NestedScrollView.sliverOverlapAbsorberHandleFor(context), 119 | ), 120 | if (sliverAppBar != null) 121 | DefaultTextStyle( 122 | maxLines: 2, 123 | style: const TextStyle(overflow: TextOverflow.ellipsis), 124 | child: sliverAppBar!, 125 | ), 126 | children.isNotEmpty 127 | ? SliverList.builder( 128 | addAutomaticKeepAlives: true, 129 | itemCount: children.length + 1, 130 | itemBuilder: (BuildContext context, int index) => index == 131 | children.length 132 | ? SizedBox( 133 | height: 134 | MediaQuery.of(context).viewInsets.bottom + 16, 135 | ) 136 | : children[index], 137 | ) 138 | : SliverFillRemaining( 139 | hasScrollBody: false, 140 | child: Center( 141 | child: Icon( 142 | const IconData(0xf201, fontFamily: "Gemairo"), 143 | size: 64 * .8, 144 | color: Theme.of(context) 145 | .colorScheme 146 | .surfaceContainerHighest, 147 | ), 148 | ), 149 | ), 150 | ], 151 | ), 152 | ), 153 | ); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /lib/widgets/ratelimit.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:gemairo/widgets/ads.dart'; 5 | import 'package:google_mobile_ads/google_mobile_ads.dart'; 6 | 7 | class RateLimitOverlay { 8 | BuildContext _context; 9 | static bool isActive = false; 10 | 11 | void hide() { 12 | if (isActive) Navigator.of(_context).pop(); 13 | isActive = false; 14 | } 15 | 16 | void show(Duration? duration) { 17 | if (!isActive) { 18 | showDialog( 19 | context: _context, 20 | barrierDismissible: false, 21 | builder: (context) => _FullScreenLoader( 22 | duration: duration, 23 | ), 24 | ); 25 | } 26 | isActive = true; 27 | } 28 | 29 | Future during(Duration duration) async { 30 | show(duration); 31 | await Future.delayed(duration); 32 | hide(); 33 | } 34 | 35 | RateLimitOverlay._create(this._context); 36 | 37 | factory RateLimitOverlay.of(BuildContext context) { 38 | return RateLimitOverlay._create(context); 39 | } 40 | } 41 | 42 | class _FullScreenLoader extends StatefulWidget { 43 | final Duration? duration; 44 | 45 | const _FullScreenLoader({required this.duration}); 46 | 47 | @override 48 | _FullScreenLoaderState createState() => _FullScreenLoaderState(); 49 | } 50 | 51 | class _FullScreenLoaderState extends State<_FullScreenLoader> { 52 | Duration? _countdown; 53 | Timer? _timer; 54 | 55 | @override 56 | void initState() { 57 | super.initState(); 58 | 59 | if (widget.duration is Duration) { 60 | _countdown = widget.duration; 61 | startCountdown(); 62 | } 63 | } 64 | 65 | @override 66 | void dispose() { 67 | _timer?.cancel(); 68 | super.dispose(); 69 | } 70 | 71 | void startCountdown() { 72 | _timer = Timer.periodic(const Duration(seconds: 1), (timer) { 73 | if (mounted) { 74 | setState(() { 75 | if (_countdown!.inSeconds > 0) { 76 | _countdown = _countdown! - const Duration(seconds: 1); 77 | } else { 78 | timer.cancel(); 79 | } 80 | }); 81 | } 82 | }); 83 | } 84 | 85 | @override 86 | Widget build(BuildContext context) { 87 | return Scaffold( 88 | backgroundColor: const Color.fromRGBO(0, 0, 0, 0.8), 89 | body: SizedBox.expand( 90 | child: Column( 91 | crossAxisAlignment: CrossAxisAlignment.center, 92 | mainAxisAlignment: MainAxisAlignment.center, 93 | mainAxisSize: MainAxisSize.max, 94 | children: [ 95 | Expanded( 96 | child: Column( 97 | crossAxisAlignment: CrossAxisAlignment.center, 98 | mainAxisAlignment: MainAxisAlignment.center, 99 | mainAxisSize: MainAxisSize.max, 100 | children: [ 101 | Padding( 102 | padding: const EdgeInsets.only(bottom: 8), 103 | child: Text( 104 | "Even wachten...", 105 | style: Theme.of(context) 106 | .textTheme 107 | .headlineMedium! 108 | .copyWith(color: Colors.white), 109 | textAlign: TextAlign.center, 110 | ), 111 | ), 112 | Text( 113 | "Magister opvraag limiet bereikt. Laat de app open.", 114 | style: Theme.of(context) 115 | .textTheme 116 | .bodyLarge! 117 | .copyWith(color: Colors.white), 118 | textAlign: TextAlign.center, 119 | ), 120 | const SizedBox(height: 20), 121 | if (_countdown is Duration) 122 | TweenAnimationBuilder( 123 | tween: Tween(begin: 1, end: 0), 124 | duration: widget.duration!, 125 | builder: (context, value, _) => 126 | CircularProgressIndicator(value: value), 127 | ), 128 | if (_countdown is! Duration) 129 | const CircularProgressIndicator(), 130 | const SizedBox(height: 20), 131 | if (_countdown is Duration) 132 | Text( 133 | "${_countdown!.inSeconds} seconden resterend", 134 | style: Theme.of(context) 135 | .textTheme 136 | .bodyLarge! 137 | .copyWith(color: Colors.white), 138 | textAlign: TextAlign.center, 139 | ), 140 | ]), 141 | ), 142 | const Expanded( 143 | child: Padding( 144 | padding: EdgeInsets.only(left: 15, right: 15, bottom: 30), 145 | child: Advertisement( 146 | size: AdSize.mediumRectangle, 147 | type: 'leaderboard', 148 | ), 149 | ), 150 | ), 151 | ], 152 | ), 153 | ), 154 | ); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: gemairo 2 | description: Krijg beter inzicht in je cijfers 3 | publish_to: "none" 4 | 5 | # The following defines the version and build number for your application. 6 | # A version number is three numbers separated by dots, like 1.2.43 7 | # followed by an optional build number separated by a +. 8 | # Both the version and the builder number may be overridden in flutter 9 | # build by specifying --build-name and --build-number, respectively. 10 | # In Android, build-name is used as versionName while build-number used as versionCode. 11 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 12 | # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. 13 | # Read more about iOS versioning at 14 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 15 | # In Windows, build-name is used as the major, minor, and patch parts 16 | # of the product and file versions while build-number is used as the build suffix. 17 | version: 2.0.8+20000 18 | 19 | environment: 20 | sdk: ">=3.0.0 <4.0.0" 21 | 22 | dependencies: 23 | flutter: 24 | sdk: flutter 25 | flutter_localizations: 26 | sdk: flutter 27 | 28 | cupertino_icons: ^1.0.2 29 | pointycastle: ^3.6.2 30 | url_launcher: ^6.3.1 31 | dio: ^5.1.1 32 | hive: ^2.2.3 33 | hive_flutter: ^1.1.0 34 | fl_chart: ^0.67.0 35 | dynamic_color: ^1.6.2 36 | background_fetch: ^1.1.5 37 | flutter_local_notifications: ^18.0.1 38 | webview_flutter: ^4.8.0 39 | fluttericon: ^2.0.0 40 | intl: ^0.19.0 41 | file_picker: ^8.1.7 42 | permission_handler: ^11.3.1 43 | collection: ^1.18.0 44 | carousel_slider: ^5.0.0 45 | provider: ^6.0.5 46 | flutter_staggered_grid_view: ^0.7.0 47 | # qr_flutter: ^4.0.0 48 | # mobile_scanner: ^3.2.0 49 | image_picker: ^1.1.2 50 | cross_file: ^0.3.3+4 51 | package_info_plus: ^8.1.2 52 | google_mobile_ads: ^5.2.0 53 | gma_mediation_applovin: ^1.2.0 54 | app_tracking_transparency: ^2.0.4 55 | cr_file_saver: ^0.0.2+1 56 | expandable_page_view: ^1.0.17 57 | firebase_core: ^3.9.0 58 | firebase_remote_config: ^5.2.0 59 | firebase_analytics: ^11.3.6 60 | firebase_messaging: ^15.1.6 61 | app_links: ^6.3.3 62 | desktop_webview_window: ^0.2.3 63 | in_app_review: ^2.0.8 64 | saaf: 65 | git: 66 | url: https://github.com/gemairo/gemairo.dart.git 67 | 68 | dev_dependencies: 69 | flutter_test: 70 | sdk: flutter 71 | flutter_lints: ^2.0.0 72 | hive_generator: ^2.0.0 73 | build_runner: ^2.3.3 74 | 75 | flutter: 76 | uses-material-design: true 77 | generate: true 78 | fonts: 79 | - family: Gemairo 80 | fonts: 81 | - asset: assets/font_package/fonts/Gemairo.ttf 82 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | //TODO: Tests 2 | --------------------------------------------------------------------------------