forked from firka/firka
Compare commits
126 Commits
backup/dev
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 3fb0fd55de | |||
| 4c06daf02f | |||
| 3d07b2b2ce | |||
| 9a45c5c456 | |||
|
|
2b9ac14d6e | ||
|
|
eea10a4878 | ||
|
|
1ef6ae6e87 | ||
|
|
141431f378 | ||
|
|
f0114e3976 | ||
|
|
414a1ad254 | ||
|
|
d858c38bb1 | ||
|
|
87e3331428 | ||
|
|
1d912abc7d | ||
| 3a2cdfce05 | |||
| c5ba318c3b | |||
| fe9a2b36f1 | |||
| 80b05c1500 | |||
| 2ed6a04bf5 | |||
| 2d5ff89cad | |||
| 16b11564cc | |||
| 1ca975f0bd | |||
| 7985a11f99 | |||
| 2cf95375cb | |||
| 163b3a3135 | |||
| e839b8fa70 | |||
| c6378c9d46 | |||
| 51456b3b6e | |||
|
|
28f8f1d6f8 | ||
| d51c9d6411 | |||
| 4ef2f479d6 | |||
| d81d261c52 | |||
| 5baec2e65f | |||
| 48ea97ecef | |||
|
|
1154997531 | ||
|
|
dde05ce1db | ||
| 99ab6c07f7 | |||
|
|
40c6b83627 | ||
|
|
f4f6ddac72 | ||
|
|
938f2a56c6 | ||
|
|
e1b571afa0 | ||
|
|
729c281f1e | ||
|
|
a61a41e677 | ||
|
|
e8f1f18725 | ||
|
|
613b66bc22 | ||
|
|
1807d96895 | ||
|
|
e06d2168c0 | ||
|
|
91603923bc | ||
|
|
67e9ded2ed | ||
|
|
e9aaa26e26 | ||
|
|
56f15a681c | ||
|
|
178ebf7271 | ||
|
|
70c1397262 | ||
|
|
a334901f84 | ||
|
|
ddba6ad888 | ||
|
|
780aaee1dd | ||
|
|
09c408a6be | ||
|
|
7847878551 | ||
|
|
09ef386eab | ||
|
|
c19dd2f7eb | ||
|
|
39d4f49e99 | ||
|
|
bbdbf406d4 | ||
|
|
b17509f9db | ||
|
|
9ec164daac | ||
|
|
08f691d559 | ||
|
|
ad9e2c6676 | ||
|
|
55dbf12a2f | ||
|
|
4fc13efc52 | ||
|
|
5f16128bb0 | ||
|
|
4c8d73aa06 | ||
|
|
e4629d8489 | ||
|
|
f806dd8143 | ||
|
|
f653dc9fb4 | ||
|
|
06249bfc3d | ||
|
|
607f49bafc | ||
|
|
251e8de446 | ||
|
|
21845f89a8 | ||
|
|
25b1d06c06 | ||
|
|
dd20aa6138 | ||
|
|
9aa84c6fa1 | ||
|
|
6c75acad66 | ||
|
|
6311cafd51 | ||
|
|
1de32a35a4 | ||
|
|
f8a69a7561 | ||
|
|
f73c1127e7 | ||
|
|
a369e4ab14 | ||
|
|
8b04b77327 | ||
|
|
d56db4fe3a | ||
|
|
e7c0a95638 | ||
|
|
fa66d9af14 | ||
|
|
90c334d859 | ||
|
|
cc9ebcf6b0 | ||
|
|
ac914aa02e | ||
|
|
061f803f93 | ||
|
|
57c5e17b1c | ||
|
|
fa6f5390b1 | ||
|
|
21ff8f082f | ||
|
|
3b09d072f0 | ||
|
|
84f8e75694 | ||
|
|
1a05cce49b | ||
|
|
e732168a1b | ||
|
|
68035140b9 | ||
|
|
e004b7ee35 | ||
|
|
34718ed8ae | ||
|
|
03a5b5e767 | ||
|
|
d30dc67626 | ||
|
|
d0a517ae1e | ||
| 32a6452c1b | |||
| 579bf6bcad | |||
| 4ecf0d1a3f | |||
| 0675a5109a | |||
| bfb06d3e5f | |||
| 71b4b6aec7 | |||
| be6d28cfe3 | |||
| 8cfca13d1a | |||
| 632bb81408 | |||
|
|
f953dbd49f | ||
|
|
01e7e559ba | ||
| b6bfef7715 | |||
| bf75f72bcd | |||
|
|
b0cb020d76 | ||
|
|
4239ffa00c | ||
|
|
427b6f8086 | ||
|
|
fbd2351073 | ||
|
|
150e90d19b | ||
|
|
a6cf8b13c6 | ||
|
|
d8ae8471ab |
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -1,6 +1,6 @@
|
|||||||
[submodule "firka/lib/l10n"]
|
[submodule "firka/lib/l10n"]
|
||||||
path = firka/lib/l10n
|
path = firka/lib/l10n
|
||||||
url = https://github.com/QwIT-Development/firka-localization
|
url = https://git.firka.app/firka/firka-localization
|
||||||
[submodule "firka_wear/lib/l10n"]
|
[submodule "firka_wear/lib/l10n"]
|
||||||
path = firka_wear/lib/l10n
|
path = firka_wear/lib/l10n
|
||||||
url = https://github.com/qwit-development/firka-localization
|
url = https://git.firka.app/firka/firka-localization
|
||||||
|
|||||||
@@ -1,35 +1,19 @@
|
|||||||
# Flutter telepítése
|
# Flutter telepítése
|
||||||
|
|
||||||
A firka androidra való lebuildeléséhez kötelező a saját Flutter fork használata, illetve minden más fajta --release buildhez is.
|
|
||||||
|
|
||||||
A Flutter telepítéséhez a dokumentáció [itt](https://docs.flutter.dev/get-started/install) található.
|
A Flutter telepítéséhez a dokumentáció [itt](https://docs.flutter.dev/get-started/install) található.
|
||||||
|
A projekt jelenleg a 3.41.2-es Flutter SDK-t használja.
|
||||||
A Flutter zip letöltése helyett a custom engine-t cloneold le ([https://git.firka.app/firka/flutter/](https://git.firka.app/firka/flutter/))
|
|
||||||
|
|
||||||
# Brotli
|
|
||||||
|
|
||||||
A firka brotlival compresseli a libflutter-t buildelés közben ezért szükséges a projekt
|
|
||||||
buildeléséhez hogy a brotli a PATH-ben legyen
|
|
||||||
|
|
||||||
## Windows
|
|
||||||
- Töltsd le a `brotli-x64-windows-static.zip`-et a [google/brotli github repoból](https://github.com/google/brotli/releases/latest)
|
|
||||||
- Csomagold ki valahol (pl. C:\Users\\<username>\dev\brotli)
|
|
||||||
- Add hozzá a mappát ahova kicsomagoltad (C:\Users\\<username>\dev\brotli) a PATH-hez
|
|
||||||
- Ne felejtsd el újraindítani az IDE-det illetve parancssorodat utánna hogy frissüljön a PATH
|
|
||||||
|
|
||||||
## Linux/MacOS
|
|
||||||
Telepítsd fel a brotli packaget a distro-d package managerével
|
|
||||||
|
|
||||||
# Keystore
|
# Keystore
|
||||||
|
|
||||||
[Secrets dokumentáció](secrets/README.md)
|
[Secrets dokumentáció](secrets/README.md)
|
||||||
|
|
||||||
# Flutter l10n
|
# Fileok generálása
|
||||||
|
|
||||||
Flutter l10n fileok generálása
|
Flutter l10n és egyéb fileok generálása
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
Flutter gen-l10n --template-arb-file app_hu.arb
|
$ cd firka # vagy firka_wear
|
||||||
|
$ dart run scripts/codegen.dart
|
||||||
```
|
```
|
||||||
|
|
||||||
# Android debug build
|
# Android debug build
|
||||||
@@ -42,20 +26,10 @@ $ Flutter build apk --debug --target-platform android-arm,android-arm64,android-
|
|||||||
|
|
||||||
# Android release build
|
# Android release build
|
||||||
|
|
||||||
A release buildhez közelező egy keystore használata, illetve a saját Flutter engineünk használata.
|
A release buildhez közelező egy keystore használata.
|
||||||
|
|
||||||
## Custom Flutter engine setupolása
|
## Release appbundle buildelése (firka és firka_wear)
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ git clone https://git.firka.app/firka/flutter
|
$ ./build.sh
|
||||||
$ cd flutter
|
|
||||||
$ . dev/tools/envsetup.sh
|
|
||||||
$ gclient sync -D
|
|
||||||
$ ./dev/tools/build_release.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## Release apk buildelése
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ ./tools/linux/build_apk.sh main
|
|
||||||
```
|
```
|
||||||
@@ -1,41 +1,24 @@
|
|||||||
# Installing flutter
|
# Installing Flutter
|
||||||
|
|
||||||
To build firka you will have to use our custom Flutter fork,
|
Flutter installation documentation can be found [here](https://docs.flutter.dev/get-started/install).
|
||||||
and to make a release build you will have to use our custom
|
The project currently uses Flutter SDK 3.41.2.
|
||||||
flutter engine.
|
|
||||||
The documentation for installing flutter can be found [here](https://docs.flutter.dev/get-started/install).
|
|
||||||
|
|
||||||
Instead of downloading the regular flutter zip, clone it from ([https://git.firka.app/firka/flutter/](https://git.firka.app/firka/flutter/)).
|
|
||||||
|
|
||||||
# Brotli
|
|
||||||
|
|
||||||
Firka uses brotli to compress libflutter during the build process to make the app smaller,
|
|
||||||
so building Firka requires you to have brotli in your path
|
|
||||||
|
|
||||||
## Windows
|
|
||||||
- Download `brotli-x64-windows-static.zip` from [google/brotli](https://github.com/google/brotli/releases/latest)
|
|
||||||
- Extract it to somewhere like C:\Users\\<username>\dev\brotli
|
|
||||||
- Add the directory (ex. C:\Users\\<username>\dev\brotli) to your PATH
|
|
||||||
- Don't forget to restart your IDE or terminal sessions for the PATH variable to update
|
|
||||||
|
|
||||||
## Linux/MacOS
|
|
||||||
Install it using your distro's package manager
|
|
||||||
|
|
||||||
# Keystore
|
# Keystore
|
||||||
|
|
||||||
[Secrets docs](secrets/README_en.md)
|
[Secrets documentation](secrets/README.md)
|
||||||
|
|
||||||
# Flutter l10n
|
# Generating files
|
||||||
|
|
||||||
Generating flutter l10n files
|
Generating Flutter l10n and other files
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
flutter gen-l10n --template-arb-file app_hu.arb
|
$ cd firka # or firka_wear
|
||||||
|
$ dart run scripts/codegen.dart
|
||||||
```
|
```
|
||||||
|
|
||||||
# Android debug build
|
# Android debug build
|
||||||
|
|
||||||
The dev build doesn't require using a custom keystore
|
The dev build does not require using a keystore
|
||||||
```shell
|
```shell
|
||||||
$ cd firka
|
$ cd firka
|
||||||
$ flutter build apk --debug --target-platform android-arm,android-arm64,android-x64
|
$ flutter build apk --debug --target-platform android-arm,android-arm64,android-x64
|
||||||
@@ -43,20 +26,10 @@ $ flutter build apk --debug --target-platform android-arm,android-arm64,android-
|
|||||||
|
|
||||||
# Android release build
|
# Android release build
|
||||||
|
|
||||||
The release build requires using a custom keystore and our custom flutter fork
|
The release build requires using a keystore.
|
||||||
|
|
||||||
## Setting up our flutter engine fork
|
## Building the release appbundle (firka and firka_wear)
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ git clone https://git.firka.app/firka/flutter
|
$ ./build.sh
|
||||||
$ cd flutter
|
|
||||||
$ . dev/tools/envsetup.sh
|
|
||||||
$ gclient sync -D
|
|
||||||
$ ./dev/tools/build_release.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## Building the release apk
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ ./tools/linux/build_apk.sh main
|
|
||||||
```
|
```
|
||||||
193
Jenkinsfile
vendored
193
Jenkinsfile
vendored
@@ -1,167 +1,66 @@
|
|||||||
pipeline {
|
pipeline {
|
||||||
agent {
|
agent any
|
||||||
label 'ubuntu'
|
|
||||||
}
|
|
||||||
environment {
|
environment {
|
||||||
PATH = "/home/jenkins/development/flutter/bin:${env.PATH}"
|
FLUTTER_ROOT = "/opt/flutter"
|
||||||
|
FLUTTER = "/opt/flutter/bin/flutter"
|
||||||
|
DART = "/opt/flutter/bin/dart"
|
||||||
|
ANDROID_SDK_ROOT = "/opt/android-sdk"
|
||||||
|
ANDROID_HOME = "/opt/android-sdk"
|
||||||
}
|
}
|
||||||
stages {
|
stages {
|
||||||
stage('Pre-build Cleanup') {
|
stage('Clone Submodules') {
|
||||||
steps {
|
steps {
|
||||||
script {
|
sh 'git submodule update --init --recursive'
|
||||||
sh '''#!/bin/sh
|
|
||||||
set -x
|
|
||||||
fusermount -u secrets || true
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
stage('Dependencies') {
|
||||||
stage('Decrypt main keys') {
|
|
||||||
when {
|
|
||||||
branch 'main'
|
|
||||||
}
|
|
||||||
steps {
|
steps {
|
||||||
script {
|
sh '''
|
||||||
def userInput = input(
|
cd firka
|
||||||
id: 'signaturePassword',
|
$FLUTTER pub get
|
||||||
message: 'Please enter the signing key password:',
|
|
||||||
parameters: [
|
|
||||||
password(
|
|
||||||
defaultValue: '',
|
|
||||||
description: 'Enter the signing key password',
|
|
||||||
name: 'password'
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
env.PASSWORD = userInput.toString()
|
|
||||||
}
|
|
||||||
sh '''#!/bin/sh
|
|
||||||
echo \$PASSWORD | gocryptfs $HOME/android_secrets secrets/ -nonempty
|
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
stage('Setup') {
|
||||||
stage('Clone submodules') {
|
steps {
|
||||||
|
sh '''
|
||||||
|
cp -r /opt/secrets ./
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Codegen') {
|
||||||
steps {
|
steps {
|
||||||
script {
|
sh '''
|
||||||
sh 'git submodule update --init --recursive'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage('Build firka') {
|
|
||||||
steps {
|
|
||||||
sh 'bash -c "./tools/linux/build_apk.sh ' + env.BRANCH_NAME + '"'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage('Rename Release APKs') {
|
|
||||||
when {
|
|
||||||
branch 'main'
|
|
||||||
}
|
|
||||||
steps {
|
|
||||||
script {
|
|
||||||
sh '''#!/bin/sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
APK_DIR="firka/build/app/outputs/flutter-apk"
|
|
||||||
|
|
||||||
# Find all release APKs and rename them
|
|
||||||
for apk_file in $APK_DIR/app-*-release.apk; do
|
|
||||||
if [ -f "$apk_file" ]; then
|
|
||||||
# Extract ABI from filename (e.g., app-arm64-v8a-release.apk -> arm64-v8a)
|
|
||||||
basename_file=$(basename "$apk_file")
|
|
||||||
abi=$(echo "$basename_file" | sed 's/app-//; s/-release.apk//')
|
|
||||||
|
|
||||||
# Create new filename
|
|
||||||
new_name="app.firka.naplo_${abi}.apk"
|
|
||||||
new_path="$APK_DIR/$new_name"
|
|
||||||
|
|
||||||
echo "Renaming $apk_file to $new_path"
|
|
||||||
mv "$apk_file" "$new_path"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
ls -la $APK_DIR/app.firka.naplo_*.apk || echo "APK files not found"
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage('Calculate Version Code') {
|
|
||||||
steps {
|
|
||||||
script {
|
|
||||||
sh '''#!/bin/sh
|
|
||||||
set -e
|
|
||||||
cd firka
|
cd firka
|
||||||
|
PATH="/opt/flutter/bin:$PATH" $DART run scripts/codegen.dart
|
||||||
# Calculate version code based on git commits (same logic as build script)
|
'''
|
||||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
|
||||||
BASE_BUILD_NUMBER=$((1300 + COMMIT_COUNT))
|
|
||||||
|
|
||||||
if [ "$BRANCH_NAME" = "main" ]; then
|
|
||||||
# For main branch, highest version code is BASE + 3000 (x64 build)
|
|
||||||
VERSION_CODE=$((BASE_BUILD_NUMBER + 3000))
|
|
||||||
else
|
|
||||||
# For debug builds, version code is BASE + 0
|
|
||||||
VERSION_CODE=$BASE_BUILD_NUMBER
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Calculated version code: $VERSION_CODE"
|
|
||||||
echo "$VERSION_CODE" > ../version_code.txt
|
|
||||||
'''
|
|
||||||
|
|
||||||
env.VERSION_CODE = readFile('version_code.txt').trim()
|
|
||||||
echo "Setting VERSION_CODE environment variable to: ${env.VERSION_CODE}"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
stage('Build') {
|
||||||
stage('Publish debug artifacts') {
|
steps {
|
||||||
when {
|
sh '''
|
||||||
not {
|
cd firka
|
||||||
branch 'main'
|
echo "--- Checking secrets path ---"
|
||||||
}
|
ls android/app/
|
||||||
}
|
ls android/app/../../../ || echo "no parent"
|
||||||
|
ls android/app/../../../secrets/ || echo "secrets not found at expected path"
|
||||||
|
$FLUTTER config --android-sdk /opt/android-sdk
|
||||||
|
$FLUTTER build apk --release
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Archive') {
|
||||||
steps {
|
steps {
|
||||||
archiveArtifacts artifacts: 'firka/build/app/outputs/flutter-apk/app-debug.apk', fingerprint: true
|
archiveArtifacts(
|
||||||
}
|
artifacts: 'firka/build/app/outputs/flutter-apk/app-debug.apk,firka/build/app/outputs/flutter-apk/app-release.apk',
|
||||||
}
|
fingerprint: true
|
||||||
|
)
|
||||||
stage('Publish release AABs artifacts') {
|
|
||||||
when {
|
|
||||||
branch 'main'
|
|
||||||
}
|
|
||||||
steps {
|
|
||||||
archiveArtifacts artifacts: 'firka/build/app/outputs/bundle/release/*.aab', fingerprint: true
|
|
||||||
sh 'rm firka/build/app/outputs/bundle/release/*.aab'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage('Publish release APKs artifacts') {
|
|
||||||
when {
|
|
||||||
branch 'main'
|
|
||||||
}
|
|
||||||
steps {
|
|
||||||
archiveArtifacts artifacts: 'firka/build/app/outputs/flutter-apk/app.firka.naplo_*.apk', fingerprint: true
|
|
||||||
sh 'rm firka/build/app/outputs/flutter-apk/app.firka.naplo_*.apk'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage('Post Cleanup') {
|
|
||||||
steps {
|
|
||||||
script {
|
|
||||||
sh '''
|
|
||||||
rm firka/build/app/outputs/bundle/release/*.aab || true
|
|
||||||
rm firka/build/app/outputs/flutter-apk/app.firka.naplo_*.apk || true
|
|
||||||
rm firka/build/app/outputs/flutter-apk/app-debug.apk || true
|
|
||||||
rm version_code.txt || true
|
|
||||||
git checkout -- firka/pubspec.yaml || true
|
|
||||||
git checkout -- firka/lib/helpers/firka_bundle.dart || true
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
deleteDir()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,6 @@ import java.util.Properties
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("kotlin-android")
|
|
||||||
id("org.jetbrains.kotlin.plugin.compose")
|
id("org.jetbrains.kotlin.plugin.compose")
|
||||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
id("dev.flutter.flutter-gradle-plugin")
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
@@ -23,10 +22,6 @@ android {
|
|||||||
compose = true
|
compose = true
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "app.firka.naplo"
|
applicationId = "app.firka.naplo"
|
||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
@@ -74,6 +69,13 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("androidx.glance:glance-appwidget:1.1.1")
|
implementation("androidx.glance:glance-appwidget:1.1.1")
|
||||||
implementation("com.google.android.gms:play-services-wearable:18.1.0")
|
implementation("com.google.android.gms:play-services-wearable:18.1.0")
|
||||||
|
|||||||
Submodule firka/android/app/src/main/java/org/brotli deleted from da8f329432
@@ -16,6 +16,7 @@ import androidx.glance.layout.padding
|
|||||||
import androidx.glance.layout.width
|
import androidx.glance.layout.width
|
||||||
import androidx.glance.text.FontWeight
|
import androidx.glance.text.FontWeight
|
||||||
import androidx.glance.text.Text
|
import androidx.glance.text.Text
|
||||||
|
import androidx.glance.text.TextAlign
|
||||||
import androidx.glance.text.TextStyle
|
import androidx.glance.text.TextStyle
|
||||||
import app.firka.naplo.model.Colors
|
import app.firka.naplo.model.Colors
|
||||||
import app.firka.naplo.glance.WidgetLesson
|
import app.firka.naplo.glance.WidgetLesson
|
||||||
@@ -31,6 +32,7 @@ fun LessonCard(
|
|||||||
colors: Colors,
|
colors: Colors,
|
||||||
modifier: GlanceModifier = GlanceModifier,
|
modifier: GlanceModifier = GlanceModifier,
|
||||||
roomBadgeWidthDp: Float = 48f,
|
roomBadgeWidthDp: Float = 48f,
|
||||||
|
subjectColumnWidthDp: Float = 226f,
|
||||||
) {
|
) {
|
||||||
Box(modifier =
|
Box(modifier =
|
||||||
modifier
|
modifier
|
||||||
@@ -49,17 +51,21 @@ fun LessonCard(
|
|||||||
|
|
||||||
|
|
||||||
Box(modifier = GlanceModifier.padding(12.dp)) {
|
Box(modifier = GlanceModifier.padding(12.dp)) {
|
||||||
Row {
|
Row(modifier = GlanceModifier.fillMaxWidth()) {
|
||||||
val badgeStyle = TextStyle(
|
val badgeStyle = TextStyle(
|
||||||
color = ColorProvider(colors.textSecondary, colors.textSecondary),
|
color = ColorProvider(colors.textSecondary, colors.textSecondary),
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
val badgePadding = GlanceModifier.padding(8.dp, 4.dp)
|
val badgePadding = GlanceModifier.padding(8.dp, 4.dp)
|
||||||
val lessonNumberBadgeModifier = GlanceModifier.cornerRadius(16.dp).width(24.dp)
|
val lessonNumberBadgeModifier = GlanceModifier.cornerRadius(16.dp).width(24.dp)
|
||||||
val roomBadgeModifier = GlanceModifier.cornerRadius(16.dp).width(roomBadgeWidthDp.dp)
|
val roomBadgeModifier = GlanceModifier.cornerRadius(16.dp).width(roomBadgeWidthDp.dp)
|
||||||
|
|
||||||
Row(modifier = GlanceModifier.width(226.dp), verticalAlignment = Alignment.CenterVertically) {
|
Row(
|
||||||
|
modifier = GlanceModifier.width(subjectColumnWidthDp.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
if (lesson.lessonNumber != null) {
|
if (lesson.lessonNumber != null) {
|
||||||
Box(
|
Box(
|
||||||
modifier = lessonNumberBadgeModifier.background(bgColor),
|
modifier = lessonNumberBadgeModifier.background(bgColor),
|
||||||
@@ -79,12 +85,14 @@ fun LessonCard(
|
|||||||
}
|
}
|
||||||
// TODO: Add subject icons
|
// TODO: Add subject icons
|
||||||
Text(
|
Text(
|
||||||
lesson.name,
|
text = lesson.name,
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
color = ColorProvider(colors.textPrimary, colors.textPrimary),
|
color = ColorProvider(colors.textPrimary, colors.textPrimary),
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
),
|
),
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = GlanceModifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,15 +106,18 @@ fun LessonCard(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
Spacer(modifier = GlanceModifier.width(8.dp))
|
Spacer(modifier = GlanceModifier.width(8.dp))
|
||||||
val roomName = (lesson.roomName ?: "N/A").take(5)
|
val roomName = lesson.roomName ?: "N/A"
|
||||||
Box(
|
Box(
|
||||||
modifier = roomBadgeModifier.background(colors.a15p),
|
modifier = roomBadgeModifier.background(colors.a15p),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
roomName,
|
text = roomName,
|
||||||
style = badgeStyle,
|
style = badgeStyle,
|
||||||
modifier = GlanceModifier.padding(4.dp, 4.dp),
|
maxLines = 1,
|
||||||
|
modifier = GlanceModifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(4.dp, 4.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,8 +116,10 @@ class TimetableWidget : GlanceAppWidget() {
|
|||||||
val displayLessons = data.lessons.take(maxLessons)
|
val displayLessons = data.lessons.take(maxLessons)
|
||||||
val lessonChunks = displayLessons.chunked(2)
|
val lessonChunks = displayLessons.chunked(2)
|
||||||
val showDate = maxLessons > 1
|
val showDate = maxLessons > 1
|
||||||
val maxRoomNameLen = displayLessons.maxOfOrNull { (it.roomName ?: "N/A").take(5).length } ?: 0
|
val roomBadgeWidthDp = 48f
|
||||||
val roomBadgeWidthDp = if (maxRoomNameLen <= 3) 28f else 48f
|
val minWidthForTimeAndChipDp = 8f + 40f + roomBadgeWidthDp
|
||||||
|
val subjectColumnWidthDp = (size.width.value - 2 * paddingDp - 32f - minWidthForTimeAndChipDp)
|
||||||
|
.coerceIn(80f, 226f)
|
||||||
val dateSectionHeight = if (showDate) headerHeightDp + spacerDp else 0f
|
val dateSectionHeight = if (showDate) headerHeightDp + spacerDp else 0f
|
||||||
val lessonListHeight = when (val n = displayLessons.size) {
|
val lessonListHeight = when (val n = displayLessons.size) {
|
||||||
0 -> 0f
|
0 -> 0f
|
||||||
@@ -148,7 +150,12 @@ class TimetableWidget : GlanceAppWidget() {
|
|||||||
for (chunk in lessonChunks) {
|
for (chunk in lessonChunks) {
|
||||||
Column {
|
Column {
|
||||||
for (lesson in chunk) {
|
for (lesson in chunk) {
|
||||||
LessonCard(lesson, data.colors, roomBadgeWidthDp = roomBadgeWidthDp)
|
LessonCard(
|
||||||
|
lesson,
|
||||||
|
data.colors,
|
||||||
|
roomBadgeWidthDp = roomBadgeWidthDp,
|
||||||
|
subjectColumnWidthDp = subjectColumnWidthDp,
|
||||||
|
)
|
||||||
Spacer(modifier = GlanceModifier.height(spacerDp.dp))
|
Spacer(modifier = GlanceModifier.height(spacerDp.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ subprojects {
|
|||||||
if (plugins.hasPlugin("com.android.application") || plugins.hasPlugin("com.android.library")) {
|
if (plugins.hasPlugin("com.android.application") || plugins.hasPlugin("com.android.library")) {
|
||||||
val androidExtension = extensions.getByName("android") as BaseExtension
|
val androidExtension = extensions.getByName("android") as BaseExtension
|
||||||
androidExtension.apply {
|
androidExtension.apply {
|
||||||
compileSdkVersion(36)
|
compileSdkVersion(37)
|
||||||
buildToolsVersion = "36.1.0"
|
buildToolsVersion = "36.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,3 +11,7 @@ org.gradle.parallel=true
|
|||||||
org.gradle.daemon=true
|
org.gradle.daemon=true
|
||||||
# Better Kotlin incremental compilation (warm builds)
|
# Better Kotlin incremental compilation (warm builds)
|
||||||
kotlin.incremental.useClasspathSnapshot=true
|
kotlin.incremental.useClasspathSnapshot=true
|
||||||
|
# This builtInKotlin flag was added automatically by Flutter migrator
|
||||||
|
android.builtInKotlin=false
|
||||||
|
|
||||||
|
android.newDsl=false
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-all.zip
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ pluginManagement {
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
id("com.android.application") version "8.11.1" apply false
|
id("com.android.application") version "9.2.0" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
id("org.jetbrains.kotlin.android") version "2.3.10" apply false
|
||||||
id("org.jetbrains.kotlin.plugin.compose") version "2.1.0" apply false
|
id("org.jetbrains.kotlin.plugin.compose") version "2.3.10" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
include(":app")
|
include(":app")
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 3.4 KiB |
@@ -1,3 +1,3 @@
|
|||||||
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.5 3C13.5 2.73478 13.3946 2.48043 13.2071 2.29289C13.0196 2.10536 12.7652 2 12.5 2C12.2348 2 11.9804 2.10536 11.7929 2.29289C11.6054 2.48043 11.5 2.73478 11.5 3V4C11.5 4.26522 11.6054 4.51957 11.7929 4.70711C11.9804 4.89464 12.2348 5 12.5 5C12.7652 5 13.0196 4.89464 13.2071 4.70711C13.3946 4.51957 13.5 4.26522 13.5 4V3ZM6.207 4.293C6.0184 4.11084 5.7658 4.01005 5.5036 4.01233C5.2414 4.0146 4.99059 4.11977 4.80518 4.30518C4.61977 4.49059 4.5146 4.7414 4.51233 5.0036C4.51005 5.2658 4.61084 5.5184 4.793 5.707L5.793 6.707C5.9816 6.88916 6.2342 6.98995 6.4964 6.98767C6.7586 6.9854 7.00941 6.88023 7.19482 6.69482C7.38023 6.50941 7.4854 6.2586 7.48767 5.9964C7.48995 5.7342 7.38916 5.4816 7.207 5.293L6.207 4.293ZM20.207 4.293C20.0195 4.10553 19.7652 4.00021 19.5 4.00021C19.2348 4.00021 18.9805 4.10553 18.793 4.293L17.793 5.293C17.6108 5.4816 17.51 5.7342 17.5123 5.9964C17.5146 6.2586 17.6198 6.50941 17.8052 6.69482C17.9906 6.88023 18.2414 6.9854 18.5036 6.98767C18.7658 6.98995 19.0184 6.88916 19.207 6.707L20.207 5.707C20.3945 5.51947 20.4998 5.26516 20.4998 5C20.4998 4.73484 20.3945 4.48053 20.207 4.293ZM12.5 7C11.1739 7 9.90215 7.52678 8.96447 8.46447C8.02678 9.40215 7.5 10.6739 7.5 12C7.5 13.3261 8.02678 14.5979 8.96447 15.5355C9.90215 16.4732 11.1739 17 12.5 17C13.8261 17 15.0979 16.4732 16.0355 15.5355C16.9732 14.5979 17.5 13.3261 17.5 12C17.5 10.6739 16.9732 9.40215 16.0355 8.46447C15.0979 7.52678 13.8261 7 12.5 7ZM3.5 11C3.23478 11 2.98043 11.1054 2.79289 11.2929C2.60536 11.4804 2.5 11.7348 2.5 12C2.5 12.2652 2.60536 12.5196 2.79289 12.7071C2.98043 12.8946 3.23478 13 3.5 13H4.5C4.76522 13 5.01957 12.8946 5.20711 12.7071C5.39464 12.5196 5.5 12.2652 5.5 12C5.5 11.7348 5.39464 11.4804 5.20711 11.2929C5.01957 11.1054 4.76522 11 4.5 11H3.5ZM20.5 11C20.2348 11 19.9804 11.1054 19.7929 11.2929C19.6054 11.4804 19.5 11.7348 19.5 12C19.5 12.2652 19.6054 12.5196 19.7929 12.7071C19.9804 12.8946 20.2348 13 20.5 13H21.5C21.7652 13 22.0196 12.8946 22.2071 12.7071C22.3946 12.5196 22.5 12.2652 22.5 12C22.5 11.7348 22.3946 11.4804 22.2071 11.2929C22.0196 11.1054 21.7652 11 21.5 11H20.5ZM7.207 18.707C7.30251 18.6148 7.37869 18.5044 7.4311 18.3824C7.48351 18.2604 7.5111 18.1292 7.51225 17.9964C7.5134 17.8636 7.4881 17.7319 7.43782 17.609C7.38754 17.4862 7.31329 17.3745 7.2194 17.2806C7.1255 17.1867 7.01385 17.1125 6.89095 17.0622C6.76806 17.0119 6.63638 16.9866 6.5036 16.9877C6.37082 16.9889 6.2396 17.0165 6.1176 17.0689C5.99559 17.1213 5.88525 17.1975 5.793 17.293L4.793 18.293C4.69749 18.3852 4.62131 18.4956 4.5689 18.6176C4.51649 18.7396 4.4889 18.8708 4.48775 19.0036C4.4866 19.1364 4.5119 19.2681 4.56218 19.391C4.61246 19.5138 4.68671 19.6255 4.7806 19.7194C4.8745 19.8133 4.98615 19.8875 5.10905 19.9378C5.23194 19.9881 5.36362 20.0134 5.4964 20.0123C5.62918 20.0111 5.7604 19.9835 5.8824 19.9311C6.00441 19.8787 6.11475 19.8025 6.207 19.707L7.207 18.707ZM19.207 17.293C19.0184 17.1108 18.7658 17.01 18.5036 17.0123C18.2414 17.0146 17.9906 17.1198 17.8052 17.3052C17.6198 17.4906 17.5146 17.7414 17.5123 18.0036C17.51 18.2658 17.6108 18.5184 17.793 18.707L18.793 19.707C18.9816 19.8892 19.2342 19.99 19.4964 19.9877C19.7586 19.9854 20.0094 19.8802 20.1948 19.6948C20.3802 19.5094 20.4854 19.2586 20.4877 18.9964C20.49 18.7342 20.3892 18.4816 20.207 18.293L19.207 17.293ZM13.5 20C13.5 19.7348 13.3946 19.4804 13.2071 19.2929C13.0196 19.1054 12.7652 19 12.5 19C12.2348 19 11.9804 19.1054 11.7929 19.2929C11.6054 19.4804 11.5 19.7348 11.5 20V21C11.5 21.2652 11.6054 21.5196 11.7929 21.7071C11.9804 21.8946 12.2348 22 12.5 22C12.7652 22 13.0196 21.8946 13.2071 21.7071C13.3946 21.5196 13.5 21.2652 13.5 21V20Z" fill="#FFFFFF"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 3C13 2.73478 12.8946 2.48043 12.7071 2.29289C12.5196 2.10536 12.2652 2 12 2C11.7348 2 11.4804 2.10536 11.2929 2.29289C11.1054 2.48043 11 2.73478 11 3V4C11 4.26522 11.1054 4.51957 11.2929 4.70711C11.4804 4.89464 11.7348 5 12 5C12.2652 5 12.5196 4.89464 12.7071 4.70711C12.8946 4.51957 13 4.26522 13 4V3ZM5.707 4.293C5.5184 4.11084 5.2658 4.01005 5.0036 4.01233C4.7414 4.0146 4.49059 4.11977 4.30518 4.30518C4.11977 4.49059 4.0146 4.7414 4.01233 5.0036C4.01005 5.2658 4.11084 5.5184 4.293 5.707L5.293 6.707C5.4816 6.88916 5.7342 6.98995 5.9964 6.98767C6.2586 6.9854 6.50941 6.88023 6.69482 6.69482C6.88023 6.50941 6.9854 6.2586 6.98767 5.9964C6.98995 5.7342 6.88916 5.4816 6.707 5.293L5.707 4.293ZM19.707 4.293C19.5195 4.10553 19.2652 4.00021 19 4.00021C18.7348 4.00021 18.4805 4.10553 18.293 4.293L17.293 5.293C17.1108 5.4816 17.01 5.7342 17.0123 5.9964C17.0146 6.2586 17.1198 6.50941 17.3052 6.69482C17.4906 6.88023 17.7414 6.9854 18.0036 6.98767C18.2658 6.98995 18.5184 6.88916 18.707 6.707L19.707 5.707C19.8945 5.51947 19.9998 5.26516 19.9998 5C19.9998 4.73484 19.8945 4.48053 19.707 4.293ZM12 7C10.6739 7 9.40215 7.52678 8.46447 8.46447C7.52678 9.40215 7 10.6739 7 12C7 13.3261 7.52678 14.5979 8.46447 15.5355C9.40215 16.4732 10.6739 17 12 17C13.3261 17 14.5979 16.4732 15.5355 15.5355C16.4732 14.5979 17 13.3261 17 12C17 10.6739 16.4732 9.40215 15.5355 8.46447C14.5979 7.52678 13.3261 7 12 7ZM3 11C2.73478 11 2.48043 11.1054 2.29289 11.2929C2.10536 11.4804 2 11.7348 2 12C2 12.2652 2.10536 12.5196 2.29289 12.7071C2.48043 12.8946 2.73478 13 3 13H4C4.26522 13 4.51957 12.8946 4.70711 12.7071C4.89464 12.5196 5 12.2652 5 12C5 11.7348 4.89464 11.4804 4.70711 11.2929C4.51957 11.1054 4.26522 11 4 11H3ZM20 11C19.7348 11 19.4804 11.1054 19.2929 11.2929C19.1054 11.4804 19 11.7348 19 12C19 12.2652 19.1054 12.5196 19.2929 12.7071C19.4804 12.8946 19.7348 13 20 13H21C21.2652 13 21.5196 12.8946 21.7071 12.7071C21.8946 12.5196 22 12.2652 22 12C22 11.7348 21.8946 11.4804 21.7071 11.2929C21.5196 11.1054 21.2652 11 21 11H20ZM6.707 18.707C6.80251 18.6148 6.87869 18.5044 6.9311 18.3824C6.98351 18.2604 7.0111 18.1292 7.01225 17.9964C7.0134 17.8636 6.9881 17.7319 6.93782 17.609C6.88754 17.4862 6.81329 17.3745 6.7194 17.2806C6.6255 17.1867 6.51385 17.1125 6.39095 17.0622C6.26806 17.0119 6.13638 16.9866 6.0036 16.9877C5.87082 16.9889 5.7396 17.0165 5.6176 17.0689C5.49559 17.1213 5.38525 17.1975 5.293 17.293L4.293 18.293C4.19749 18.3852 4.12131 18.4956 4.0689 18.6176C4.01649 18.7396 3.9889 18.8708 3.98775 19.0036C3.9866 19.1364 4.0119 19.2681 4.06218 19.391C4.11246 19.5138 4.18671 19.6255 4.2806 19.7194C4.3745 19.8133 4.48615 19.8875 4.60905 19.9378C4.73194 19.9881 4.86362 20.0134 4.9964 20.0123C5.12918 20.0111 5.2604 19.9835 5.3824 19.9311C5.50441 19.8787 5.61475 19.8025 5.707 19.707L6.707 18.707ZM18.707 17.293C18.5184 17.1108 18.2658 17.01 18.0036 17.0123C17.7414 17.0146 17.4906 17.1198 17.3052 17.3052C17.1198 17.4906 17.0146 17.7414 17.0123 18.0036C17.01 18.2658 17.1108 18.5184 17.293 18.707L18.293 19.707C18.4816 19.8892 18.7342 19.99 18.9964 19.9877C19.2586 19.9854 19.5094 19.8802 19.6948 19.6948C19.8802 19.5094 19.9854 19.2586 19.9877 18.9964C19.99 18.7342 19.8892 18.4816 19.707 18.293L18.707 17.293ZM13 20C13 19.7348 12.8946 19.4804 12.7071 19.2929C12.5196 19.1054 12.2652 19 12 19C11.7348 19 11.4804 19.1054 11.2929 19.2929C11.1054 19.4804 11 19.7348 11 20V21C11 21.2652 11.1054 21.5196 11.2929 21.7071C11.4804 21.8946 11.7348 22 12 22C12.2652 22 12.5196 21.8946 12.7071 21.7071C12.8946 21.5196 13 21.2652 13 21V20Z" fill="#FFFFFF"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.6 KiB |
@@ -1,15 +1,15 @@
|
|||||||
icons:
|
icons:
|
||||||
"flutter_launcher_icons.yaml": "c600507ca0df7cebd0f708124842512a14ed3d597b779176200d6ba25b1335b1"
|
"flutter_launcher_icons.yaml": "c600507ca0df7cebd0f708124842512a14ed3d597b779176200d6ba25b1335b1"
|
||||||
"pubspec.yaml": "cc5c8123f956bca34e25cec666ccc13fe5b7bdec06e217a58e1fa0ee58dedc20"
|
"pubspec.yaml": "a6ae0bd67a2b6226bec2dfd55437b3db2b7ca4a03f315c1a0a8c2f4b505c7c87"
|
||||||
"assets/images/logos/colored_logo.webp": "4b4fa99d144fe6694aa4487ba1b26aeecafae41e3c877836cd7da28d61a77983"
|
"assets/images/logos/colored_logo.webp": "4b4fa99d144fe6694aa4487ba1b26aeecafae41e3c877836cd7da28d61a77983"
|
||||||
"assets/images/logos/monochrome_logo.png": "188d2b0a64c70323b09bcee721663d6698fb557066f20ddaec97bba6869c1c6c"
|
"assets/images/logos/monochrome_logo.png": "188d2b0a64c70323b09bcee721663d6698fb557066f20ddaec97bba6869c1c6c"
|
||||||
"assets/images/logos/colored_logo_without_mustache.png": "d11cff9f38985885873bfdd2d84e61f8fab03803eada94d4caac1545ef3685f3"
|
"assets/images/logos/colored_logo_without_mustache.png": "d11cff9f38985885873bfdd2d84e61f8fab03803eada94d4caac1545ef3685f3"
|
||||||
"assets/images/logos/colored_logo_only_mustache.png": "bad6220c11bdfb1dfe04e5173bd2ebedd3999689d4b3a68fc63dc520c96dd33b"
|
"assets/images/logos/colored_logo_only_mustache.png": "bad6220c11bdfb1dfe04e5173bd2ebedd3999689d4b3a68fc63dc520c96dd33b"
|
||||||
l10n:
|
l10n:
|
||||||
"l10n.yml": "a57bc304cac4a2b0235593586f17f400a5165d67fc9aadeaa11893cfa36ee082"
|
"l10n.yml": "a57bc304cac4a2b0235593586f17f400a5165d67fc9aadeaa11893cfa36ee082"
|
||||||
"lib/l10n/app_de.arb": "9cd5913be1e3bc3ed6c088ef448d5ce2924a6290b7dd6006d1af624c5e9a2503"
|
"lib/l10n/app_de.arb": "ecfbf13bd33be9d27a2b54bfd8fb61e46c1a1dce905869d3f30cd05b4aecf258"
|
||||||
"lib/l10n/app_hu.arb": "17077ec76b68ed03796a264b99e4dba9e6ddd532e27a92d8fb237ea6f211f757"
|
"lib/l10n/app_en.arb": "7c43928f67b5b735283da04e3741f1afa2e9d41cdeb2e91c740e77fc84e7f046"
|
||||||
"lib/l10n/app_en.arb": "cbad6dd2485a983e399cce97371c19089b9110d30536488c14a7ea709c7b6ead"
|
"lib/l10n/app_hu.arb": "696a1ea2e86be364e9c815e5f739d3a2f5f3da9c05066084d9d26defe5018e2c"
|
||||||
isar:
|
isar:
|
||||||
"lib/data/models/app_settings_model.dart": "5eb5af345f1347f104257f0999763650fe2673f9da1754bd12d3f756fe5c9723"
|
"lib/data/models/app_settings_model.dart": "5eb5af345f1347f104257f0999763650fe2673f9da1754bd12d3f756fe5c9723"
|
||||||
"lib/data/models/generic_cache_model.dart": "79151d0467fb5d40c532eaaa08ad7c7e24a34304199280fbf49cf6e5adcce6bc"
|
"lib/data/models/generic_cache_model.dart": "79151d0467fb5d40c532eaaa08ad7c7e24a34304199280fbf49cf6e5adcce6bc"
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import 'dart:convert';
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:firka/core/extensions.dart';
|
||||||
import 'package:firka/data/models/generic_cache_model.dart';
|
import 'package:firka/data/models/generic_cache_model.dart';
|
||||||
import 'package:firka/data/models/timetable_cache_model.dart';
|
import 'package:firka/data/models/timetable_cache_model.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:isar_community/isar.dart';
|
import 'package:isar_community/isar.dart';
|
||||||
import 'package:kreta_api/kreta_api.dart' hide KretaEndpoints;
|
import 'package:kreta_api/kreta_api.dart';
|
||||||
|
|
||||||
import 'package:firka/app/app_state.dart';
|
import 'package:firka/app/app_state.dart';
|
||||||
import 'package:firka/core/bloc/reauth_cubit.dart';
|
import 'package:firka/core/bloc/reauth_cubit.dart';
|
||||||
@@ -46,6 +46,13 @@ class KretaClient {
|
|||||||
|
|
||||||
Future<void> _setReauthFlag() async {
|
Future<void> _setReauthFlag() async {
|
||||||
if (needsReauth) return;
|
if (needsReauth) return;
|
||||||
|
if (Platform.isIOS) {
|
||||||
|
try {
|
||||||
|
_watchChannel.invokeMethod('notifyReauthRequired');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[KretaClient] Watch reauth notification skipped: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
_reauthCubit.setNeedsReauth(true);
|
_reauthCubit.setNeedsReauth(true);
|
||||||
debugPrint('[KretaClient] Reauth flag set');
|
debugPrint('[KretaClient] Reauth flag set');
|
||||||
}
|
}
|
||||||
@@ -405,94 +412,166 @@ class KretaClient {
|
|||||||
return (resp.data, resp.statusCode!);
|
return (resp.data, resp.statusCode!);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<(dynamic, int, Object?, bool)> _cachingGet(
|
Future<ApiResponse<List<Lesson>>> _timetableCachingGet(
|
||||||
|
DateTime weekday,
|
||||||
|
bool forceCache,
|
||||||
|
) async {
|
||||||
|
var from = weekday.getMonday();
|
||||||
|
return await _cachingGet(
|
||||||
|
genCacheKey(from, model.studentIdNorm!),
|
||||||
|
KretaEndpoints.getTimeTable(
|
||||||
|
model.iss!,
|
||||||
|
from,
|
||||||
|
from.add(Duration(days: 6)),
|
||||||
|
),
|
||||||
|
forceCache,
|
||||||
|
0,
|
||||||
|
isar.timetableCacheModels,
|
||||||
|
(key, resp) => TimetableCacheModel()
|
||||||
|
..cacheKey = key
|
||||||
|
..values = (resp as List<dynamic>)
|
||||||
|
.map((item) => jsonEncode(item))
|
||||||
|
.toList(),
|
||||||
|
(cache) => cache.values
|
||||||
|
.map((data) => Lesson.fromJson(jsonDecode(data)))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ApiResponse<List<R>>> _genericListedCachingGet<R>(
|
||||||
CacheId id,
|
CacheId id,
|
||||||
String url,
|
String url,
|
||||||
bool forceCache,
|
bool forceCache,
|
||||||
int counter,
|
R Function(dynamic) mapResultEntries,
|
||||||
) async {
|
) async {
|
||||||
// it would be *ideal* to use xor and left shift here, however
|
return await _genericCachingGet(
|
||||||
// binary operations seem to round the number down to
|
id,
|
||||||
// 32 bits for some reason???
|
url,
|
||||||
var cacheKey = model.studentIdNorm! + ((id.index + 1) * pow(10, 11));
|
forceCache,
|
||||||
var cache = await isar.genericCacheModels.get(cacheKey as int);
|
(data) => (data as List<dynamic>).map(mapResultEntries).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ApiResponse<R>> _genericCachingGet<R>(
|
||||||
|
CacheId id,
|
||||||
|
String url,
|
||||||
|
bool forceCache,
|
||||||
|
R Function(dynamic) makeResult,
|
||||||
|
) async {
|
||||||
|
return await _cachingGet(
|
||||||
|
// it would be *ideal* to use xor and left shift here, however
|
||||||
|
// binary operations seem to round the number down to
|
||||||
|
// 32 bits for some reason???
|
||||||
|
(model.studentIdNorm! + ((id.index + 1) * pow(10, 11))) as Id,
|
||||||
|
url,
|
||||||
|
forceCache,
|
||||||
|
0,
|
||||||
|
isar.genericCacheModels,
|
||||||
|
(key, resp) => GenericCacheModel()
|
||||||
|
..cacheKey = key
|
||||||
|
..cacheData = jsonEncode(resp),
|
||||||
|
(cache) {
|
||||||
|
return makeResult(jsonDecode(cache.cacheData!));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ApiResponse<R>> _cachingGet<T, R>(
|
||||||
|
Id cacheKey,
|
||||||
|
String url,
|
||||||
|
bool forceCache,
|
||||||
|
int counter,
|
||||||
|
IsarCollection<T> collection,
|
||||||
|
T Function(Id, dynamic) makeCache,
|
||||||
|
R Function(T) makeResult,
|
||||||
|
) async {
|
||||||
|
var cache = await collection.get(cacheKey);
|
||||||
|
|
||||||
|
if (forceCache && cache != null) {
|
||||||
|
logger.finest(
|
||||||
|
"_cachingGet(forceCache: $forceCache}): decoding cached response for: $url",
|
||||||
|
);
|
||||||
|
return ApiResponse.cached(makeResult(cache));
|
||||||
|
}
|
||||||
|
|
||||||
dynamic resp;
|
|
||||||
int statusCode;
|
|
||||||
try {
|
try {
|
||||||
if (forceCache && cache != null) {
|
var (resp, statusCode) = await _authJson("GET", url);
|
||||||
logger.finest(
|
|
||||||
"_cachingGet(forceCache: $forceCache}): decoding cached response for: $url",
|
if (statusCode >= 400 && cache != null) {
|
||||||
);
|
logger.finest("request failed: $statusCode, using cache for: $url");
|
||||||
return (jsonDecode(cache.cacheData!), 200, null, true);
|
return ApiResponse(makeResult(cache), statusCode, null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
var newCache = makeCache(cacheKey, resp);
|
||||||
(resp, statusCode) = await _authJson("GET", url);
|
|
||||||
|
|
||||||
if (statusCode >= 400) {
|
await isar.writeTxn(() async {
|
||||||
if (cache != null) {
|
collection.put(newCache);
|
||||||
logger.finest(
|
});
|
||||||
"_cachingGet(forceCache: $forceCache}): decoding uncached response for: $url",
|
|
||||||
);
|
return ApiResponse(makeResult(newCache), statusCode, null, false);
|
||||||
return (jsonDecode(cache.cacheData!), statusCode, null, true);
|
} catch (ex) {
|
||||||
}
|
if (_isTokenExpired(ex)) {
|
||||||
}
|
logger.warning("Token expired, setting needsReauth flag");
|
||||||
} catch (ex) {
|
await _setReauthFlag();
|
||||||
if (ex is Error) {
|
|
||||||
logger.finest(
|
return ApiResponse(null, 0, ex, false);
|
||||||
"Request failed for $url",
|
}
|
||||||
ex.toString(),
|
|
||||||
ex.stackTrace,
|
if (ex is DioException && counter < backoffCount) {
|
||||||
);
|
|
||||||
} else {
|
|
||||||
logger.finest("Request failed for $url", ex.toString());
|
|
||||||
}
|
|
||||||
logger.finest("Retrying: $counter / $backoffCount");
|
logger.finest("Retrying: $counter / $backoffCount");
|
||||||
if (_isTokenExpired(ex) ||
|
|
||||||
ex is! DioException ||
|
|
||||||
counter >= backoffCount) {
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
|
|
||||||
final backoffDelay = backoffMin + (counter * backoffStep);
|
final backoffDelay = backoffMin + (counter * backoffStep);
|
||||||
logger.finest("Waiting: $backoffDelay");
|
logger.finest("Waiting: $backoffDelay");
|
||||||
await Future.delayed(Duration(milliseconds: backoffDelay));
|
await Future.delayed(Duration(milliseconds: backoffDelay));
|
||||||
|
|
||||||
return _cachingGet(id, url, forceCache, counter + 1);
|
return _cachingGet(
|
||||||
}
|
cacheKey,
|
||||||
} catch (ex) {
|
url,
|
||||||
if (_isTokenExpired(ex)) {
|
forceCache,
|
||||||
await _setReauthFlag();
|
counter + 1,
|
||||||
logger.warning("Token expired, setting needsReauth flag");
|
collection,
|
||||||
|
makeCache,
|
||||||
if (Platform.isIOS && needsReauth) {
|
makeResult,
|
||||||
try {
|
);
|
||||||
_watchChannel.invokeMethod('notifyReauthRequired');
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('[KretaClient] Watch reauth notification skipped: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cache != null) {
|
if (cache != null) {
|
||||||
logger.finest("request failed, using cache for: $url");
|
logger.finest("request failed, using cache for: $url");
|
||||||
return (jsonDecode(cache.cacheData!), 0, ex, true);
|
return ApiResponse(makeResult(cache), 0, ex, true);
|
||||||
} else {
|
|
||||||
logger.finest("request failed, no cache for: $url");
|
|
||||||
return (null, 0, ex, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.finest("request failed, no cache for: $url");
|
||||||
|
return ApiResponse(null, 0, ex, false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await isar.writeTxn(() async {
|
ApiResponse<List<ClassGroupSubjectAverage>>? classGroupAveragesCache;
|
||||||
var cache = GenericCacheModel();
|
|
||||||
cache.cacheKey = cacheKey;
|
|
||||||
cache.cacheData = jsonEncode(resp);
|
|
||||||
|
|
||||||
isar.genericCacheModels.put(cache);
|
Future<ApiResponse<List<ClassGroupSubjectAverage>>> getClassGroupAverages(
|
||||||
});
|
ClassGroup classGroup, {
|
||||||
|
bool forceCache = true,
|
||||||
|
}) async {
|
||||||
|
if (classGroup.studyTask == null) {
|
||||||
|
String? err = "classGroup.studyTask is null";
|
||||||
|
logger.warning(err);
|
||||||
|
return ApiResponse([], 0, err, false);
|
||||||
|
}
|
||||||
|
if (!forceCache) {
|
||||||
|
classGroupAveragesCache = null;
|
||||||
|
} else if (classGroupAveragesCache != null) {
|
||||||
|
return classGroupAveragesCache!;
|
||||||
|
}
|
||||||
|
var studyTaskUid = classGroup.studyTask!.uid.toString().split(",").first;
|
||||||
|
var resp = await _genericListedCachingGet(
|
||||||
|
CacheId.getClassGroupAvg,
|
||||||
|
KretaEndpoints.getClassGroupAvg(model.iss!, studyTaskUid),
|
||||||
|
forceCache,
|
||||||
|
(item) => ClassGroupSubjectAverage.fromJson(item),
|
||||||
|
);
|
||||||
|
|
||||||
return (resp, statusCode, null, false);
|
if (resp.err == null) {
|
||||||
|
classGroupAveragesCache = ApiResponse.cached(resp.response);
|
||||||
|
}
|
||||||
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
ApiResponse<Student>? studentCache;
|
ApiResponse<Student>? studentCache;
|
||||||
@@ -503,28 +582,18 @@ class KretaClient {
|
|||||||
} else if (studentCache != null) {
|
} else if (studentCache != null) {
|
||||||
return studentCache!;
|
return studentCache!;
|
||||||
}
|
}
|
||||||
var (resp, status, ex, cached) = await _cachingGet(
|
|
||||||
|
return await _genericCachingGet(
|
||||||
CacheId.getStudent,
|
CacheId.getStudent,
|
||||||
KretaEndpoints.getStudentUrl(model.iss!),
|
KretaEndpoints.getStudentUrl(model.iss!),
|
||||||
forceCache,
|
forceCache,
|
||||||
0,
|
(cache) => Student.fromJson(cache),
|
||||||
);
|
).then((resp) {
|
||||||
|
if (resp.err == null) {
|
||||||
Student? student;
|
studentCache = ApiResponse.cached(resp.response);
|
||||||
String? err;
|
}
|
||||||
try {
|
return resp;
|
||||||
student = Student.fromJson(resp);
|
});
|
||||||
} catch (ex) {
|
|
||||||
err = ex.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ex != null) {
|
|
||||||
err = ex.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ex == null) studentCache = ApiResponse(student, 200, null, true);
|
|
||||||
|
|
||||||
return ApiResponse(student, status, err, cached);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ApiResponse<List<ClassGroup>>? classGroupCache;
|
ApiResponse<List<ClassGroup>>? classGroupCache;
|
||||||
@@ -537,31 +606,18 @@ class KretaClient {
|
|||||||
} else {
|
} else {
|
||||||
if (classGroupCache != null) return classGroupCache!;
|
if (classGroupCache != null) return classGroupCache!;
|
||||||
}
|
}
|
||||||
var (resp, status, ex, cached) = await _cachingGet(
|
|
||||||
|
return await _genericListedCachingGet(
|
||||||
CacheId.getClassGroup,
|
CacheId.getClassGroup,
|
||||||
KretaEndpoints.getClassGroups(model.iss!),
|
KretaEndpoints.getClassGroups(model.iss!),
|
||||||
forceCache,
|
forceCache,
|
||||||
0,
|
(item) => ClassGroup.fromJson(item),
|
||||||
);
|
).then((resp) {
|
||||||
|
if (resp.err == null) {
|
||||||
final classGroups = List<ClassGroup>.empty(growable: true);
|
classGroupCache = ApiResponse.cached(resp.response);
|
||||||
String? err;
|
|
||||||
try {
|
|
||||||
List<dynamic> rawItems = resp;
|
|
||||||
for (var item in rawItems) {
|
|
||||||
classGroups.add(ClassGroup.fromJson(item));
|
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
return resp;
|
||||||
err = ex.toString();
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (ex != null) {
|
|
||||||
err = ex.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ex == null) classGroupCache = ApiResponse(classGroups, 200, null, true);
|
|
||||||
|
|
||||||
return ApiResponse(classGroups, status, err, cached);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ApiResponse<List<NoticeBoardItem>>? noticeBoardCache;
|
ApiResponse<List<NoticeBoardItem>>? noticeBoardCache;
|
||||||
@@ -574,64 +630,40 @@ class KretaClient {
|
|||||||
} else if (noticeBoardCache != null) {
|
} else if (noticeBoardCache != null) {
|
||||||
return noticeBoardCache!;
|
return noticeBoardCache!;
|
||||||
}
|
}
|
||||||
var (resp, status, ex, cached) = await _cachingGet(
|
|
||||||
|
return await _genericListedCachingGet(
|
||||||
CacheId.getNoticeBoard,
|
CacheId.getNoticeBoard,
|
||||||
KretaEndpoints.getNoticeBoard(model.iss!),
|
KretaEndpoints.getNoticeBoard(model.iss!),
|
||||||
forceCache,
|
forceCache,
|
||||||
0,
|
(item) => NoticeBoardItem.fromJson(item),
|
||||||
);
|
).then((resp) {
|
||||||
|
if (resp.err == null) {
|
||||||
var items = List<NoticeBoardItem>.empty(growable: true);
|
noticeBoardCache = ApiResponse.cached(resp.response);
|
||||||
String? err;
|
|
||||||
try {
|
|
||||||
List<dynamic> rawItems = resp;
|
|
||||||
for (var item in rawItems) {
|
|
||||||
items.add(NoticeBoardItem.fromJson(item));
|
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
return resp;
|
||||||
err = ex.toString();
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (ex != null) {
|
|
||||||
err = ex.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err == null) noticeBoardCache = ApiResponse(items, 200, null, true);
|
|
||||||
|
|
||||||
return ApiResponse(items, status, err, cached);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ApiResponse<List<InfoBoardItem>>? infoBoardCache;
|
ApiResponse<List<InfoBoardItem>>? infoBoardCache;
|
||||||
|
|
||||||
Future<ApiResponse<List<InfoBoardItem>>> getInfoBoard({
|
Future<ApiResponse<List<InfoBoardItem>>> getInfoBoard({
|
||||||
|
DateTime? from,
|
||||||
|
DateTime? to,
|
||||||
bool forceCache = true,
|
bool forceCache = true,
|
||||||
}) async {
|
}) async {
|
||||||
if (forceCache && infoBoardCache != null) return infoBoardCache!;
|
if (forceCache && infoBoardCache != null) return infoBoardCache!;
|
||||||
var (resp, status, ex, cached) = await _cachingGet(
|
|
||||||
|
return await _genericListedCachingGet(
|
||||||
CacheId.getInfoBoard,
|
CacheId.getInfoBoard,
|
||||||
KretaEndpoints.getInfoBoard(model.iss!),
|
KretaEndpoints.getInfoBoard(model.iss!, from, to),
|
||||||
forceCache,
|
forceCache,
|
||||||
0,
|
(item) => InfoBoardItem.fromJson(item),
|
||||||
);
|
).then((resp) {
|
||||||
|
if (resp.err == null) {
|
||||||
var items = List<InfoBoardItem>.empty(growable: true);
|
infoBoardCache = ApiResponse.cached(resp.response);
|
||||||
String? err;
|
|
||||||
try {
|
|
||||||
List<dynamic> rawItems = resp;
|
|
||||||
for (var item in rawItems) {
|
|
||||||
items.add(InfoBoardItem.fromJson(item));
|
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
return resp;
|
||||||
err = ex.toString();
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (ex != null) {
|
|
||||||
err = ex.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err == null) infoBoardCache = ApiResponse(items, 200, null, true);
|
|
||||||
|
|
||||||
return ApiResponse(items, status, err, cached);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ApiResponse<List<Grade>>? gradeCache;
|
ApiResponse<List<Grade>>? gradeCache;
|
||||||
@@ -642,33 +674,19 @@ class KretaClient {
|
|||||||
} else if (gradeCache != null) {
|
} else if (gradeCache != null) {
|
||||||
return gradeCache!;
|
return gradeCache!;
|
||||||
}
|
}
|
||||||
var (resp, status, ex, cached) = await _cachingGet(
|
|
||||||
|
return await _genericListedCachingGet(
|
||||||
CacheId.getGrades,
|
CacheId.getGrades,
|
||||||
KretaEndpoints.getGrades(model.iss!),
|
KretaEndpoints.getGrades(model.iss!),
|
||||||
forceCache,
|
forceCache,
|
||||||
0,
|
(item) => Grade.fromJson(item),
|
||||||
);
|
).then((resp) {
|
||||||
|
if (resp.err == null) {
|
||||||
var items = List<Grade>.empty(growable: true);
|
resp.response!.sort((a, b) => b.recordDate.compareTo(a.recordDate));
|
||||||
String? err;
|
gradeCache = ApiResponse.cached(resp.response);
|
||||||
try {
|
|
||||||
List<dynamic> rawItems = resp;
|
|
||||||
for (var item in rawItems) {
|
|
||||||
items.add(Grade.fromJson(item));
|
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
return resp;
|
||||||
err = ex.toString();
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (ex != null) {
|
|
||||||
err = ex.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
items.sort((a, b) => b.recordDate.compareTo(a.recordDate));
|
|
||||||
|
|
||||||
if (ex == null) gradeCache = ApiResponse(items, 200, null, true);
|
|
||||||
|
|
||||||
return ApiResponse(items, status, err, cached);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ApiResponse<List<SubjectAverage>>? subjectAverageCache;
|
ApiResponse<List<SubjectAverage>>? subjectAverageCache;
|
||||||
@@ -694,224 +712,36 @@ class KretaClient {
|
|||||||
return subjectAverageCache!;
|
return subjectAverageCache!;
|
||||||
}
|
}
|
||||||
var studyTaskUid = classGroup.studyTask!.uid.toString().split(",").first;
|
var studyTaskUid = classGroup.studyTask!.uid.toString().split(",").first;
|
||||||
var (resp, status, ex, cached) = await _cachingGet(
|
|
||||||
|
return await _genericListedCachingGet(
|
||||||
CacheId.getSubjectAvg,
|
CacheId.getSubjectAvg,
|
||||||
KretaEndpoints.getSubjectAvg(model.iss!, studyTaskUid),
|
KretaEndpoints.getSubjectAvg(model.iss!, studyTaskUid),
|
||||||
forceCache,
|
forceCache,
|
||||||
0,
|
(item) => SubjectAverage.fromJson(item),
|
||||||
);
|
).then((resp) {
|
||||||
|
if (resp.err == null) {
|
||||||
var items = List<SubjectAverage>.empty(growable: true);
|
subjectAverageCache = ApiResponse.cached(resp.response);
|
||||||
try {
|
|
||||||
List<dynamic> rawItems = resp;
|
|
||||||
for (var item in rawItems) {
|
|
||||||
items.add(SubjectAverage.fromJson(item));
|
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
return resp;
|
||||||
err = ex.toString();
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (ex != null) {
|
|
||||||
err = ex.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ex == null) subjectAverageCache = ApiResponse(items, 200, null, true);
|
|
||||||
return ApiResponse(items, status, err, cached);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<(List<dynamic>, int, Object?, bool)>
|
|
||||||
_timedCachingGet<T extends DatedCacheEntry>(
|
|
||||||
IsarCollection<T> cacheModel,
|
|
||||||
String endpoint,
|
|
||||||
DateTime from,
|
|
||||||
DateTime? to,
|
|
||||||
bool forceCache,
|
|
||||||
int counter,
|
|
||||||
Future<void> Function(dynamic, int) storeCache,
|
|
||||||
) async {
|
|
||||||
var cacheKey = genCacheKey(from, model.studentIdNorm!);
|
|
||||||
var cache = await cacheModel.get(cacheKey);
|
|
||||||
var formatter = DateFormat('yyyy-MM-dd');
|
|
||||||
var fromStr = formatter.format(from);
|
|
||||||
var toStr = to != null ? formatter.format(to) : null;
|
|
||||||
var now = timeNow();
|
|
||||||
|
|
||||||
if (cache != null && (cache as dynamic).values == null) {
|
|
||||||
(cache as dynamic).values = List<String>.empty(growable: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<dynamic> resp;
|
|
||||||
int statusCode;
|
|
||||||
try {
|
|
||||||
if (forceCache && cache != null) {
|
|
||||||
var items = List<dynamic>.empty(growable: true);
|
|
||||||
for (var item in (cache as dynamic).values) {
|
|
||||||
items.add(jsonDecode(item));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (items, 200, null, true);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
if (toStr == null) {
|
|
||||||
(resp, statusCode) = await _authJson(
|
|
||||||
"GET",
|
|
||||||
"$endpoint?"
|
|
||||||
"datumTol=$fromStr",
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
(resp, statusCode) = await _authJson(
|
|
||||||
"GET",
|
|
||||||
"$endpoint?"
|
|
||||||
"datumTol=$fromStr&datumIg=$toStr",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (statusCode >= 400) {
|
|
||||||
if (cache != null) {
|
|
||||||
var items = List<dynamic>.empty(growable: true);
|
|
||||||
for (var item in (cache as dynamic).values) {
|
|
||||||
items.add(jsonDecode(item));
|
|
||||||
}
|
|
||||||
return (items, statusCode, null, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (ex) {
|
|
||||||
if (_isTokenExpired(ex) ||
|
|
||||||
ex is! DioException ||
|
|
||||||
counter >= backoffCount) {
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Future.delayed(
|
|
||||||
Duration(milliseconds: backoffMin + (counter * backoffStep)),
|
|
||||||
);
|
|
||||||
|
|
||||||
return _timedCachingGet(
|
|
||||||
cacheModel,
|
|
||||||
endpoint,
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
forceCache,
|
|
||||||
counter + 1,
|
|
||||||
storeCache,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (ex) {
|
|
||||||
if (_isTokenExpired(ex)) {
|
|
||||||
await _setReauthFlag();
|
|
||||||
logger.warning(
|
|
||||||
"Token expired in timed request, setting needsReauth flag",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cache != null) {
|
|
||||||
var items = List<dynamic>.empty(growable: true);
|
|
||||||
for (var item in (cache as dynamic).values) {
|
|
||||||
items.add(jsonDecode(item));
|
|
||||||
}
|
|
||||||
return (items, 0, ex, true);
|
|
||||||
} else {
|
|
||||||
return (List<dynamic>.empty(growable: true), 0, ex, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// only cache stuff 4 months ago and a month in advance
|
|
||||||
if (from.millisecondsSinceEpoch >=
|
|
||||||
now.subtract(Duration(days: 120)).millisecondsSinceEpoch) {
|
|
||||||
if (to == null ||
|
|
||||||
to.millisecondsSinceEpoch <=
|
|
||||||
now.add(Duration(days: 31)).millisecondsSinceEpoch) {
|
|
||||||
await isar.writeTxn(() async {
|
|
||||||
await storeCache(resp, cacheKey);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (resp, statusCode, null, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Expects from and to to be 7 days apart
|
|
||||||
Future<ApiResponse<List<Lesson>>> _getTimeTable(
|
|
||||||
DateTime from,
|
|
||||||
DateTime to,
|
|
||||||
bool forceCache,
|
|
||||||
) async {
|
|
||||||
var (
|
|
||||||
resp,
|
|
||||||
status,
|
|
||||||
ex,
|
|
||||||
cached,
|
|
||||||
) = await _timedCachingGet<TimetableCacheModel>(
|
|
||||||
isar.timetableCacheModels,
|
|
||||||
KretaEndpoints.getTimeTable(model.iss!),
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
forceCache,
|
|
||||||
0,
|
|
||||||
(dynamic resp, int cacheKey) async {
|
|
||||||
TimetableCacheModel cache = TimetableCacheModel();
|
|
||||||
var rawClasses = List<String>.empty(growable: true);
|
|
||||||
|
|
||||||
for (var obj in resp) {
|
|
||||||
rawClasses.add(jsonEncode(obj));
|
|
||||||
}
|
|
||||||
|
|
||||||
cache.cacheKey = cacheKey;
|
|
||||||
cache.values = rawClasses;
|
|
||||||
|
|
||||||
await isar.timetableCacheModels.put(cache as dynamic);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
var items = List<Lesson>.empty(growable: true);
|
|
||||||
String? err;
|
|
||||||
try {
|
|
||||||
List<dynamic> rawItems = resp;
|
|
||||||
for (var item in rawItems) {
|
|
||||||
items.add(Lesson.fromJson(item));
|
|
||||||
}
|
|
||||||
} catch (ex) {
|
|
||||||
err = ex.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ex != null) {
|
|
||||||
err = ex.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return ApiResponse(items, status, err, cached);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ApiResponse<List<Homework>>> getHomework({
|
Future<ApiResponse<List<Homework>>> getHomework({
|
||||||
|
DateTime? from,
|
||||||
|
DateTime? to,
|
||||||
bool forceCache = true,
|
bool forceCache = true,
|
||||||
}) async {
|
}) async {
|
||||||
final now = timeNow().subtract(Duration(days: 365));
|
if (from == null && to == null) {
|
||||||
var formatter = DateFormat('yyyy-MM-dd');
|
DateTime now = timeNow();
|
||||||
var start = formatter.format(now);
|
DateTime start = now.copyWith(month: 9, day: 1);
|
||||||
var (resp, status, ex, cached) = await _cachingGet(
|
from = now.isBefore(start) ? start.subtract(Duration(days: 365)) : start;
|
||||||
|
}
|
||||||
|
return await _genericListedCachingGet(
|
||||||
CacheId.getHomework,
|
CacheId.getHomework,
|
||||||
"${KretaEndpoints.getHomework(model.iss!)}?datumTol=$start",
|
KretaEndpoints.getHomework(model.iss!, from, to),
|
||||||
forceCache,
|
forceCache,
|
||||||
0,
|
(item) => Homework.fromJson(item),
|
||||||
);
|
);
|
||||||
|
|
||||||
var items = List<Homework>.empty(growable: true);
|
|
||||||
String? err;
|
|
||||||
try {
|
|
||||||
List<dynamic> rawItems = resp;
|
|
||||||
for (var item in rawItems) {
|
|
||||||
items.add(Homework.fromJson(item));
|
|
||||||
}
|
|
||||||
} catch (ex) {
|
|
||||||
err = ex.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ex != null) {
|
|
||||||
err = ex.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// items.sort((a, b) => a.date.compareTo(b.date));
|
|
||||||
|
|
||||||
return ApiResponse(items, status, err, cached);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Automatically aligns requests to start at Monday and end at Sunday
|
/// Automatically aligns requests to start at Monday and end at Sunday
|
||||||
@@ -929,30 +759,25 @@ class KretaClient {
|
|||||||
i < to.millisecondsSinceEpoch;
|
i < to.millisecondsSinceEpoch;
|
||||||
i += 604800000
|
i += 604800000
|
||||||
) {
|
) {
|
||||||
var from = DateTime.fromMillisecondsSinceEpoch(i);
|
var weekday = DateTime.fromMillisecondsSinceEpoch(i);
|
||||||
var start = from.subtract(Duration(days: from.weekday - 1));
|
|
||||||
var end = start.add(Duration(days: 6));
|
|
||||||
|
|
||||||
var resp = await _getTimeTable(start, end, forceCache);
|
var resp = await _timetableCachingGet(weekday, forceCache);
|
||||||
if (resp.err != null) {
|
if (resp.err != null) {
|
||||||
err = resp.err;
|
return resp;
|
||||||
if (!resp.cached) {
|
|
||||||
return resp;
|
|
||||||
} else {
|
|
||||||
lessons.addAll(resp.response!);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
lessons.addAll(resp.response!);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lessons.addAll(resp.response!);
|
||||||
|
|
||||||
if (!resp.cached) cached = false;
|
if (!resp.cached) cached = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
lessons.sort((a, b) => a.start.compareTo(b.start));
|
lessons =
|
||||||
lessons = lessons
|
lessons
|
||||||
.where(
|
.where(
|
||||||
(lesson) => lesson.start.isAfter(from) && lesson.end.isBefore(to),
|
(lesson) => lesson.start.isAfter(from) && lesson.end.isBefore(to),
|
||||||
)
|
)
|
||||||
.toList();
|
.toList()
|
||||||
|
..sort((a, b) => a.start.compareTo(b.start));
|
||||||
|
|
||||||
return ApiResponse(lessons, 200, err, cached);
|
return ApiResponse(lessons, 200, err, cached);
|
||||||
}
|
}
|
||||||
@@ -960,66 +785,25 @@ class KretaClient {
|
|||||||
Future<ApiResponse<List<AllLessons>>> getLessons({
|
Future<ApiResponse<List<AllLessons>>> getLessons({
|
||||||
bool forceCache = true,
|
bool forceCache = true,
|
||||||
}) async {
|
}) async {
|
||||||
var (resp, status, ex, cached) = await _cachingGet(
|
return await _genericListedCachingGet(
|
||||||
CacheId.getLessons,
|
CacheId.getLessons,
|
||||||
KretaEndpoints.getLessons(model.iss!),
|
KretaEndpoints.getLessons(model.iss!),
|
||||||
forceCache,
|
forceCache,
|
||||||
0,
|
(item) => AllLessons.fromJson(item),
|
||||||
);
|
);
|
||||||
|
|
||||||
var items = <AllLessons>[];
|
|
||||||
String? err;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (resp is List) {
|
|
||||||
for (var item in resp) {
|
|
||||||
if (item != null && item is Map<String, dynamic>) {
|
|
||||||
items.add(AllLessons.fromJson(item));
|
|
||||||
} else {
|
|
||||||
logger.warning("$item");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
err = "${resp.runtimeType}";
|
|
||||||
}
|
|
||||||
} catch (e, stack) {
|
|
||||||
err = e.toString();
|
|
||||||
logger.warning(e, stack);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ex != null) {
|
|
||||||
err = ex.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return ApiResponse(items, status, err, cached);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ApiResponse<List<Test>>> getTests({bool forceCache = true}) async {
|
Future<ApiResponse<List<Test>>> getTests({
|
||||||
var (resp, status, ex, cached) = await _cachingGet(
|
DateTime? from,
|
||||||
|
DateTime? to,
|
||||||
|
bool forceCache = true,
|
||||||
|
}) async {
|
||||||
|
return await _genericListedCachingGet(
|
||||||
CacheId.getTests,
|
CacheId.getTests,
|
||||||
KretaEndpoints.getTests(model.iss!),
|
KretaEndpoints.getTests(model.iss!, from, to),
|
||||||
forceCache,
|
forceCache,
|
||||||
0,
|
(item) => Test.fromJson(item),
|
||||||
);
|
);
|
||||||
|
|
||||||
var items = List<Test>.empty(growable: true);
|
|
||||||
String? err;
|
|
||||||
try {
|
|
||||||
List<dynamic> rawItems = resp;
|
|
||||||
for (var item in rawItems) {
|
|
||||||
items.add(Test.fromJson(item));
|
|
||||||
}
|
|
||||||
} catch (ex) {
|
|
||||||
err = ex.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ex != null) {
|
|
||||||
err = ex.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// items.sort((a, b) => a.date.compareTo(b.date));
|
|
||||||
|
|
||||||
return ApiResponse(items, status, err, cached);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ApiResponse<List<Omission>>? omissionsCache;
|
ApiResponse<List<Omission>>? omissionsCache;
|
||||||
@@ -1032,33 +816,18 @@ class KretaClient {
|
|||||||
} else {
|
} else {
|
||||||
if (omissionsCache != null) return omissionsCache!;
|
if (omissionsCache != null) return omissionsCache!;
|
||||||
}
|
}
|
||||||
var (resp, status, ex, cached) = await _cachingGet(
|
return await _genericListedCachingGet(
|
||||||
CacheId.getOmissions,
|
CacheId.getOmissions,
|
||||||
KretaEndpoints.getOmissions(model.iss!),
|
KretaEndpoints.getOmissions(model.iss!),
|
||||||
forceCache,
|
forceCache,
|
||||||
0,
|
(item) => Omission.fromJson(item),
|
||||||
);
|
).then((resp) {
|
||||||
|
if (resp.err == null) {
|
||||||
var items = List<Omission>.empty(growable: true);
|
resp.response!.sort((a, b) => a.date.compareTo(b.date));
|
||||||
String? err;
|
omissionsCache = ApiResponse.cached(resp.response);
|
||||||
try {
|
|
||||||
List<dynamic> rawItems = resp;
|
|
||||||
for (var item in rawItems) {
|
|
||||||
items.add(Omission.fromJson(item));
|
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
return resp;
|
||||||
err = ex.toString();
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (ex != null) {
|
|
||||||
err = ex.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
items.sort((a, b) => a.date.compareTo(b.date));
|
|
||||||
|
|
||||||
if (ex == null) omissionsCache = ApiResponse(items, 200, null, true);
|
|
||||||
|
|
||||||
return ApiResponse(items, status, err, cached);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void evictMemCache() {
|
void evictMemCache() {
|
||||||
@@ -1071,5 +840,4 @@ class KretaClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool _isTokenExpired(Object ex) =>
|
bool _isTokenExpired(Object ex) =>
|
||||||
ex.toString() == TokenExpiredException().toString() ||
|
ex is TokenExpiredException || ex is InvalidGrantException;
|
||||||
ex.toString() == InvalidGrantException().toString();
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import 'dart:typed_data';
|
|||||||
|
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:firka/app/app_state.dart';
|
import 'package:firka/app/app_state.dart';
|
||||||
import 'package:kreta_api/kreta_api.dart' as ka;
|
|
||||||
|
|
||||||
class Constants {
|
class Constants {
|
||||||
static String get clientId {
|
static String get clientId {
|
||||||
@@ -17,7 +16,7 @@ class Constants {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static const applicationId = "hu.ekreta.student";
|
static const applicationId = "hu.ekreta.student";
|
||||||
static const applicationVersion = "5.7.0";
|
static const applicationVersion = "5.15.0";
|
||||||
|
|
||||||
static String get userAgent {
|
static String get userAgent {
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
@@ -41,7 +40,7 @@ class TimetableConsts {
|
|||||||
static const event = "TanevRendjeEsemeny";
|
static const event = "TanevRendjeEsemeny";
|
||||||
}
|
}
|
||||||
|
|
||||||
class KretaEndpoints {
|
class KretaLoginEndpoints {
|
||||||
static String _generateCodeVerifier() {
|
static String _generateCodeVerifier() {
|
||||||
var random = Random.secure();
|
var random = Random.secure();
|
||||||
final bytes = List<int>.generate(32, (i) => random.nextInt(256));
|
final bytes = List<int>.generate(32, (i) => random.nextInt(256));
|
||||||
@@ -63,8 +62,6 @@ class KretaEndpoints {
|
|||||||
return base64Url.encode(bytes).replaceAll('=', '');
|
return base64Url.encode(bytes).replaceAll('=', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
static String kreta(String iss) => ka.KretaEndpoints.kreta(iss);
|
|
||||||
|
|
||||||
static final String codeVerifier = _generateCodeVerifier();
|
static final String codeVerifier = _generateCodeVerifier();
|
||||||
static final String _codeChallenge = _generateCodeChallenge(codeVerifier);
|
static final String _codeChallenge = _generateCodeChallenge(codeVerifier);
|
||||||
static final String stateOrNonce = generateStateOrNonce();
|
static final String stateOrNonce = generateStateOrNonce();
|
||||||
@@ -77,30 +74,4 @@ class KretaEndpoints {
|
|||||||
static String kretaLoginUrlRefresh(String username, String schoolId) =>
|
static String kretaLoginUrlRefresh(String username, String schoolId) =>
|
||||||
"$kretaIdp/Account/Login?ReturnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Fredirect_uri%3Dhttps%253A%252F%252Fmobil.e-kreta.hu%252Fellenorzo-student%252Fprod%252Foauthredirect%26client_id%3D$clientId%26response_type%3Dcode%26login_hint%3D$username%26prompt%3Dlogin%26state%3D$stateOrNonce%26nonce%3D$stateOrNonce%26scope%3Dopenid%2520email%2520offline_access%2520kreta-ellenorzo-webapi.public%2520kreta-eugyintezes-webapi.public%2520kreta-fileservice-webapi.public%2520kreta-mobile-global-webapi.public%2520kreta-dkt-webapi.public%2520kreta-ier-webapi.public%26code_challenge%3D$_codeChallenge%26code_challenge_method%3DS256%26institute_code%3D$schoolId%26suppressed_prompt%3Dlogin";
|
"$kretaIdp/Account/Login?ReturnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Fredirect_uri%3Dhttps%253A%252F%252Fmobil.e-kreta.hu%252Fellenorzo-student%252Fprod%252Foauthredirect%26client_id%3D$clientId%26response_type%3Dcode%26login_hint%3D$username%26prompt%3Dlogin%26state%3D$stateOrNonce%26nonce%3D$stateOrNonce%26scope%3Dopenid%2520email%2520offline_access%2520kreta-ellenorzo-webapi.public%2520kreta-eugyintezes-webapi.public%2520kreta-fileservice-webapi.public%2520kreta-mobile-global-webapi.public%2520kreta-dkt-webapi.public%2520kreta-ier-webapi.public%26code_challenge%3D$_codeChallenge%26code_challenge_method%3DS256%26institute_code%3D$schoolId%26suppressed_prompt%3Dlogin";
|
||||||
static String tokenGrantUrl = "$kretaIdp/connect/token";
|
static String tokenGrantUrl = "$kretaIdp/connect/token";
|
||||||
|
|
||||||
static String getStudentUrl(String iss) =>
|
|
||||||
ka.KretaEndpoints.getStudentUrl(iss);
|
|
||||||
|
|
||||||
static String getClassGroups(String iss) =>
|
|
||||||
ka.KretaEndpoints.getClassGroups(iss);
|
|
||||||
|
|
||||||
static String getNoticeBoard(String iss) =>
|
|
||||||
ka.KretaEndpoints.getNoticeBoard(iss);
|
|
||||||
|
|
||||||
static String getInfoBoard(String iss) => ka.KretaEndpoints.getInfoBoard(iss);
|
|
||||||
|
|
||||||
static String getGrades(String iss) => ka.KretaEndpoints.getGrades(iss);
|
|
||||||
|
|
||||||
static String getSubjectAvg(String iss, String studyGroupId) =>
|
|
||||||
ka.KretaEndpoints.getSubjectAvg(iss, studyGroupId);
|
|
||||||
|
|
||||||
static String getTimeTable(String iss) => ka.KretaEndpoints.getTimeTable(iss);
|
|
||||||
|
|
||||||
static String getOmissions(String iss) => ka.KretaEndpoints.getOmissions(iss);
|
|
||||||
|
|
||||||
static String getHomework(String iss) => ka.KretaEndpoints.getHomework(iss);
|
|
||||||
|
|
||||||
static String getTests(String iss) => ka.KretaEndpoints.getTests(iss);
|
|
||||||
|
|
||||||
static String getLessons(String iss) => ka.KretaEndpoints.getLessons(iss);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:firka/data/models/token_model.dart';
|
import 'package:firka/data/models/token_model.dart';
|
||||||
import 'package:kreta_api/kreta_api.dart' hide KretaEndpoints;
|
|
||||||
|
|
||||||
import 'package:firka/app/app_state.dart';
|
import 'package:firka/app/app_state.dart';
|
||||||
|
import 'package:kreta_api/kreta_api.dart';
|
||||||
import 'consts.dart';
|
import 'consts.dart';
|
||||||
|
|
||||||
Future<TokenGrantResponse> getAccessToken(String code) async {
|
Future<TokenGrantResponse> getAccessToken(String code) async {
|
||||||
@@ -14,7 +14,7 @@ Future<TokenGrantResponse> getAccessToken(String code) async {
|
|||||||
|
|
||||||
final formData = <String, String>{
|
final formData = <String, String>{
|
||||||
"code": code,
|
"code": code,
|
||||||
"code_verifier": KretaEndpoints.codeVerifier,
|
"code_verifier": KretaLoginEndpoints.codeVerifier,
|
||||||
"redirect_uri":
|
"redirect_uri":
|
||||||
"https://mobil.e-kreta.hu/ellenorzo-student/prod/oauthredirect",
|
"https://mobil.e-kreta.hu/ellenorzo-student/prod/oauthredirect",
|
||||||
"client_id": Constants.clientId,
|
"client_id": Constants.clientId,
|
||||||
@@ -23,7 +23,7 @@ Future<TokenGrantResponse> getAccessToken(String code) async {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await dio.post(
|
final response = await dio.post(
|
||||||
KretaEndpoints.tokenGrantUrl,
|
KretaLoginEndpoints.tokenGrantUrl,
|
||||||
options: Options(headers: headers),
|
options: Options(headers: headers),
|
||||||
data: formData,
|
data: formData,
|
||||||
);
|
);
|
||||||
@@ -76,7 +76,7 @@ Future<TokenGrantResponse> extendToken(TokenModel model) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final response = await dio.post(
|
final response = await dio.post(
|
||||||
KretaEndpoints.tokenGrantUrl,
|
KretaLoginEndpoints.tokenGrantUrl,
|
||||||
options: Options(headers: headers),
|
options: Options(headers: headers),
|
||||||
data: formData,
|
data: formData,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import 'package:kreta_api/kreta_api.dart';
|
|
||||||
|
|
||||||
double calculateAverage(List<Grade> sortedGrades) {
|
|
||||||
double totalWeight = 0.0;
|
|
||||||
double weightedSum = 0.0;
|
|
||||||
|
|
||||||
for (final grade in sortedGrades) {
|
|
||||||
final value = grade.numericValue;
|
|
||||||
final weight = grade.weightPercentage;
|
|
||||||
|
|
||||||
if (value != null && weight != null) {
|
|
||||||
weightedSum += value * weight;
|
|
||||||
totalWeight += weight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalWeight == 0) {
|
|
||||||
return double.parse(0.0.toStringAsFixed(2));
|
|
||||||
}
|
|
||||||
|
|
||||||
final avg = weightedSum / totalWeight;
|
|
||||||
return double.parse(avg.toStringAsFixed(2));
|
|
||||||
}
|
|
||||||
@@ -14,10 +14,9 @@ extension TimetableExtension on Iterable<Lesson> {
|
|||||||
for (var lesson in this) {
|
for (var lesson in this) {
|
||||||
if (lesson.lessonNumber == null) continue;
|
if (lesson.lessonNumber == null) continue;
|
||||||
|
|
||||||
if (lessons.firstWhereOrNull(
|
if (!lessons.any(
|
||||||
(lesson2) => lesson.lessonNumber == lesson2.lessonNumber,
|
(lesson2) => lesson.lessonNumber == lesson2.lessonNumber,
|
||||||
) ==
|
)) {
|
||||||
null) {
|
|
||||||
final ref = reference.start;
|
final ref = reference.start;
|
||||||
final newStart = DateTime(
|
final newStart = DateTime(
|
||||||
ref.year,
|
ref.year,
|
||||||
@@ -69,10 +68,20 @@ extension DurationExtension on Duration {
|
|||||||
String seconds = inSeconds.remainder(60).toString().padLeft(2, '0');
|
String seconds = inSeconds.remainder(60).toString().padLeft(2, '0');
|
||||||
return "$hours:$minutes:$seconds";
|
return "$hours:$minutes:$seconds";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? timeLeft(AppLocalizations l10n) {
|
||||||
|
return inMinutes > 0
|
||||||
|
? "${inMinutes + 1} ${l10n.starting_min_plural}"
|
||||||
|
: inSeconds > 0
|
||||||
|
? "$inSeconds ${inSeconds == 1 ? l10n.starting_sec : l10n.starting_sec_plural}"
|
||||||
|
: null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum FormatMode {
|
enum FormatMode {
|
||||||
yearly,
|
yearly,
|
||||||
|
mmmd,
|
||||||
|
main,
|
||||||
grades,
|
grades,
|
||||||
welcome,
|
welcome,
|
||||||
hmm,
|
hmm,
|
||||||
@@ -87,8 +96,22 @@ enum FormatMode {
|
|||||||
|
|
||||||
enum Cycle { morning, day, afternoon, night }
|
enum Cycle { morning, day, afternoon, night }
|
||||||
|
|
||||||
|
extension ComparableExtension<T extends Comparable<T>> on Comparable<T> {
|
||||||
|
T min(T other) {
|
||||||
|
return compareTo(other) < 0 ? this as T : other;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isBetween(T from, T to) {
|
||||||
|
return compareTo(from) > 0 && compareTo(to) < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
T max(T other) {
|
||||||
|
return compareTo(other) > 0 ? this as T : other;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension DateExtension on DateTime {
|
extension DateExtension on DateTime {
|
||||||
String format(AppLocalizations l10n, FormatMode mode) {
|
String? translatedDay(AppLocalizations l10n) {
|
||||||
var today = timeNow().getMidnight();
|
var today = timeNow().getMidnight();
|
||||||
|
|
||||||
var tomorrowLim = today.add(Duration(days: 2));
|
var tomorrowLim = today.add(Duration(days: 2));
|
||||||
@@ -96,32 +119,43 @@ extension DateExtension on DateTime {
|
|||||||
var yesterday = today.subtract(Duration(days: 1));
|
var yesterday = today.subtract(Duration(days: 1));
|
||||||
var yesterdayLim = today.subtract(Duration(days: 2));
|
var yesterdayLim = today.subtract(Duration(days: 2));
|
||||||
|
|
||||||
var weekStart = subtract(Duration(days: weekday - 1));
|
if (isAfter(yesterdayLim) && isBefore(today)) {
|
||||||
|
return l10n.yesterday;
|
||||||
|
}
|
||||||
|
if (isAfter(yesterday) && isBefore(tomorrow)) {
|
||||||
|
return l10n.today;
|
||||||
|
}
|
||||||
|
if (isAfter(today) && isBefore(tomorrowLim)) {
|
||||||
|
return l10n.tomorrow;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String format(AppLocalizations l10n, FormatMode mode) {
|
||||||
|
var weekStart = getMonday();
|
||||||
var weekEnd = weekStart.add(Duration(days: 6));
|
var weekEnd = weekStart.add(Duration(days: 6));
|
||||||
|
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
|
case FormatMode.main:
|
||||||
|
var lastWeek = timeNow().getMidnight().subtract(Duration(days: 8));
|
||||||
|
var isLastWeek =
|
||||||
|
lastWeek.millisecondsSinceEpoch <
|
||||||
|
getMidnight().millisecondsSinceEpoch;
|
||||||
|
final dayName = DateFormat(
|
||||||
|
'EEEE',
|
||||||
|
l10n.localeName,
|
||||||
|
).format(this).firstUpper();
|
||||||
|
return translatedDay(l10n) ??
|
||||||
|
(isLastWeek
|
||||||
|
? "$dayName (${format(l10n, FormatMode.mmmd).firstUpper()})"
|
||||||
|
: "${format(l10n, FormatMode.yearly).firstUpper()} ($dayName)");
|
||||||
case FormatMode.grades:
|
case FormatMode.grades:
|
||||||
if (isBefore(yesterdayLim)) {
|
return translatedDay(l10n) ?? format(l10n, FormatMode.yearly);
|
||||||
final month = DateFormat(
|
|
||||||
'MMMM',
|
|
||||||
l10n.localeName,
|
|
||||||
).format(this).firstUpper();
|
|
||||||
final day = DateFormat('d', l10n.localeName).format(this);
|
|
||||||
return "$month $day";
|
|
||||||
}
|
|
||||||
if (isAfter(yesterdayLim) && isBefore(today)) {
|
|
||||||
return l10n.yesterday;
|
|
||||||
}
|
|
||||||
if (isAfter(yesterday) && isBefore(tomorrow)) {
|
|
||||||
return l10n.today;
|
|
||||||
}
|
|
||||||
if (isAfter(today) && isBefore(tomorrowLim)) {
|
|
||||||
return l10n.tomorrow;
|
|
||||||
}
|
|
||||||
|
|
||||||
return format(l10n, FormatMode.yearly);
|
|
||||||
case FormatMode.yearly:
|
case FormatMode.yearly:
|
||||||
return DateFormat('MMMM dd', l10n.localeName).format(this);
|
return DateFormat('MMMM d', l10n.localeName).format(this);
|
||||||
|
case FormatMode.mmmd:
|
||||||
|
return DateFormat('MMM d', l10n.localeName).format(this);
|
||||||
case FormatMode.hmm:
|
case FormatMode.hmm:
|
||||||
return DateFormat('H:mm', l10n.localeName).format(this);
|
return DateFormat('H:mm', l10n.localeName).format(this);
|
||||||
case FormatMode.welcome:
|
case FormatMode.welcome:
|
||||||
@@ -155,7 +189,10 @@ extension DateExtension on DateTime {
|
|||||||
case FormatMode.yyyymmdd:
|
case FormatMode.yyyymmdd:
|
||||||
return DateFormat('yyyy. MM. dd.', l10n.localeName).format(this);
|
return DateFormat('yyyy. MM. dd.', l10n.localeName).format(this);
|
||||||
case FormatMode.yyyymmddhhmmss:
|
case FormatMode.yyyymmddhhmmss:
|
||||||
return DateFormat('yyyy-MM-dd hh:mm:ss', l10n.localeName).format(this);
|
return DateFormat(
|
||||||
|
'yyyy. MM. dd. H:mm:ss',
|
||||||
|
l10n.localeName,
|
||||||
|
).format(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,55 +244,9 @@ extension DateGrouper<T> on Iterable<T> {
|
|||||||
Map<DateTime, List<T>> groupList(DateTime Function(T elem) getDate) {
|
Map<DateTime, List<T>> groupList(DateTime Function(T elem) getDate) {
|
||||||
Map<DateTime, List<T>> newList = {};
|
Map<DateTime, List<T>> newList = {};
|
||||||
|
|
||||||
var today = timeNow();
|
|
||||||
today = today.subtract(
|
|
||||||
Duration(
|
|
||||||
hours: today.hour,
|
|
||||||
minutes: today.minute,
|
|
||||||
seconds: today.second,
|
|
||||||
milliseconds: today.millisecond,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
var tomorrow = today.add(Duration(days: 1));
|
|
||||||
var yesterday = today.subtract(Duration(days: 1));
|
|
||||||
|
|
||||||
for (var elem in this) {
|
for (var elem in this) {
|
||||||
var date = getDate(elem);
|
var date = getDate(elem);
|
||||||
var day = date.subtract(
|
var day = date.getMidnight();
|
||||||
Duration(
|
|
||||||
hours: date.hour,
|
|
||||||
minutes: date.minute,
|
|
||||||
seconds: date.second,
|
|
||||||
milliseconds: date.millisecond,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (date.isAfter(tomorrow.add(Duration(days: 1)))) {
|
|
||||||
if (newList[day] == null) {
|
|
||||||
newList[day] = List<T>.empty(growable: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
newList[day]!.add(elem);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (date.isAfter(today)) {
|
|
||||||
if (newList[tomorrow] == null) {
|
|
||||||
newList[tomorrow] = List<T>.empty(growable: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
newList[tomorrow]!.add(elem);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (date.isAfter(yesterday.subtract(Duration(days: 1))) &&
|
|
||||||
date.isBefore(today)) {
|
|
||||||
if (newList[yesterday] == null) {
|
|
||||||
newList[yesterday] = List<T>.empty(growable: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
newList[yesterday]!.add(elem);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newList[day] == null) {
|
if (newList[day] == null) {
|
||||||
newList[day] = List<T>.empty(growable: true);
|
newList[day] = List<T>.empty(growable: true);
|
||||||
@@ -280,7 +271,7 @@ extension LessonExtension on List<Lesson> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Lesson? getPrevLesson(DateTime now) {
|
Lesson? getPrevLesson(DateTime now) {
|
||||||
return firstWhereOrNull(
|
return reversed.firstWhereOrNull(
|
||||||
(lesson) => lesson.end.isBefore(now.add(Duration(milliseconds: 1))),
|
(lesson) => lesson.end.isBefore(now.add(Duration(milliseconds: 1))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ enum CacheId {
|
|||||||
getSubjectAvg,
|
getSubjectAvg,
|
||||||
getLessons,
|
getLessons,
|
||||||
getHomework,
|
getHomework,
|
||||||
|
getClassGroupAvg,
|
||||||
}
|
}
|
||||||
|
|
||||||
@collection
|
@collection
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:isar_community/isar.dart';
|
import 'package:isar_community/isar.dart';
|
||||||
|
|
||||||
import 'package:firka/core/debug_helper.dart';
|
import 'package:firka/core/debug_helper.dart';
|
||||||
|
|
||||||
class DatedCacheEntry {
|
class DatedCacheEntry {
|
||||||
Id? cacheKey;
|
Id? cacheKey;
|
||||||
List<String>? values;
|
late List<String> values;
|
||||||
}
|
}
|
||||||
|
|
||||||
int genCacheKey(DateTime date, int studentId) {
|
int genCacheKey(DateTime date, int studentId) {
|
||||||
@@ -17,10 +16,9 @@ int genCacheKey(DateTime date, int studentId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DateTime getDate(int key) {
|
DateTime getDate(int key) {
|
||||||
var currentDate = timeNow();
|
|
||||||
var md = key ~/ pow(10, 11);
|
var md = key ~/ pow(10, 11);
|
||||||
var month = md ~/ pow(10, 2);
|
var month = md ~/ pow(10, 2);
|
||||||
var day = md - month * pow(10, 2);
|
var day = (md - month * pow(10, 2)) as int;
|
||||||
|
|
||||||
return DateFormat("yyyy-M-d").parse("${currentDate.year}-$month-$day");
|
return DateTime(timeNow().year, month, day);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import 'dart:collection';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:firka/api/client/kreta_client.dart';
|
import 'package:firka/api/client/kreta_client.dart';
|
||||||
|
import 'package:firka_common/ui/components/grade_helpers.dart';
|
||||||
import 'package:kreta_api/kreta_api.dart';
|
import 'package:kreta_api/kreta_api.dart';
|
||||||
import 'package:firka/core/debug_helper.dart';
|
import 'package:firka/core/debug_helper.dart';
|
||||||
import 'package:firka/data/ios_widget_helper.dart';
|
import 'package:firka/data/ios_widget_helper.dart';
|
||||||
@@ -249,31 +251,20 @@ class WidgetCacheHelper {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final Map<String, double> subjectAverages = {};
|
final Map<String, double> subjectAverages = {};
|
||||||
final Set<String> subjectUids = {};
|
final HashSet<Subject> subjects = HashSet(
|
||||||
|
hashCode: (s) => s.uid.hashCode,
|
||||||
|
equals: (s, s2) => s.uid == s2.uid,
|
||||||
|
);
|
||||||
|
|
||||||
for (var grade in grades) {
|
subjects.addAll(grades.map((g) => g.subject));
|
||||||
subjectUids.add(grade.subject.uid);
|
|
||||||
}
|
|
||||||
|
|
||||||
double overallSum = 0;
|
for (var subject in subjects) {
|
||||||
int validSubjectCount = 0;
|
final average = grades.getAverageBySubject(subject);
|
||||||
|
if (average != null) {
|
||||||
for (var uid in subjectUids) {
|
subjectAverages[subject.uid] = average;
|
||||||
final subjectGrades = grades
|
|
||||||
.where((g) => g.subject.uid == uid)
|
|
||||||
.toList();
|
|
||||||
final avg = _calculateWeightedAverage(subjectGrades);
|
|
||||||
if (!avg.isNaN && avg > 0) {
|
|
||||||
subjectAverages[uid] = avg;
|
|
||||||
overallSum += avg;
|
|
||||||
validSubjectCount++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final double? overallAverage = validSubjectCount > 0
|
|
||||||
? overallSum / validSubjectCount
|
|
||||||
: null;
|
|
||||||
|
|
||||||
WidgetBreakInfo? currentBreak;
|
WidgetBreakInfo? currentBreak;
|
||||||
|
|
||||||
await updateIOSWidgets(
|
await updateIOSWidgets(
|
||||||
@@ -285,7 +276,7 @@ class WidgetCacheHelper {
|
|||||||
nextSchoolDayDate: nextSchoolDayDate,
|
nextSchoolDayDate: nextSchoolDayDate,
|
||||||
grades: grades,
|
grades: grades,
|
||||||
subjectAverages: subjectAverages,
|
subjectAverages: subjectAverages,
|
||||||
overallAverage: overallAverage,
|
overallAverage: grades.getSubjectAverage(),
|
||||||
currentBreak: currentBreak,
|
currentBreak: currentBreak,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -315,21 +306,4 @@ class WidgetCacheHelper {
|
|||||||
debugPrint('Error clearing iOS widgets: $e');
|
debugPrint('Error clearing iOS widgets: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate weighted average for a list of grades
|
|
||||||
static double _calculateWeightedAverage(List<Grade> grades) {
|
|
||||||
var weightTotal = 0.0;
|
|
||||||
var sum = 0.0;
|
|
||||||
|
|
||||||
for (var grade in grades) {
|
|
||||||
if (grade.numericValue != null) {
|
|
||||||
var weight = (grade.weightPercentage ?? 100) / 100.0;
|
|
||||||
weightTotal += weight;
|
|
||||||
sum += grade.numericValue! * weight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (weightTotal == 0) return double.nan;
|
|
||||||
return sum / weightTotal;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Submodule firka/lib/l10n updated: c65b8073ca...d489003d32
@@ -24,6 +24,16 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:kreta_api/kreta_api.dart';
|
import 'package:kreta_api/kreta_api.dart';
|
||||||
|
|
||||||
GoRouter createAppRouter() {
|
GoRouter createAppRouter() {
|
||||||
|
final subjectRoute = GoRoute(
|
||||||
|
path: 'subject',
|
||||||
|
builder: (context, state) {
|
||||||
|
return DefaultAssetBundle(
|
||||||
|
bundle: FirkaBundle(),
|
||||||
|
child: HomeGradesSubjectScreen(state.extra as Subject, initData),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return GoRouter(
|
return GoRouter(
|
||||||
navigatorKey: navigatorKey,
|
navigatorKey: navigatorKey,
|
||||||
initialLocation: _initialLocation,
|
initialLocation: _initialLocation,
|
||||||
@@ -84,7 +94,7 @@ GoRouter createAppRouter() {
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/message',
|
path: '/message',
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final info = state.extra as InfoBoardItem?;
|
final info = state.extra as MessageItem?;
|
||||||
if (info == null) {
|
if (info == null) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
@@ -124,19 +134,7 @@ GoRouter createAppRouter() {
|
|||||||
child: HomeMainScreen(initData),
|
child: HomeMainScreen(initData),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
routes: [
|
routes: [subjectRoute],
|
||||||
GoRoute(
|
|
||||||
path: 'subject/:uid',
|
|
||||||
builder: (context, state) {
|
|
||||||
final uid = state.pathParameters['uid'] ?? '';
|
|
||||||
activeSubjectUid = uid;
|
|
||||||
return DefaultAssetBundle(
|
|
||||||
bundle: FirkaBundle(),
|
|
||||||
child: HomeGradesSubjectScreen(initData),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -151,19 +149,7 @@ GoRouter createAppRouter() {
|
|||||||
child: HomeGradesScreen(initData),
|
child: HomeGradesScreen(initData),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
routes: [
|
routes: [subjectRoute],
|
||||||
GoRoute(
|
|
||||||
path: 'subject/:uid',
|
|
||||||
builder: (context, state) {
|
|
||||||
final uid = state.pathParameters['uid'] ?? '';
|
|
||||||
activeSubjectUid = uid;
|
|
||||||
return DefaultAssetBundle(
|
|
||||||
bundle: FirkaBundle(),
|
|
||||||
child: HomeGradesSubjectScreen(initData),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -186,17 +172,7 @@ GoRouter createAppRouter() {
|
|||||||
child: HomeTimetableMonthlyScreen(initData),
|
child: HomeTimetableMonthlyScreen(initData),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
subjectRoute,
|
||||||
path: 'subject/:uid',
|
|
||||||
builder: (context, state) {
|
|
||||||
final uid = state.pathParameters['uid'] ?? '';
|
|
||||||
activeSubjectUid = uid;
|
|
||||||
return DefaultAssetBundle(
|
|
||||||
bundle: FirkaBundle(),
|
|
||||||
child: HomeGradesSubjectScreen(initData),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -41,57 +41,33 @@ class ShellWithNavBar extends StatelessWidget {
|
|||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(55, 0, 55, 12),
|
padding: const EdgeInsets.fromLTRB(55, 16, 55, 12),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
BottomNavIconWidget(
|
...[
|
||||||
() {
|
(data.l10n.home, Majesticon.homeSolid, Majesticon.homeLine),
|
||||||
if (currentIndex != 0) {
|
(
|
||||||
navigationShell.goBranch(0);
|
data.l10n.grades,
|
||||||
}
|
Majesticon.bookmarkSolid,
|
||||||
},
|
Majesticon.bookmarkLine,
|
||||||
currentIndex == 0,
|
),
|
||||||
currentIndex == 0
|
(
|
||||||
? Majesticon.homeSolid
|
data.l10n.timetable,
|
||||||
: Majesticon.homeLine,
|
Majesticon.calendarSolid,
|
||||||
data.l10n.home,
|
Majesticon.calendarLine,
|
||||||
currentIndex == 0
|
),
|
||||||
? appStyle.colors.accent
|
].indexed.map(
|
||||||
: appStyle.colors.secondary,
|
(nav) => BottomNavIconWidget(
|
||||||
appStyle.colors.textPrimary,
|
() {
|
||||||
),
|
if (currentIndex != nav.$1) {
|
||||||
BottomNavIconWidget(
|
navigationShell.goBranch(nav.$1);
|
||||||
() {
|
}
|
||||||
if (currentIndex != 1) {
|
},
|
||||||
navigationShell.goBranch(1);
|
currentIndex == nav.$1,
|
||||||
}
|
currentIndex == nav.$1 ? nav.$2.$2 : nav.$2.$3,
|
||||||
},
|
nav.$2.$1,
|
||||||
currentIndex == 1,
|
),
|
||||||
currentIndex == 1
|
|
||||||
? Majesticon.bookmarkSolid
|
|
||||||
: Majesticon.bookmarkLine,
|
|
||||||
data.l10n.grades,
|
|
||||||
currentIndex == 1
|
|
||||||
? appStyle.colors.accent
|
|
||||||
: appStyle.colors.secondary,
|
|
||||||
appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
BottomNavIconWidget(
|
|
||||||
() {
|
|
||||||
if (currentIndex != 2) {
|
|
||||||
navigationShell.goBranch(2);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
currentIndex == 2,
|
|
||||||
currentIndex == 2
|
|
||||||
? Majesticon.calendarSolid
|
|
||||||
: Majesticon.calendarLine,
|
|
||||||
data.l10n.timetable,
|
|
||||||
currentIndex == 2
|
|
||||||
? appStyle.colors.accent
|
|
||||||
: appStyle.colors.secondary,
|
|
||||||
appStyle.colors.textPrimary,
|
|
||||||
),
|
),
|
||||||
BottomNavIconWidget(
|
BottomNavIconWidget(
|
||||||
() {
|
() {
|
||||||
@@ -102,8 +78,6 @@ class ShellWithNavBar extends StatelessWidget {
|
|||||||
? data.profilePicture!
|
? data.profilePicture!
|
||||||
: Majesticon.menuLine,
|
: Majesticon.menuLine,
|
||||||
data.l10n.other,
|
data.l10n.other,
|
||||||
appStyle.colors.secondary,
|
|
||||||
appStyle.colors.textPrimary,
|
|
||||||
isProfilePicture: data.profilePicture != null,
|
isProfilePicture: data.profilePicture != null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1581,12 +1581,12 @@ class LiveActivityService {
|
|||||||
final emptyType = NameUidDesc(
|
final emptyType = NameUidDesc(
|
||||||
uid: 'placeholder',
|
uid: 'placeholder',
|
||||||
name: 'Placeholder',
|
name: 'Placeholder',
|
||||||
description: null,
|
description: '',
|
||||||
);
|
);
|
||||||
final emptyState = NameUidDesc(
|
final emptyState = NameUidDesc(
|
||||||
uid: 'active',
|
uid: 'active',
|
||||||
name: 'Active',
|
name: 'Active',
|
||||||
description: null,
|
description: '',
|
||||||
);
|
);
|
||||||
|
|
||||||
placeholderLesson = Lesson(
|
placeholderLesson = Lesson(
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,7 @@
|
|||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
|
import 'package:firka/core/extensions.dart';
|
||||||
import 'package:kreta_api/kreta_api.dart';
|
import 'package:kreta_api/kreta_api.dart';
|
||||||
import 'package:firka/core/average_helper.dart';
|
|
||||||
import 'package:firka/ui/components/firka_card.dart';
|
import 'package:firka/ui/components/firka_card.dart';
|
||||||
import 'package:firka/ui/components/grade_helpers.dart';
|
import 'package:firka/ui/components/grade_helpers.dart';
|
||||||
import 'package:firka/ui/phone/widgets/grade_chart.dart';
|
import 'package:firka/ui/phone/widgets/grade_chart.dart';
|
||||||
@@ -27,17 +29,12 @@ class HomeGradesScreen extends StatefulWidget {
|
|||||||
State<StatefulWidget> createState() => _HomeGradesScreen();
|
State<StatefulWidget> createState() => _HomeGradesScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
String activeSubjectUid = "";
|
|
||||||
String subjectName = "";
|
|
||||||
String subjectId = "";
|
|
||||||
String subjectCategory = "";
|
|
||||||
List<Subject> subjectInfo = [];
|
|
||||||
|
|
||||||
class _HomeGradesScreen extends FirkaState<HomeGradesScreen> {
|
class _HomeGradesScreen extends FirkaState<HomeGradesScreen> {
|
||||||
ApiResponse<List<Grade>>? grades;
|
ApiResponse<List<Grade>>? grades;
|
||||||
ApiResponse<List<Lesson>>? week;
|
ApiResponse<List<Lesson>>? week;
|
||||||
ApiResponse<List<ClassGroup>>? classGroups;
|
ApiResponse<List<ClassGroup>>? classGroups;
|
||||||
ApiResponse<List<SubjectAverage>>? lessons;
|
ApiResponse<List<SubjectAverage>>? lessons;
|
||||||
|
ApiResponse<List<ClassGroupSubjectAverage>>? classAvgs;
|
||||||
|
|
||||||
void _onRefreshRequested(BuildContext context) async {
|
void _onRefreshRequested(BuildContext context) async {
|
||||||
final cubit = context.read<HomeRefreshCubit>();
|
final cubit = context.read<HomeRefreshCubit>();
|
||||||
@@ -54,6 +51,10 @@ class _HomeGradesScreen extends FirkaState<HomeGradesScreen> {
|
|||||||
group,
|
group,
|
||||||
forceCache: false,
|
forceCache: false,
|
||||||
);
|
);
|
||||||
|
classAvgs = await widget.data.client.getClassGroupAverages(
|
||||||
|
group,
|
||||||
|
forceCache: false,
|
||||||
|
);
|
||||||
await Future.delayed(Duration(milliseconds: 100));
|
await Future.delayed(Duration(milliseconds: 100));
|
||||||
}
|
}
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -77,6 +78,7 @@ class _HomeGradesScreen extends FirkaState<HomeGradesScreen> {
|
|||||||
if (classGroups?.response?.isNotEmpty ?? false) {
|
if (classGroups?.response?.isNotEmpty ?? false) {
|
||||||
var group = classGroups!.response!.first;
|
var group = classGroups!.response!.first;
|
||||||
lessons = await widget.data.client.getSubjectAverage(group);
|
lessons = await widget.data.client.getSubjectAverage(group);
|
||||||
|
classAvgs = await widget.data.client.getClassGroupAverages(group);
|
||||||
await Future.delayed(Duration(milliseconds: 100));
|
await Future.delayed(Duration(milliseconds: 100));
|
||||||
}
|
}
|
||||||
if (mounted) setState(() {});
|
if (mounted) setState(() {});
|
||||||
@@ -96,7 +98,10 @@ class _HomeGradesScreen extends FirkaState<HomeGradesScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildContent(BuildContext context) {
|
Widget _buildContent(BuildContext context) {
|
||||||
if (grades == null || week == null) {
|
if (grades == null ||
|
||||||
|
lessons == null ||
|
||||||
|
classAvgs == null ||
|
||||||
|
week == null) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: MediaQuery.of(context).size.height / 1.35,
|
height: MediaQuery.of(context).size.height / 1.35,
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -105,133 +110,46 @@ class _HomeGradesScreen extends FirkaState<HomeGradesScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
var subjectAvg = 0.00;
|
final allGrades = grades!.response!;
|
||||||
var subjectCount = 0;
|
final allLessons = lessons!.response!;
|
||||||
var subjectAvgRounded = 0.00;
|
|
||||||
final summaryAvg2 = calculateAverage(grades!.response!);
|
final subjectAverage = allGrades.getSubjectAverage();
|
||||||
final List<Subject> subjects = List<Subject>.empty(growable: true);
|
final roundedSubjectAverage = allGrades.getRoundedSubjectAverage();
|
||||||
|
final classAverages = classAvgs!.response!
|
||||||
|
.map((c) => c.classGroupAverage)
|
||||||
|
.nonNulls;
|
||||||
|
|
||||||
|
double? classAverage = classAverages.isNotEmpty
|
||||||
|
? classAverages.reduce((f, s) => f + s) / classAverages.length
|
||||||
|
: null;
|
||||||
|
|
||||||
|
final Set<Subject> subjects = HashSet(
|
||||||
|
hashCode: (s) => s.uid.hashCode,
|
||||||
|
equals: (s, s2) => s.uid == s2.uid,
|
||||||
|
);
|
||||||
final List<Widget> gradeCards = [];
|
final List<Widget> gradeCards = [];
|
||||||
|
|
||||||
for (var grade in grades!.response!) {
|
allGrades.map((g) => g.subject).forEach(subjects.add);
|
||||||
if (subjects.where((s) => s.uid == grade.subject.uid).isEmpty) {
|
allLessons.map((l) => l.subject).forEach(subjects.add);
|
||||||
subjects.add(grade.subject);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lessons != null && lessons!.response != null) {
|
for (var subject
|
||||||
for (var lesson in lessons!.response!) {
|
in subjects.toList()..sort((s1, s2) => s1.name.compareTo(s2.name))) {
|
||||||
if (subjects.where((s) => s.uid == lesson.uid).isEmpty) {
|
gradeCards.add(
|
||||||
subjects.add(
|
GestureDetector(
|
||||||
Subject(
|
child: GradeSmallCard(
|
||||||
uid: lesson.uid,
|
allGrades,
|
||||||
name: lesson.name,
|
classAvgs!.response!
|
||||||
category: NameUidDesc(
|
.firstWhereOrNull((s) => s.subject.uid == subject.uid)
|
||||||
uid: lesson.subjectCategoryId,
|
?.classGroupAverage,
|
||||||
name: lesson.subjectCategoryName,
|
subject,
|
||||||
description: lesson.subjectCategoryDescription,
|
|
||||||
),
|
|
||||||
sortIndex: lesson.sortIndex,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
subjects.sort((s1, s2) => s1.name.compareTo(s2.name));
|
|
||||||
|
|
||||||
for (var subject in subjects) {
|
|
||||||
final subjectGrades = grades!.response!
|
|
||||||
.where((g) => g.subject.uid == subject.uid)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
double avg = double.nan;
|
|
||||||
if (subjectGrades.isNotEmpty) {
|
|
||||||
for (var grade in subjectGrades) {
|
|
||||||
if (grade.valueType.name == "Szazalekos") {
|
|
||||||
grade.valueType = NameUidDesc(
|
|
||||||
uid: "1,Osztalyzat",
|
|
||||||
name: "Osztalyzat",
|
|
||||||
description: "",
|
|
||||||
);
|
|
||||||
if (grade.numericValue != null) {
|
|
||||||
grade.numericValue = percentageToGrade(grade.numericValue!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
avg = grades!.response!.getAverageBySubject(subject);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (avg.isNaN) {
|
|
||||||
gradeCards.add(
|
|
||||||
GestureDetector(
|
|
||||||
child: GradeSmallCard(grades!.response!, subject),
|
|
||||||
onTap: () {
|
|
||||||
activeSubjectUid = subject.uid;
|
|
||||||
subjectName = subject.name;
|
|
||||||
subjectId = subject.uid;
|
|
||||||
subjectCategory = subject.category.name!;
|
|
||||||
subjectInfo = subjects
|
|
||||||
.where((s) => s.uid == subject.uid)
|
|
||||||
.toList();
|
|
||||||
context.go('/grades/subject/${subject.uid}');
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
onTap: () {
|
||||||
} else {
|
context.go('/grades/subject', extra: subject);
|
||||||
gradeCards.add(
|
},
|
||||||
GestureDetector(
|
),
|
||||||
child: GradeSmallCard(grades!.response!, subject),
|
);
|
||||||
onTap: () {
|
|
||||||
activeSubjectUid = subject.uid;
|
|
||||||
subjectName = subject.name;
|
|
||||||
subjectId = subject.uid;
|
|
||||||
subjectCategory = subject.category.name!;
|
|
||||||
subjectInfo = subjects
|
|
||||||
.where((s) => s.uid == subject.uid)
|
|
||||||
.toList();
|
|
||||||
context.go('/grades/subject/${subject.uid}');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!avg.isNaN) {
|
|
||||||
subjectCount++;
|
|
||||||
subjectAvg += avg;
|
|
||||||
final rounding = widget.data.settings
|
|
||||||
.group("settings")
|
|
||||||
.subGroup("application")
|
|
||||||
.subGroup("rounding");
|
|
||||||
subjectAvgRounded += roundGrade(
|
|
||||||
avg,
|
|
||||||
t1: rounding.dbl("1"),
|
|
||||||
t2: rounding.dbl("2"),
|
|
||||||
t3: rounding.dbl("3"),
|
|
||||||
t4: rounding.dbl("4"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
subjectAvg /= subjectCount;
|
|
||||||
subjectAvgRounded /= subjectCount;
|
|
||||||
|
|
||||||
if (subjectCount == 0) {
|
|
||||||
subjectAvg = 0.00;
|
|
||||||
subjectAvgRounded = 0.00;
|
|
||||||
}
|
|
||||||
|
|
||||||
final rounding = widget.data.settings
|
|
||||||
.group("settings")
|
|
||||||
.subGroup("application")
|
|
||||||
.subGroup("rounding");
|
|
||||||
var subjectAvgColor = getGradeColor(
|
|
||||||
subjectAvg,
|
|
||||||
t1: rounding.dbl("1"),
|
|
||||||
t2: rounding.dbl("2"),
|
|
||||||
t3: rounding.dbl("3"),
|
|
||||||
t4: rounding.dbl("4"),
|
|
||||||
);
|
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(left: 20.0, right: 20.0, top: 12.0),
|
padding: const EdgeInsets.only(left: 20.0, right: 20.0, top: 12.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -247,13 +165,11 @@ class _HomeGradesScreen extends FirkaState<HomeGradesScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
GradeChartWithInteraction(grades: grades?.response ?? []),
|
SizedBox(height: 20),
|
||||||
SizedBox(height: 2),
|
GradeChartWithInteraction(grades: allGrades),
|
||||||
GradeSummaryBar(
|
SizedBox(height: 10),
|
||||||
grades: grades?.response ?? [],
|
GradeSummaryBar(grades: allGrades, l10n: widget.data.l10n),
|
||||||
l10n: widget.data.l10n,
|
SizedBox(height: 20),
|
||||||
),
|
|
||||||
SizedBox(height: 12),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
@@ -264,7 +180,11 @@ class _HomeGradesScreen extends FirkaState<HomeGradesScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 16),
|
SizedBox(height: 16),
|
||||||
...gradeCards,
|
Column(
|
||||||
|
spacing: 16,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: gradeCards,
|
||||||
|
),
|
||||||
SizedBox(height: 16),
|
SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
widget.data.l10n.data,
|
widget.data.l10n.data,
|
||||||
@@ -283,24 +203,25 @@ class _HomeGradesScreen extends FirkaState<HomeGradesScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
right: [
|
right: [
|
||||||
Card(
|
if (subjectAverage != null)
|
||||||
shadowColor: Colors.transparent,
|
Container(
|
||||||
color: subjectAvgColor.withAlpha(38),
|
width: 48,
|
||||||
child: Padding(
|
height: 26,
|
||||||
padding: EdgeInsets.only(
|
decoration: ShapeDecoration(
|
||||||
left: 8,
|
color: getGradeColor(subjectAverage).withAlpha(38),
|
||||||
right: 8,
|
shape: RoundedRectangleBorder(
|
||||||
top: 4,
|
borderRadius: BorderRadius.circular(12),
|
||||||
bottom: 4,
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Center(
|
||||||
subjectAvg.toStringAsFixed(2),
|
child: Text(
|
||||||
style: appStyle.fonts.B_16SB.apply(
|
subjectAverage.toStringAsFixed(2),
|
||||||
color: subjectAvgColor,
|
style: appStyle.fonts.B_16R.apply(
|
||||||
|
color: getGradeColor(subjectAverage),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
FirkaCard(
|
FirkaCard(
|
||||||
@@ -313,66 +234,63 @@ class _HomeGradesScreen extends FirkaState<HomeGradesScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
right: [
|
right: [
|
||||||
Card(
|
if (roundedSubjectAverage != null)
|
||||||
shadowColor: Colors.transparent,
|
Container(
|
||||||
color: subjectAvgColor.withAlpha(38),
|
width: 48,
|
||||||
child: Padding(
|
height: 26,
|
||||||
padding: EdgeInsets.only(
|
decoration: ShapeDecoration(
|
||||||
left: 8,
|
color: getGradeColor(
|
||||||
right: 8,
|
roundedSubjectAverage,
|
||||||
top: 4,
|
).withAlpha(38),
|
||||||
bottom: 4,
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Center(
|
||||||
subjectAvgRounded.toStringAsFixed(2),
|
child: Text(
|
||||||
style: appStyle.fonts.B_16SB.apply(
|
roundedSubjectAverage.toStringAsFixed(2),
|
||||||
color: subjectAvgColor,
|
style: appStyle.fonts.B_16R.apply(
|
||||||
|
color: getGradeColor(roundedSubjectAverage),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
FirkaCard(
|
if (classAverage != null)
|
||||||
left: [
|
FirkaCard(
|
||||||
Text(
|
left: [
|
||||||
"Összesített átlag",
|
Text(
|
||||||
style: appStyle.fonts.B_16SB.apply(
|
widget.data.l10n.class_avg,
|
||||||
color: appStyle.colors.textPrimary,
|
style: appStyle.fonts.B_16SB.apply(
|
||||||
),
|
color: appStyle.colors.textPrimary,
|
||||||
),
|
|
||||||
],
|
|
||||||
right: [
|
|
||||||
Card(
|
|
||||||
shadowColor: Colors.transparent,
|
|
||||||
color: subjectAvgColor.withAlpha(38),
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
left: 8,
|
|
||||||
right: 8,
|
|
||||||
top: 4,
|
|
||||||
bottom: 4,
|
|
||||||
),
|
),
|
||||||
child: Text(
|
),
|
||||||
summaryAvg2.toStringAsFixed(2),
|
],
|
||||||
style: appStyle.fonts.B_16SB.apply(
|
right: [
|
||||||
color: subjectAvgColor,
|
Container(
|
||||||
|
width: 48,
|
||||||
|
height: 26,
|
||||||
|
decoration: ShapeDecoration(
|
||||||
|
color: Colors.transparent,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
side: BorderSide(
|
||||||
|
color: getGradeColor(classAverage),
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
classAverage.toStringAsFixed(2),
|
||||||
|
style: appStyle.fonts.B_16R.apply(
|
||||||
|
color: getGradeColor(classAverage),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
|
||||||
FirkaCard(
|
|
||||||
left: [
|
|
||||||
Text(
|
|
||||||
widget.data.l10n.class_avg,
|
|
||||||
style: appStyle.fonts.B_16SB.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
FirkaCard(
|
FirkaCard(
|
||||||
left: [
|
left: [
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import 'package:firka/ui/phone/widgets/grade_summary_bar.dart';
|
import 'package:firka/ui/phone/widgets/grade_summary_bar.dart';
|
||||||
|
import 'package:firka/ui/phone/widgets/info_card.dart';
|
||||||
|
import 'package:firka_common/ui/components/filled_circle.dart';
|
||||||
import 'package:kreta_api/kreta_api.dart';
|
import 'package:kreta_api/kreta_api.dart';
|
||||||
import 'package:firka/core/extensions.dart';
|
import 'package:firka/core/extensions.dart';
|
||||||
import 'package:firka/ui/components/common_bottom_sheets.dart';
|
import 'package:firka/ui/components/common_bottom_sheets.dart';
|
||||||
@@ -21,8 +23,9 @@ import 'package:firka/ui/theme/style.dart';
|
|||||||
|
|
||||||
class HomeGradesSubjectScreen extends StatefulWidget {
|
class HomeGradesSubjectScreen extends StatefulWidget {
|
||||||
final AppInitialization data;
|
final AppInitialization data;
|
||||||
|
final Subject subject;
|
||||||
|
|
||||||
const HomeGradesSubjectScreen(this.data, {super.key});
|
const HomeGradesSubjectScreen(this.subject, this.data, {super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _HomeGradesSubjectScreen();
|
State<StatefulWidget> createState() => _HomeGradesSubjectScreen();
|
||||||
@@ -34,9 +37,9 @@ class _HomeGradesSubjectScreen extends FirkaState<HomeGradesSubjectScreen> {
|
|||||||
|
|
||||||
void _onRefreshRequested(BuildContext context) async {
|
void _onRefreshRequested(BuildContext context) async {
|
||||||
final cubit = context.read<HomeRefreshCubit>();
|
final cubit = context.read<HomeRefreshCubit>();
|
||||||
grades = (await widget.data.client.getGrades(forceCache: false)).response!
|
grades = (await widget.data.client.getGrades(
|
||||||
.where((grade) => grade.subject.uid == activeSubjectUid)
|
forceCache: false,
|
||||||
.where((grade) => grade.type.name != "felevi_jegy_ertekeles");
|
)).response!.where((grade) => grade.subject.uid == widget.subject.uid);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
@@ -49,9 +52,9 @@ class _HomeGradesSubjectScreen extends FirkaState<HomeGradesSubjectScreen> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
(() async {
|
(() async {
|
||||||
grades = (await widget.data.client.getGrades()).response!
|
grades = (await widget.data.client.getGrades()).response!.where(
|
||||||
.where((grade) => grade.subject.uid == activeSubjectUid)
|
(grade) => grade.subject.uid == widget.subject.uid,
|
||||||
.where((grade) => grade.type.name != "felevi_jegy_ertekeles");
|
);
|
||||||
|
|
||||||
if (mounted) setState(() {});
|
if (mounted) setState(() {});
|
||||||
})();
|
})();
|
||||||
@@ -105,348 +108,264 @@ class _HomeGradesSubjectScreen extends FirkaState<HomeGradesSubjectScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildContent(BuildContext context) {
|
Widget _buildContent(BuildContext context) {
|
||||||
if (grades != null && grades!.isNotEmpty && activeSubjectUid != "") {
|
if (grades == null || grades!.isEmpty) {
|
||||||
var aGrade = grades!.first;
|
return Container(
|
||||||
var groups = grades!.groupList((grade) => grade.recordDate);
|
color: appStyle.colors.background,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
final ghostGradeWidgets = _ghostEntries.reversed.map((e) {
|
child: Column(
|
||||||
return GestureDetector(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: FirkaCard(
|
children: [
|
||||||
left: [
|
Row(
|
||||||
Row(
|
children: [
|
||||||
children: [
|
Transform.translate(
|
||||||
GradeWidget.gradeValue(e.$1),
|
offset: const Offset(-4, 0),
|
||||||
SizedBox(width: 8),
|
child: GestureDetector(
|
||||||
Text(
|
child: FirkaIconWidget(
|
||||||
'${widget.data.l10n.ghost_grade} ${e.$2}%',
|
FirkaIconType.majesticons,
|
||||||
style: appStyle.fonts.B_16SB.apply(
|
Majesticon.chevronLeftLine,
|
||||||
|
color: appStyle.colors.textSecondary,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Transform.translate(
|
||||||
|
offset: const Offset(-4, 1),
|
||||||
|
child: Text(
|
||||||
|
widget.data.l10n.subjects,
|
||||||
|
style: appStyle.fonts.B_16R.apply(
|
||||||
color: appStyle.colors.textPrimary,
|
color: appStyle.colors.textPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {},
|
|
||||||
);
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
var gradeWidgets = List<Widget>.empty(growable: true);
|
|
||||||
if (ghostGradeWidgets.isNotEmpty) {
|
|
||||||
gradeWidgets.add(
|
|
||||||
Text(
|
|
||||||
widget.data.l10n.ghost_grades,
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
),
|
||||||
),
|
SizedBox(height: 16),
|
||||||
);
|
Expanded(
|
||||||
gradeWidgets.addAll(ghostGradeWidgets);
|
child: Column(
|
||||||
}
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
|
||||||
for (var group in groups.entries) {
|
|
||||||
gradeWidgets.add(SizedBox(height: 8));
|
|
||||||
gradeWidgets.add(
|
|
||||||
Text(
|
|
||||||
group.key.format(widget.data.l10n, FormatMode.grades),
|
|
||||||
style: appStyle.fonts.H_14px.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
gradeWidgets.add(SizedBox(height: 8));
|
|
||||||
for (var grade in group.value) {
|
|
||||||
gradeWidgets.add(
|
|
||||||
GestureDetector(
|
|
||||||
child: FirkaCard(
|
|
||||||
left: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
GradeWidget(grade),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: MediaQuery.of(context).size.width / 1.45,
|
|
||||||
child: Text(
|
|
||||||
(grade.topic ?? grade.type.description!)
|
|
||||||
.firstUpper(),
|
|
||||||
style: appStyle.fonts.B_16SB.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
grade.mode?.description != null
|
|
||||||
? SizedBox(
|
|
||||||
width:
|
|
||||||
MediaQuery.of(context).size.width / 1.45,
|
|
||||||
child: Text(
|
|
||||||
grade.mode!.description!.firstUpper(),
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.textSecondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: SizedBox(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
showGradeBottomSheet(context, widget.data, grade);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Material(
|
|
||||||
color: appStyle.colors.background,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
SizedBox(height: 12),
|
|
||||||
Column(
|
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Card(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
shadowColor: const Color.fromRGBO(0, 0, 0, 0),
|
||||||
children: [
|
color: appStyle.colors.a15p,
|
||||||
Row(
|
shape: RoundedRectangleBorder(
|
||||||
children: [
|
borderRadius: BorderRadius.circular(16),
|
||||||
Transform.translate(
|
),
|
||||||
offset: const Offset(-4, 0),
|
child: Padding(
|
||||||
child: GestureDetector(
|
padding: EdgeInsetsGeometry.all(6),
|
||||||
child: FirkaIconWidget(
|
child: ClassIconWidget.subject(
|
||||||
FirkaIconType.majesticons,
|
subject: widget.subject,
|
||||||
Majesticon.chevronLeftLine,
|
color: appStyle.colors.accent,
|
||||||
color: appStyle.colors.textSecondary,
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
context.pop();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Transform.translate(
|
|
||||||
offset: const Offset(-4, 0),
|
|
||||||
child: Text(
|
|
||||||
widget.data.l10n.subjects,
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
GestureDetector(
|
),
|
||||||
child: Card(
|
|
||||||
color: appStyle.colors.buttonSecondaryFill,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(4),
|
|
||||||
child: FirkaIconWidget(
|
|
||||||
FirkaIconType.majesticons,
|
|
||||||
Majesticon.menuSolid,
|
|
||||||
size: 26.0,
|
|
||||||
color: appStyle.colors.accent,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
showSubjectBottomSheetSettings(
|
|
||||||
context,
|
|
||||||
widget.data,
|
|
||||||
aGrade.subject,
|
|
||||||
onAddFromCalculator: (g, w) {
|
|
||||||
setState(() => _ghostEntries.add((g, w)));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
SizedBox(height: 8),
|
||||||
),
|
Text(
|
||||||
SizedBox(height: 16),
|
widget.subject.name,
|
||||||
Expanded(
|
style: appStyle.fonts.H_H2.apply(
|
||||||
child: ListView(
|
color: appStyle.colors.textPrimary,
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Card(
|
|
||||||
shadowColor: const Color.fromRGBO(0, 0, 0, 0),
|
|
||||||
color: appStyle.colors.a15p,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsetsGeometry.all(6),
|
|
||||||
child: ClassIconWidget(
|
|
||||||
uid: aGrade.subject.uid,
|
|
||||||
className: aGrade.subject.name,
|
|
||||||
category: aGrade.subject.category.name!,
|
|
||||||
color: appStyle.colors.accent,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
aGrade.subject.name,
|
|
||||||
style: appStyle.fonts.H_H2.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 2),
|
|
||||||
Text(
|
|
||||||
aGrade.teacher,
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.textSecondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 15),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
GradeChartWithInteraction(
|
),
|
||||||
grades: _gradesWithGhosts(aGrade.subject),
|
SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
widget.data.l10n.unknown_teacher,
|
||||||
|
style: appStyle.fonts.B_16R.apply(
|
||||||
|
color: appStyle.colors.textSecondary,
|
||||||
),
|
),
|
||||||
SizedBox(height: 2),
|
),
|
||||||
GradeSummaryBar(
|
Expanded(
|
||||||
grades: _gradesWithGhosts(aGrade.subject),
|
child: Center(
|
||||||
l10n: widget.data.l10n,
|
|
||||||
showAverage: ghostGradeWidgets.isNotEmpty,
|
|
||||||
),
|
|
||||||
SizedBox(height: 12),
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.only(left: 4),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: gradeWidgets,
|
children: [
|
||||||
|
SvgPicture.asset(
|
||||||
|
"assets/images/logos/dave.svg",
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
),
|
||||||
|
SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
widget.data.l10n.no_grades,
|
||||||
|
style: appStyle.fonts.B_16R.apply(
|
||||||
|
color: appStyle.colors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return Material(
|
|
||||||
color: appStyle.colors.background,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Transform.translate(
|
|
||||||
offset: const Offset(-4, 0),
|
|
||||||
child: GestureDetector(
|
|
||||||
child: FirkaIconWidget(
|
|
||||||
FirkaIconType.majesticons,
|
|
||||||
Majesticon.chevronLeftLine,
|
|
||||||
color: appStyle.colors.textSecondary,
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
context.pop();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Transform.translate(
|
|
||||||
offset: const Offset(-4, 1),
|
|
||||||
child: Text(
|
|
||||||
widget.data.l10n.subjects,
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 16),
|
),
|
||||||
SizedBox(
|
],
|
||||||
height:
|
|
||||||
MediaQuery.of(context).size.height -
|
|
||||||
MediaQuery.of(context).padding.top -
|
|
||||||
230,
|
|
||||||
child: ListView(
|
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Card(
|
|
||||||
shadowColor: const Color.fromRGBO(0, 0, 0, 0),
|
|
||||||
color: appStyle.colors.a15p,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsetsGeometry.all(6),
|
|
||||||
child: ClassIconWidget(
|
|
||||||
uid: subjectId,
|
|
||||||
className: subjectName,
|
|
||||||
category: subjectCategory,
|
|
||||||
color: appStyle.colors.accent,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
subjectName,
|
|
||||||
style: appStyle.fonts.H_H2.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 2),
|
|
||||||
Text(
|
|
||||||
widget.data.l10n.unknown_teacher,
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.textSecondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
height:
|
|
||||||
MediaQuery.of(context).size.height -
|
|
||||||
MediaQuery.of(context).padding.top -
|
|
||||||
320,
|
|
||||||
child: Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
SvgPicture.asset(
|
|
||||||
"assets/images/logos/dave.svg",
|
|
||||||
width: 48,
|
|
||||||
height: 48,
|
|
||||||
),
|
|
||||||
SizedBox(height: 12),
|
|
||||||
Text(
|
|
||||||
widget.data.l10n.no_grades,
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.textSecondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
var aGrade = grades!.first;
|
||||||
|
|
||||||
|
final ghostGradeWidgets = _ghostEntries.indexed
|
||||||
|
.map((e) {
|
||||||
|
return InfoCard.gradeGhost(
|
||||||
|
e.$2.$1,
|
||||||
|
e.$2.$2,
|
||||||
|
onTap: (ctx) {
|
||||||
|
setState(() {
|
||||||
|
_ghostEntries.removeAt(e.$1);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.toList()
|
||||||
|
.reversed;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
color: appStyle.colors.background,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Transform.translate(
|
||||||
|
offset: const Offset(-4, 0),
|
||||||
|
child: GestureDetector(
|
||||||
|
child: FirkaIconWidget(
|
||||||
|
FirkaIconType.majesticons,
|
||||||
|
Majesticon.chevronLeftLine,
|
||||||
|
color: appStyle.colors.textSecondary,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Transform.translate(
|
||||||
|
offset: const Offset(-4, 0),
|
||||||
|
child: Text(
|
||||||
|
widget.data.l10n.subjects,
|
||||||
|
style: appStyle.fonts.B_16R.apply(
|
||||||
|
color: appStyle.colors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
child: Card(
|
||||||
|
color: appStyle.colors.buttonSecondaryFill,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
child: FirkaIconWidget(
|
||||||
|
FirkaIconType.majesticons,
|
||||||
|
Majesticon.menuSolid,
|
||||||
|
size: 26.0,
|
||||||
|
color: appStyle.colors.accent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
showSubjectBottomSheetSettings(
|
||||||
|
context,
|
||||||
|
widget.data,
|
||||||
|
aGrade.subject,
|
||||||
|
onAddFromCalculator: (g, w) {
|
||||||
|
setState(() => _ghostEntries.add((g, w)));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 24),
|
||||||
|
Expanded(
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
FilledCircle(
|
||||||
|
diameter: 36,
|
||||||
|
color: appStyle.colors.a15p,
|
||||||
|
child: ClassIconWidget.subject(
|
||||||
|
subject: aGrade.subject,
|
||||||
|
color: appStyle.colors.accent,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
aGrade.subject.name,
|
||||||
|
style: appStyle.fonts.H_H2.apply(
|
||||||
|
color: appStyle.colors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
aGrade.teacher,
|
||||||
|
style: appStyle.fonts.B_16R.apply(
|
||||||
|
color: appStyle.colors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 24),
|
||||||
|
GradeChartWithInteraction(
|
||||||
|
grades: _gradesWithGhosts(aGrade.subject),
|
||||||
|
),
|
||||||
|
SizedBox(height: 10),
|
||||||
|
GradeSummaryBar(
|
||||||
|
grades: _gradesWithGhosts(aGrade.subject),
|
||||||
|
l10n: widget.data.l10n,
|
||||||
|
showAverage: ghostGradeWidgets.isNotEmpty,
|
||||||
|
),
|
||||||
|
SizedBox(height: 20),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 20,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 10,
|
||||||
|
children: [
|
||||||
|
if (ghostGradeWidgets.isNotEmpty)
|
||||||
|
Text(
|
||||||
|
initData.l10n.ghost_grades,
|
||||||
|
style: appStyle.fonts.B_16R.apply(
|
||||||
|
color: appStyle.colors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...ghostGradeWidgets,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
...grades!
|
||||||
|
.groupList((e) => e.recordDate)
|
||||||
|
.entries
|
||||||
|
.map(
|
||||||
|
(e) => Column(
|
||||||
|
spacing: 10,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
e.key.format(widget.data.l10n, FormatMode.main),
|
||||||
|
style: appStyle.fonts.B_16R.apply(
|
||||||
|
color: appStyle.colors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...e.value.map((v) => InfoCard.gradeDesc(v)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
|
import 'package:carousel_slider/carousel_slider.dart';
|
||||||
import 'package:firka/api/client/kreta_stream.dart';
|
import 'package:firka/api/client/kreta_stream.dart';
|
||||||
|
import 'package:firka/ui/phone/widgets/info_card.dart';
|
||||||
|
import 'package:firka/ui/phone/widgets/lesson.dart';
|
||||||
|
import 'package:firka/ui/phone/widgets/lesson_slider.dart';
|
||||||
|
import 'package:firka_common/ui/components/filled_circle.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:kreta_api/kreta_api.dart';
|
import 'package:kreta_api/kreta_api.dart';
|
||||||
import 'package:firka/core/extensions.dart';
|
import 'package:firka/core/extensions.dart';
|
||||||
import 'package:firka/ui/components/common_bottom_sheets.dart';
|
|
||||||
import 'package:firka/ui/phone/widgets/home_main_starting_soon.dart';
|
import 'package:firka/ui/phone/widgets/home_main_starting_soon.dart';
|
||||||
import 'package:firka/ui/phone/widgets/homework.dart';
|
|
||||||
import 'package:firka/ui/phone/widgets/info_board_item.dart';
|
|
||||||
import 'package:firka/ui/phone/widgets/lesson_small.dart';
|
import 'package:firka/ui/phone/widgets/lesson_small.dart';
|
||||||
import 'package:firka/ui/shared/delayed_spinner.dart';
|
import 'package:firka/ui/shared/delayed_spinner.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:majesticons_flutter/majesticons_flutter.dart';
|
import 'package:majesticons_flutter/majesticons_flutter.dart';
|
||||||
|
|
||||||
import 'package:firka/core/debug_helper.dart';
|
import 'package:firka/core/debug_helper.dart';
|
||||||
import 'package:firka/core/state/firka_state.dart';
|
import 'package:firka/core/state/firka_state.dart';
|
||||||
import 'package:firka/ui/components/firka_card.dart';
|
import 'package:firka/ui/components/firka_card.dart';
|
||||||
import 'package:firka/ui/components/grade.dart';
|
|
||||||
import 'package:firka/app/app_state.dart';
|
import 'package:firka/app/app_state.dart';
|
||||||
import 'package:firka/core/bloc/home_refresh_cubit.dart';
|
import 'package:firka/core/bloc/home_refresh_cubit.dart';
|
||||||
import 'package:firka/ui/theme/style.dart';
|
import 'package:firka/ui/theme/style.dart';
|
||||||
@@ -37,7 +39,6 @@ class HomeMainScreen extends StatefulWidget {
|
|||||||
class _HomeMainScreen extends FirkaState<HomeMainScreen> {
|
class _HomeMainScreen extends FirkaState<HomeMainScreen> {
|
||||||
_HomeMainScreen();
|
_HomeMainScreen();
|
||||||
|
|
||||||
DateTime now = timeNow();
|
|
||||||
List<Lesson>? lessons;
|
List<Lesson>? lessons;
|
||||||
List<NoticeBoardItem>? noticeBoard;
|
List<NoticeBoardItem>? noticeBoard;
|
||||||
List<InfoBoardItem>? infoBoard;
|
List<InfoBoardItem>? infoBoard;
|
||||||
@@ -49,14 +50,14 @@ class _HomeMainScreen extends FirkaState<HomeMainScreen> {
|
|||||||
|
|
||||||
void _onRefreshRequested(BuildContext context) async {
|
void _onRefreshRequested(BuildContext context) async {
|
||||||
final cubit = context.read<HomeRefreshCubit>();
|
final cubit = context.read<HomeRefreshCubit>();
|
||||||
await fetchData(cacheOnly: false);
|
setState(() {});
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
cubit.onRefreshComplete();
|
cubit.onRefreshComplete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchData({bool cacheOnly = false}) async {
|
Future<void> fetchData({bool cacheOnly = false}) async {
|
||||||
final midnight = now.getMidnight();
|
final midnight = timeNow().getMidnight();
|
||||||
|
|
||||||
var lessonsFetched = 0;
|
var lessonsFetched = 0;
|
||||||
var noticeBoardFetched = 0;
|
var noticeBoardFetched = 0;
|
||||||
@@ -173,25 +174,9 @@ class _HomeMainScreen extends FirkaState<HomeMainScreen> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
now = timeNow();
|
|
||||||
if (!mounted) return;
|
|
||||||
|
|
||||||
(() async {
|
(() async {
|
||||||
await fetchData();
|
await fetchData();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
timer = Timer.periodic(Duration(seconds: 1), (timer) async {
|
|
||||||
if (!mounted) return;
|
|
||||||
setState(() {
|
|
||||||
now = timeNow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
timer?.cancel();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -207,193 +192,94 @@ class _HomeMainScreen extends FirkaState<HomeMainScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildContent(BuildContext context) {
|
Widget _buildContent(BuildContext context) {
|
||||||
Widget welcomeWidget = SizedBox();
|
|
||||||
Widget nextClass = SizedBox();
|
|
||||||
Widget? nextTest;
|
|
||||||
bool lessonActive = false;
|
|
||||||
|
|
||||||
if (lessons != null && lessons!.isNotEmpty) {
|
|
||||||
if (now.isBefore(lessons!.first.start)) {
|
|
||||||
welcomeWidget = StartingSoonWidget(widget.data.l10n, now, lessons!);
|
|
||||||
} else {
|
|
||||||
var currentLesson = lessons!.firstWhereOrNull(
|
|
||||||
(lesson) => now.isAfter(lesson.start) && now.isBefore(lesson.end),
|
|
||||||
);
|
|
||||||
var prevLesson = lessons!.getPrevLesson(now);
|
|
||||||
var nextLesson = lessons!.getNextLesson(now);
|
|
||||||
int? lessonIndex;
|
|
||||||
|
|
||||||
if (currentLesson != null) {
|
|
||||||
lessonIndex = lessons!.getLessonNo(currentLesson);
|
|
||||||
lessonActive = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
welcomeWidget = LessonBigWidget(
|
|
||||||
widget.data.l10n,
|
|
||||||
now,
|
|
||||||
lessonIndex,
|
|
||||||
currentLesson,
|
|
||||||
prevLesson,
|
|
||||||
nextLesson,
|
|
||||||
lessons!,
|
|
||||||
tests ?? [],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (lessons != null && lessons!.isNotEmpty) {
|
|
||||||
var nextLesson = lessons!.getNextLesson(now);
|
|
||||||
if (nextLesson != null) {
|
|
||||||
nextClass = LessonSmallWidget(
|
|
||||||
widget.data.l10n,
|
|
||||||
nextLesson,
|
|
||||||
lessonActive,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (tests != null) {
|
|
||||||
final testsOnDate = tests!
|
|
||||||
.where(
|
|
||||||
(test) =>
|
|
||||||
test.date.isAfter(
|
|
||||||
nextLesson.start.getMidnight().subtract(
|
|
||||||
Duration(seconds: 1),
|
|
||||||
),
|
|
||||||
) &&
|
|
||||||
test.date.isBefore(
|
|
||||||
nextLesson.end.getMidnight().add(
|
|
||||||
Duration(hours: 23, minutes: 59),
|
|
||||||
),
|
|
||||||
) &&
|
|
||||||
test.subject.uid == nextLesson.subject?.uid,
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (testsOnDate.isNotEmpty) {
|
|
||||||
final test = testsOnDate.first;
|
|
||||||
|
|
||||||
nextTest = FirkaCard(
|
|
||||||
left: [
|
|
||||||
FirkaIconWidget(
|
|
||||||
FirkaIconType.majesticons,
|
|
||||||
Majesticon.editPen4Solid,
|
|
||||||
color: appStyle.colors.accent,
|
|
||||||
),
|
|
||||||
SizedBox(width: 6),
|
|
||||||
Text(
|
|
||||||
test.theme,
|
|
||||||
style: appStyle.fonts.B_16SB.apply(
|
|
||||||
color: appStyle.colors.textSecondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
right: [
|
|
||||||
Text(
|
|
||||||
test.method.description ?? "N/A",
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.textTertiary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (student != null && lessons != null) {
|
if (student != null && lessons != null) {
|
||||||
final infoItems = infoBoard ?? [];
|
final now = timeNow();
|
||||||
|
final infoItems = [...(infoBoard ?? []), ...(noticeBoard ?? [])];
|
||||||
final gradeItems = grades ?? [];
|
final gradeItems = grades ?? [];
|
||||||
|
final testItems = tests ?? [];
|
||||||
final homeworkItems = homework ?? [];
|
final homeworkItems = homework ?? [];
|
||||||
final noticeBoardWidgets = <(Widget, DateTime)>[];
|
final noticeBoardWidgets = <(Widget, DateTime)>[];
|
||||||
|
|
||||||
for (final item in infoItems) {
|
for (final item in infoItems) {
|
||||||
noticeBoardWidgets.add((
|
noticeBoardWidgets.add((InfoCard.messageItem(item), item.date));
|
||||||
GestureDetector(
|
}
|
||||||
child: InfoBoardItemWidget(item),
|
|
||||||
onTap: () {
|
for (final test in testItems) {
|
||||||
context.push('/message', extra: item);
|
noticeBoardWidgets.add((InfoCard.test(test), test.reportDate));
|
||||||
},
|
|
||||||
),
|
|
||||||
item.date,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final grade in gradeItems) {
|
for (final grade in gradeItems) {
|
||||||
noticeBoardWidgets.add((
|
noticeBoardWidgets.add((InfoCard.gradeSubj(grade), grade.creationDate));
|
||||||
GestureDetector(
|
|
||||||
child: FirkaCard(
|
|
||||||
left: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
GradeWidget(grade),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: MediaQuery.of(context).size.width / 1.45,
|
|
||||||
child: Text(
|
|
||||||
(grade.topic ?? grade.type.description!)
|
|
||||||
.firstUpper(),
|
|
||||||
style: appStyle.fonts.B_16SB.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
grade.mode?.description != null
|
|
||||||
? SizedBox(
|
|
||||||
width: MediaQuery.of(context).size.width / 1.45,
|
|
||||||
child: Text(
|
|
||||||
grade.mode!.description!.firstUpper(),
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.textSecondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: SizedBox(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
showGradeBottomSheet(context, widget.data, grade);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
grade.recordDate,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final entry in homeworkItems) {
|
for (final entry in homeworkItems) {
|
||||||
noticeBoardWidgets.add((
|
noticeBoardWidgets.add((InfoCard.homework(entry), entry.creationDate));
|
||||||
HomeworkWidget(widget.data, entry),
|
|
||||||
entry.creationDate,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
noticeBoardWidgets.sort(
|
noticeBoardWidgets.sort(
|
||||||
(item1, item2) => item2.$2.difference(item1.$2).inMilliseconds,
|
(item1, item2) => item2.$2.difference(item1.$2).inMilliseconds,
|
||||||
);
|
);
|
||||||
|
|
||||||
return Padding(
|
LinkedHashMap<Lesson, Test?> lessonTestMap = LinkedHashMap.fromEntries(
|
||||||
padding: const EdgeInsets.only(left: 20.0, top: 24.0, right: 20.0),
|
lessons!.indexed.map(
|
||||||
child: Column(
|
(i) => MapEntry(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
i.$2,
|
||||||
children: [
|
testItems.firstWhereOrNull(
|
||||||
WelcomeWidget(widget.data.l10n, now, student!, lessons!),
|
(t) =>
|
||||||
SizedBox(height: 48),
|
t.date.getMidnight() == i.$2.start.getMidnight() &&
|
||||||
welcomeWidget,
|
(i.$2.lessonNumber ?? i.$1) == t.lessonNumber,
|
||||||
lessonActive ? SizedBox(height: 5) : SizedBox(height: 0),
|
|
||||||
nextClass,
|
|
||||||
nextTest != null ? SizedBox(height: 12) : SizedBox(height: 0),
|
|
||||||
nextTest ?? SizedBox(),
|
|
||||||
nextTest != null ? SizedBox(height: 12) : SizedBox(height: 0),
|
|
||||||
Expanded(
|
|
||||||
child: ListView(
|
|
||||||
children: noticeBoardWidgets.map((e) => e.$1).toList(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
int testsTomorrow = testItems
|
||||||
|
.where(
|
||||||
|
(test) =>
|
||||||
|
test.date.isAfter(
|
||||||
|
now.getMidnight().add(Duration(hours: 23, minutes: 59)),
|
||||||
|
) &&
|
||||||
|
test.date.isBefore(now.getMidnight().add(Duration(days: 2))),
|
||||||
|
)
|
||||||
|
.length;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||||
|
child: RefreshIndicator(
|
||||||
|
onRefresh: () => fetchData(),
|
||||||
|
notificationPredicate: (ScrollNotification notification) {
|
||||||
|
return notification.depth == 0;
|
||||||
|
},
|
||||||
|
triggerMode: RefreshIndicatorTriggerMode.onEdge,
|
||||||
|
displacement: 0,
|
||||||
|
child: ListView(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
SizedBox(height: 24),
|
||||||
|
WelcomeWidget(widget.data.l10n, now, student!, lessons!),
|
||||||
|
SizedBox(height: 48),
|
||||||
|
LessonSlider(lessonTestMap, testsTomorrow),
|
||||||
|
...noticeBoardWidgets
|
||||||
|
.groupList((e) => e.$2)
|
||||||
|
.entries
|
||||||
|
.map(
|
||||||
|
(e) => Column(
|
||||||
|
spacing: 10,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
e.key.format(widget.data.l10n, FormatMode.main),
|
||||||
|
style: appStyle.fonts.B_16R.apply(
|
||||||
|
color: appStyle.colors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...e.value.map((v) => v.$1),
|
||||||
|
SizedBox(height: 10),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:carousel_slider/carousel_slider.dart';
|
import 'package:carousel_slider/carousel_slider.dart';
|
||||||
import 'package:firka/api/client/kreta_stream.dart';
|
import 'package:firka/api/client/kreta_stream.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import 'package:kreta_api/kreta_api.dart';
|
import 'package:kreta_api/kreta_api.dart';
|
||||||
import 'package:firka/core/debug_helper.dart';
|
import 'package:firka/core/debug_helper.dart';
|
||||||
import 'package:firka/core/extensions.dart';
|
import 'package:firka/core/extensions.dart';
|
||||||
@@ -287,7 +288,7 @@ class _HomeTimetableScreen extends FirkaState<HomeTimetableScreen>
|
|||||||
active = -1;
|
active = -1;
|
||||||
|
|
||||||
const double cardWidth = 40.0;
|
const double cardWidth = 40.0;
|
||||||
const double spacing = 8.0;
|
const double spacing = 16.0;
|
||||||
final double totalCardWidth = cardWidth + spacing;
|
final double totalCardWidth = cardWidth + spacing;
|
||||||
|
|
||||||
// Calculate animation positions based on real display indices
|
// Calculate animation positions based on real display indices
|
||||||
@@ -365,193 +366,169 @@ class _HomeTimetableScreen extends FirkaState<HomeTimetableScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildContent(BuildContext context) {
|
Widget _buildContent(BuildContext context) {
|
||||||
if (lessons != null && tests != null && events != null && dates != null) {
|
if (lessons == null || tests == null || events == null || dates == null) {
|
||||||
List<Widget> ttWidgets = [];
|
return Column(
|
||||||
List<Widget> ttDays = [];
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
final showABTimetable = widget.data.settings
|
children: [
|
||||||
.group("settings")
|
Row(
|
||||||
.subGroup("timetable_toast")
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
.boolean("ab_timetable");
|
children: [DelayedSpinnerWidget()],
|
||||||
// Build navigation icons using original dates
|
),
|
||||||
for (var i = 0; i < dates!.length; i++) {
|
],
|
||||||
final date = dates![i];
|
);
|
||||||
final realIndex = i; // Always use real index for nav icons
|
}
|
||||||
|
|
||||||
final testsOnDate = tests!
|
List<Widget> ttWidgets = [];
|
||||||
.where(
|
List<Widget> ttDays = [];
|
||||||
(test) =>
|
final showABTimetable = widget.data.settings
|
||||||
test.date.isAfter(date.subtract(Duration(seconds: 1))) &&
|
.group("settings")
|
||||||
test.date.isBefore(
|
.subGroup("timetable_toast")
|
||||||
date.add(Duration(hours: 23, minutes: 59)),
|
.boolean("ab_timetable");
|
||||||
),
|
// Build navigation icons using original dates
|
||||||
)
|
for (var i = 0; i < dates!.length; i++) {
|
||||||
.toList();
|
final date = dates![i];
|
||||||
|
final realIndex = i; // Always use real index for nav icons
|
||||||
|
|
||||||
if (testsOnDate.isNotEmpty) {
|
final testsOnDate = tests!
|
||||||
ttWidgets.add(
|
.where(
|
||||||
Stack(
|
(test) =>
|
||||||
children: [
|
test.date.isAfter(date.subtract(Duration(seconds: 1))) &&
|
||||||
BottomTimeTableNavIconWidget(
|
test.date.isBefore(date.add(Duration(hours: 23, minutes: 59))),
|
||||||
widget.data.l10n,
|
)
|
||||||
() {
|
.toList();
|
||||||
_handleNavTap(active, realIndex);
|
|
||||||
},
|
|
||||||
active == i,
|
|
||||||
date,
|
|
||||||
),
|
|
||||||
Transform.translate(
|
|
||||||
offset: Offset(38, -10),
|
|
||||||
child: BubbleTest(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ttWidgets.add(
|
|
||||||
BottomTimeTableNavIconWidget(
|
|
||||||
widget.data.l10n,
|
|
||||||
() {
|
|
||||||
_handleNavTap(active, realIndex);
|
|
||||||
},
|
|
||||||
active == i,
|
|
||||||
date,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build carousel pages using animation dates
|
Widget ttWidget = BottomTimeTableNavIconWidget(
|
||||||
for (var i = 0; i < _animationDates!.length; i++) {
|
widget.data.l10n,
|
||||||
final date = _animationDates![i];
|
() {
|
||||||
|
_handleNavTap(active, realIndex);
|
||||||
final lessonsOnDate = lessons!
|
},
|
||||||
.where(
|
active == i,
|
||||||
(lesson) =>
|
date,
|
||||||
lesson.start.isAfter(date) &&
|
);
|
||||||
lesson.end.isBefore(date.add(Duration(hours: 24))),
|
if (testsOnDate.isNotEmpty) {
|
||||||
)
|
ttWidgets.add(
|
||||||
.toList();
|
Stack(
|
||||||
final lessonsOnDay = lessons!
|
children: [
|
||||||
.where(
|
ttWidget,
|
||||||
(lesson) =>
|
Transform.translate(offset: Offset(34, -9), child: BubbleTest()),
|
||||||
lesson.start.getMidnight().millisecondsSinceEpoch ==
|
],
|
||||||
date.getMidnight().millisecondsSinceEpoch,
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
final eventsOnDate = events!
|
|
||||||
.where(
|
|
||||||
(lesson) =>
|
|
||||||
lesson.start.isAfter(date.subtract(Duration(seconds: 1))) &&
|
|
||||||
lesson.end.isBefore(
|
|
||||||
date.add(Duration(hours: 23, minutes: 59)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
final testsOnDate = tests!
|
|
||||||
.where(
|
|
||||||
(test) =>
|
|
||||||
test.date.isAfter(date.subtract(Duration(seconds: 1))) &&
|
|
||||||
test.date.isBefore(
|
|
||||||
date.add(Duration(hours: 23, minutes: 59)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
ttDays.add(
|
|
||||||
TimeTableDayWidget(
|
|
||||||
widget.data,
|
|
||||||
date,
|
|
||||||
lessons!,
|
|
||||||
lessonsOnDate,
|
|
||||||
eventsOnDate,
|
|
||||||
testsOnDate,
|
|
||||||
lessonsOnDay,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
List<Widget> ttEmptyCards = List.empty(growable: true);
|
|
||||||
|
|
||||||
if (animating || _showAnimatedCard) {
|
|
||||||
for (var i = 0; i < ttDays.length; i++) {
|
|
||||||
if (i == 0) {
|
|
||||||
Widget cardWidget = Card(
|
|
||||||
color: appStyle.colors.buttonSecondaryFill,
|
|
||||||
shadowColor: Colors.transparent,
|
|
||||||
child: SizedBox(width: 40, height: 54),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (_showAnimatedCard && _cardOffsetAnimation != null) {
|
|
||||||
ttEmptyCards.add(
|
|
||||||
AnimatedBuilder(
|
|
||||||
animation: _cardOffsetAnimation!,
|
|
||||||
builder: (context, child) {
|
|
||||||
return Transform.translate(
|
|
||||||
offset: _cardOffsetAnimation!.value,
|
|
||||||
child: cardWidget,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ttEmptyCards.add(
|
|
||||||
Transform.translate(offset: Offset(0, 0), child: cardWidget),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ttEmptyCards.add(
|
|
||||||
Card(
|
|
||||||
color: Colors.transparent,
|
|
||||||
shadowColor: Colors.transparent,
|
|
||||||
child: SizedBox(width: 40, height: 54),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
ttEmptyCards.clear();
|
ttWidgets.add(ttWidget);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Stack(
|
// Build carousel pages using animation dates
|
||||||
children: [
|
for (var i = 0; i < _animationDates!.length; i++) {
|
||||||
Column(
|
final date = _animationDates![i];
|
||||||
children: [
|
|
||||||
SizedBox(
|
final lessonsOnDate = lessons!
|
||||||
width: MediaQuery.of(context).size.width,
|
.where(
|
||||||
height: 74 + 16,
|
(lesson) =>
|
||||||
child: Padding(
|
lesson.start.isAfter(date) &&
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
lesson.end.isBefore(date.add(Duration(hours: 24))),
|
||||||
child: Column(
|
)
|
||||||
children: [
|
.toList();
|
||||||
Row(
|
final lessonsOnDay = lessons!
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
.where(
|
||||||
children: [
|
(lesson) =>
|
||||||
Text(
|
lesson.start.getMidnight().millisecondsSinceEpoch ==
|
||||||
widget.data.l10n.timetable,
|
date.getMidnight().millisecondsSinceEpoch,
|
||||||
style: appStyle.fonts.H_H2.apply(
|
)
|
||||||
color: appStyle.colors.textPrimary,
|
.toList();
|
||||||
),
|
final eventsOnDate = events!
|
||||||
|
.where(
|
||||||
|
(lesson) =>
|
||||||
|
lesson.start.isAfter(date.subtract(Duration(seconds: 1))) &&
|
||||||
|
lesson.end.isBefore(date.add(Duration(hours: 23, minutes: 59))),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
final testsOnDate = tests!
|
||||||
|
.where(
|
||||||
|
(test) =>
|
||||||
|
test.date.isAfter(date.subtract(Duration(seconds: 1))) &&
|
||||||
|
test.date.isBefore(date.add(Duration(hours: 23, minutes: 59))),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
ttDays.add(
|
||||||
|
TimeTableDayWidget(
|
||||||
|
widget.data,
|
||||||
|
date,
|
||||||
|
lessons!,
|
||||||
|
lessonsOnDate,
|
||||||
|
eventsOnDate,
|
||||||
|
testsOnDate,
|
||||||
|
lessonsOnDay,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget ttAnimatedCard = BottomTimeTableNavIconWidget(
|
||||||
|
widget.data.l10n,
|
||||||
|
() => {},
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (_cardOffsetAnimation != null && _showAnimatedCard) {
|
||||||
|
ttAnimatedCard = AnimatedBuilder(
|
||||||
|
animation: _cardOffsetAnimation!,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Transform.translate(
|
||||||
|
offset: _cardOffsetAnimation!.value,
|
||||||
|
child: BottomTimeTableNavIconWidget(
|
||||||
|
widget.data.l10n,
|
||||||
|
() => {},
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: MediaQuery.of(context).size.width,
|
||||||
|
height: 74 + 16,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.data.l10n.timetable,
|
||||||
|
style: appStyle.fonts.H_H2.apply(
|
||||||
|
color: appStyle.colors.textPrimary,
|
||||||
),
|
),
|
||||||
Row(
|
),
|
||||||
children: [
|
Row(
|
||||||
GestureDetector(
|
children: [
|
||||||
child: Card(
|
GestureDetector(
|
||||||
color: appStyle.colors.buttonSecondaryFill,
|
child: Card(
|
||||||
child: Padding(
|
color: appStyle.colors.buttonSecondaryFill,
|
||||||
padding: const EdgeInsets.all(8),
|
child: Padding(
|
||||||
child: FirkaIconWidget(
|
padding: const EdgeInsets.all(8),
|
||||||
FirkaIconType.majesticons,
|
child: FirkaIconWidget(
|
||||||
Majesticon.tableSolid,
|
FirkaIconType.majesticons,
|
||||||
size: 26.0,
|
Majesticon.tableSolid,
|
||||||
color: appStyle.colors.accent,
|
size: 26.0,
|
||||||
),
|
color: appStyle.colors.accent,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
|
||||||
context.push('/timetable/monthly');
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
/* TODO: 1.1.0
|
onTap: () {
|
||||||
|
context.push('/timetable/monthly');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
/* TODO: 1.1.0
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
color: appStyle.colors.buttonSecondaryFill,
|
color: appStyle.colors.buttonSecondaryFill,
|
||||||
@@ -566,205 +543,193 @@ class _HomeTimetableScreen extends FirkaState<HomeTimetableScreen>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
*/
|
*/
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
child: Card(
|
child: Card(
|
||||||
color: appStyle.colors.buttonSecondaryFill,
|
color: appStyle.colors.buttonSecondaryFill,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
child: FirkaIconWidget(
|
child: FirkaIconWidget(
|
||||||
FirkaIconType.majesticons,
|
FirkaIconType.majesticons,
|
||||||
Majesticon.settingsCogSolid,
|
Majesticon.settingsCogSolid,
|
||||||
size: 26.0,
|
size: 26.0,
|
||||||
color: appStyle.colors.accent,
|
color: appStyle.colors.accent,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
|
||||||
showSettingsSheet(
|
|
||||||
context,
|
|
||||||
MediaQuery.of(context).size.height * 0.4,
|
|
||||||
widget.data,
|
|
||||||
widget.data.settings
|
|
||||||
.group("settings")
|
|
||||||
.subGroup("timetable_toast"),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 16),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
GestureDetector(
|
|
||||||
behavior: HitTestBehavior.translucent,
|
|
||||||
child: SizedBox(
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
child: FirkaIconWidget(
|
|
||||||
FirkaIconType.icons,
|
|
||||||
"dropdownLeft",
|
|
||||||
size: 24,
|
|
||||||
color: appStyle.colors.accent,
|
|
||||||
),
|
),
|
||||||
|
onTap: () {
|
||||||
|
showSettingsSheet(
|
||||||
|
context,
|
||||||
|
MediaQuery.of(context).size.height * 0.4,
|
||||||
|
widget.data,
|
||||||
|
widget.data.settings
|
||||||
|
.group("settings")
|
||||||
|
.subGroup("timetable_toast"),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: FirkaIconWidget(
|
||||||
|
FirkaIconType.icons,
|
||||||
|
"dropdownLeft",
|
||||||
|
size: 24,
|
||||||
|
color: appStyle.colors.accent,
|
||||||
),
|
),
|
||||||
onTap: () async {
|
|
||||||
var newNow = now!.subtract(Duration(days: 7));
|
|
||||||
if (!mounted) return;
|
|
||||||
setState(() {
|
|
||||||
now = newNow;
|
|
||||||
lessons = null;
|
|
||||||
dates = null;
|
|
||||||
});
|
|
||||||
await initForWeek(newNow);
|
|
||||||
setState(() {
|
|
||||||
now = newNow;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
GestureDetector(
|
onTap: () async {
|
||||||
child: Row(
|
var newNow = now!.subtract(Duration(days: 7));
|
||||||
children: [
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
now = newNow;
|
||||||
|
lessons = null;
|
||||||
|
dates = null;
|
||||||
|
});
|
||||||
|
await initForWeek(newNow);
|
||||||
|
setState(() {
|
||||||
|
now = newNow;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
child: Row(
|
||||||
|
spacing: 4,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
now!.format(
|
||||||
|
widget.data.l10n,
|
||||||
|
FormatMode.yyyymmddwedd,
|
||||||
|
),
|
||||||
|
style: appStyle.fonts.B_16R.apply(
|
||||||
|
color: appStyle.colors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (showABTimetable) ...[
|
||||||
Text(
|
Text(
|
||||||
now!.format(
|
"•",
|
||||||
widget.data.l10n,
|
style: appStyle.fonts.B_16R.apply(
|
||||||
FormatMode.yyyymmddwedd,
|
color: appStyle.colors.accent,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
now!.isAWeek()
|
||||||
|
? widget.data.l10n.a_week
|
||||||
|
: widget.data.l10n.b_week,
|
||||||
style: appStyle.fonts.B_16R.apply(
|
style: appStyle.fonts.B_16R.apply(
|
||||||
color: appStyle.colors.textPrimary,
|
color: appStyle.colors.textPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(width: 4),
|
|
||||||
if (showABTimetable) ...[
|
|
||||||
Text(
|
|
||||||
"•",
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.accent,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
now!.isAWeek()
|
|
||||||
? widget.data.l10n.a_week
|
|
||||||
: widget.data.l10n.b_week,
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
],
|
||||||
onTap: () {
|
|
||||||
now = timeNow();
|
|
||||||
setActiveToToday();
|
|
||||||
_controller.jumpToPage(active);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
GestureDetector(
|
onTap: () {
|
||||||
behavior: HitTestBehavior.translucent,
|
now = timeNow();
|
||||||
child: SizedBox(
|
setActiveToToday();
|
||||||
width: 24,
|
_controller.jumpToPage(active);
|
||||||
height: 24,
|
},
|
||||||
child: FirkaIconWidget(
|
),
|
||||||
FirkaIconType.icons,
|
GestureDetector(
|
||||||
"dropdownRight",
|
behavior: HitTestBehavior.translucent,
|
||||||
size: 24,
|
child: SizedBox(
|
||||||
color: appStyle.colors.accent,
|
width: 24,
|
||||||
),
|
height: 24,
|
||||||
|
child: FirkaIconWidget(
|
||||||
|
FirkaIconType.icons,
|
||||||
|
"dropdownRight",
|
||||||
|
size: 24,
|
||||||
|
color: appStyle.colors.accent,
|
||||||
),
|
),
|
||||||
onTap: () async {
|
|
||||||
var newNow = now!.add(Duration(days: 7));
|
|
||||||
if (!mounted) return;
|
|
||||||
setState(() {
|
|
||||||
now = newNow;
|
|
||||||
lessons = null;
|
|
||||||
dates = null;
|
|
||||||
});
|
|
||||||
await initForWeek(newNow);
|
|
||||||
setState(() {
|
|
||||||
now = newNow;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
onTap: () async {
|
||||||
),
|
var newNow = now!.add(Duration(days: 7));
|
||||||
],
|
if (!mounted) return;
|
||||||
),
|
setState(() {
|
||||||
),
|
now = newNow;
|
||||||
),
|
lessons = null;
|
||||||
],
|
dates = null;
|
||||||
),
|
});
|
||||||
Column(
|
await initForWeek(newNow);
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
setState(() {
|
||||||
children: [
|
now = newNow;
|
||||||
Expanded(
|
});
|
||||||
child: TransparentPointer(
|
},
|
||||||
child: CarouselSlider(
|
),
|
||||||
items: ttDays,
|
],
|
||||||
carouselController: _controller,
|
|
||||||
options: CarouselOptions(
|
|
||||||
height: MediaQuery.of(context).size.height,
|
|
||||||
viewportFraction: 1,
|
|
||||||
enableInfiniteScroll: false,
|
|
||||||
initialPage: active,
|
|
||||||
onPageChanged: (i, _) {
|
|
||||||
if (animating || !mounted) return;
|
|
||||||
|
|
||||||
HapticFeedback.mediumImpact();
|
|
||||||
|
|
||||||
// Convert animation index to display index
|
|
||||||
final displayIndex = dates!.indexOf(
|
|
||||||
_animationDates![i],
|
|
||||||
);
|
|
||||||
maybeCacheNextWeek(displayIndex);
|
|
||||||
maybeCachePreviousWeek(displayIndex);
|
|
||||||
setState(() {
|
|
||||||
active = displayIndex;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.only(bottom: 12),
|
|
||||||
decoration: ShapeDecoration(
|
|
||||||
color: Colors.transparent,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(0),
|
|
||||||
),
|
|
||||||
shadows: [
|
|
||||||
BoxShadow(
|
|
||||||
color: appStyle.colors.background,
|
|
||||||
blurRadius: 30,
|
|
||||||
spreadRadius: 20,
|
|
||||||
offset: Offset(0, -25),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TransparentPointer(
|
||||||
|
child: CarouselSlider(
|
||||||
|
items: ttDays,
|
||||||
|
carouselController: _controller,
|
||||||
|
options: CarouselOptions(
|
||||||
|
height: MediaQuery.of(context).size.height,
|
||||||
|
viewportFraction: 1,
|
||||||
|
enableInfiniteScroll: false,
|
||||||
|
initialPage: active,
|
||||||
|
onPageChanged: (i, _) {
|
||||||
|
if (animating || !mounted) return;
|
||||||
|
|
||||||
|
HapticFeedback.mediumImpact();
|
||||||
|
|
||||||
|
// Convert animation index to display index
|
||||||
|
final displayIndex = dates!.indexOf(_animationDates![i]);
|
||||||
|
maybeCacheNextWeek(displayIndex);
|
||||||
|
maybeCachePreviousWeek(displayIndex);
|
||||||
|
setState(() {
|
||||||
|
active = displayIndex;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.only(bottom: 2),
|
||||||
|
decoration: ShapeDecoration(
|
||||||
|
color: Colors.transparent,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(0),
|
||||||
|
),
|
||||||
|
shadows: [
|
||||||
|
BoxShadow(
|
||||||
|
color: appStyle.colors.background,
|
||||||
|
blurRadius: 36,
|
||||||
|
offset: Offset(0, -27),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Wrap(spacing: 10, children: ttEmptyCards),
|
ttAnimatedCard,
|
||||||
Wrap(spacing: 10, children: ttWidgets),
|
Wrap(spacing: 16, children: ttWidgets),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
);
|
],
|
||||||
} else {
|
);
|
||||||
return Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [DelayedSpinnerWidget()],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,9 +63,7 @@ class _HomeTimetableMonthlyScreen
|
|||||||
forceCache: forceCache,
|
forceCache: forceCache,
|
||||||
);
|
);
|
||||||
var testsResp = await widget.data.client.getTests(forceCache: forceCache);
|
var testsResp = await widget.data.client.getTests(forceCache: forceCache);
|
||||||
var omissionsResp = await widget.data.client.getOmissions(
|
var omissionsResp = await widget.data.client.getOmissions(forceCache: true);
|
||||||
forceCache: forceCache,
|
|
||||||
);
|
|
||||||
List<DateTime> dates = List.empty(growable: true);
|
List<DateTime> dates = List.empty(growable: true);
|
||||||
|
|
||||||
for (var i = 0; i < days; i++) {
|
for (var i = 0; i < days; i++) {
|
||||||
@@ -119,496 +117,10 @@ class _HomeTimetableMonthlyScreen
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildContent(BuildContext context) {
|
Widget _buildContent(BuildContext context) {
|
||||||
if (lessons != null &&
|
if (lessons == null ||
|
||||||
omissions != null &&
|
omissions == null ||
|
||||||
tests != null &&
|
tests == null ||
|
||||||
dates != null) {
|
dates == null) {
|
||||||
List<Widget> ttDays = [];
|
|
||||||
|
|
||||||
final meow = dates![20];
|
|
||||||
final currentMonthStart = DateTime.utc(meow.year, meow.month, 1);
|
|
||||||
final currentMonthEnd = DateTime.utc(
|
|
||||||
meow.year,
|
|
||||||
meow.month + 1,
|
|
||||||
).subtract(Duration(days: 1));
|
|
||||||
|
|
||||||
// column-major -> row-major
|
|
||||||
for (var day = 0; day < 7; day++) {
|
|
||||||
for (var week = 0; week < 7; week++) {
|
|
||||||
final d = dates![week * 7 + day];
|
|
||||||
|
|
||||||
if (d.isBefore(currentMonthStart) || d.isAfter(currentMonthEnd)) {
|
|
||||||
ttDays.add(
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
decoration: ShapeDecoration(
|
|
||||||
color: appStyle.colors.cardTranslucent,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
d.format(widget.data.l10n, FormatMode.d),
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color:
|
|
||||||
(d.weekday == DateTime.saturday ||
|
|
||||||
d.weekday == DateTime.sunday)
|
|
||||||
? appStyle.colors.errorText
|
|
||||||
: appStyle.colors.textTertiary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
Widget body = SizedBox();
|
|
||||||
Color bodyBgColor = appStyle.colors.a15p;
|
|
||||||
|
|
||||||
var lessonsToday = lessons!.where(
|
|
||||||
(lesson) =>
|
|
||||||
lesson.start.isAfter(d.getMidnight()) &&
|
|
||||||
lesson.end.isBefore(
|
|
||||||
d.getMidnight().add(Duration(hours: 23, minutes: 59)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
var omissionType = lessonsToday.firstWhereOrNull(
|
|
||||||
(lesson) =>
|
|
||||||
lesson.studentPresence != null &&
|
|
||||||
lesson.studentPresence?.name != OmissionConsts.na &&
|
|
||||||
lesson.studentPresence?.name != OmissionConsts.present,
|
|
||||||
);
|
|
||||||
|
|
||||||
switch (activeFilter) {
|
|
||||||
case ActiveFilter.lessonNo:
|
|
||||||
if (lessonsToday.isNotEmpty) {
|
|
||||||
body = Center(
|
|
||||||
child: Text(
|
|
||||||
lessonsToday.length.toString(),
|
|
||||||
style: appStyle.fonts.H_16px.apply(
|
|
||||||
color:
|
|
||||||
omissionType != null &&
|
|
||||||
(omissionType.studentPresence!.name ==
|
|
||||||
OmissionConsts.absence ||
|
|
||||||
omissionType.studentPresence!.name ==
|
|
||||||
OmissionConsts.na)
|
|
||||||
? appStyle.colors.errorText
|
|
||||||
: timeNow().day == d.day &&
|
|
||||||
timeNow().month == d.month
|
|
||||||
? appStyle.colors.accent
|
|
||||||
: appStyle.colors.secondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (omissionType != null &&
|
|
||||||
(omissionType.studentPresence!.name ==
|
|
||||||
OmissionConsts.absence ||
|
|
||||||
omissionType.studentPresence!.name ==
|
|
||||||
OmissionConsts.na)) {
|
|
||||||
bodyBgColor = appStyle.colors.error15p;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ActiveFilter.tests:
|
|
||||||
if (lessonsToday.firstWhereOrNull(
|
|
||||||
(lesson) => tests!.any(
|
|
||||||
(test) =>
|
|
||||||
test.lessonNumber == lesson.lessonNumber &&
|
|
||||||
lesson.start.isAfter(test.date.getMidnight()) &&
|
|
||||||
lesson.end.isBefore(
|
|
||||||
test.date.getMidnight().add(
|
|
||||||
Duration(hours: 23, minutes: 59),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
) !=
|
|
||||||
null) {
|
|
||||||
body = Center(
|
|
||||||
child: FirkaIconWidget(
|
|
||||||
FirkaIconType.majesticons,
|
|
||||||
Majesticon.editPen4Solid,
|
|
||||||
size: 20.0,
|
|
||||||
color: appStyle.colors.accent,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ActiveFilter.omissions:
|
|
||||||
if (omissionType != null) {
|
|
||||||
switch (omissionType.studentPresence!.name) {
|
|
||||||
case OmissionConsts.absence:
|
|
||||||
final omission = omissions!.firstWhereOrNull((omission) {
|
|
||||||
return omission.date
|
|
||||||
.getMidnight()
|
|
||||||
.millisecondsSinceEpoch ==
|
|
||||||
omissionType.start
|
|
||||||
.getMidnight()
|
|
||||||
.millisecondsSinceEpoch &&
|
|
||||||
omission.subject.uid == omissionType.subject?.uid;
|
|
||||||
});
|
|
||||||
if (omission != null) {
|
|
||||||
switch (omission.state) {
|
|
||||||
case "Igazolando":
|
|
||||||
body = Center(
|
|
||||||
child: FirkaIconWidget(
|
|
||||||
FirkaIconType.majesticons,
|
|
||||||
Majesticon.restrictedSolid,
|
|
||||||
size: 20.0,
|
|
||||||
color: appStyle.colors.warningAccent,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
bodyBgColor = appStyle.colors.warning15p;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
body = Center(
|
|
||||||
child: FirkaIconWidget(
|
|
||||||
FirkaIconType.majesticons,
|
|
||||||
Majesticon.multiplySolid,
|
|
||||||
size: 20.0,
|
|
||||||
color: appStyle.colors.accent,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
body = Center(
|
|
||||||
child: FirkaIconWidget(
|
|
||||||
FirkaIconType.majesticons,
|
|
||||||
Majesticon.multiplySolid,
|
|
||||||
size: 20.0,
|
|
||||||
color: appStyle.colors.accent,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
logger.fine(
|
|
||||||
"omission: ${omissionType.studentPresence!.name}",
|
|
||||||
);
|
|
||||||
body = Center(
|
|
||||||
child: FirkaIconWidget(
|
|
||||||
FirkaIconType.majesticons,
|
|
||||||
Majesticon.timerLine,
|
|
||||||
size: 20.0,
|
|
||||||
color: appStyle.colors.accent,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
ttDays.add(
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
decoration: ShapeDecoration(
|
|
||||||
color: bodyBgColor,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: body,
|
|
||||||
),
|
|
||||||
SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
d.format(widget.data.l10n, FormatMode.d),
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color:
|
|
||||||
(d.weekday == DateTime.saturday ||
|
|
||||||
d.weekday == DateTime.sunday) &&
|
|
||||||
lessonsToday.isEmpty
|
|
||||||
? appStyle.colors.errorText
|
|
||||||
: appStyle.colors.textSecondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 12),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (timeNow().getMidnight().millisecondsSinceEpoch ==
|
|
||||||
d.toLocal().getMidnight().millisecondsSinceEpoch) {
|
|
||||||
bodyBgColor = appStyle.colors.buttonSecondaryFill;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Scaffold(
|
|
||||||
backgroundColor: appStyle.colors.background,
|
|
||||||
body: Stack(
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: MediaQuery.of(context).size.width,
|
|
||||||
height: 74 + 16,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
widget.data.l10n.timetable,
|
|
||||||
style: appStyle.fonts.H_H2.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
GestureDetector(
|
|
||||||
child: Card(
|
|
||||||
color: appStyle.colors.buttonSecondaryFill,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: FirkaIconWidget(
|
|
||||||
FirkaIconType.majesticons,
|
|
||||||
Majesticon.tableSolid,
|
|
||||||
size: 26.0,
|
|
||||||
color: appStyle.colors.accent,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
context.pop();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
// Nincs elkészítve jelenleg: Dolgozat stb hozzáadása(?)
|
|
||||||
// Card(
|
|
||||||
// color: appStyle.colors.buttonSecondaryFill,
|
|
||||||
// child: Padding(
|
|
||||||
// padding: const EdgeInsets.all(4),
|
|
||||||
// child: FirkaIconWidget(
|
|
||||||
// FirkaIconType.majesticons,
|
|
||||||
// Majesticon.plusLine,
|
|
||||||
// size: 32.0,
|
|
||||||
// color: appStyle.colors.accent,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
GestureDetector(
|
|
||||||
child: Card(
|
|
||||||
color: appStyle.colors.buttonSecondaryFill,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: FirkaIconWidget(
|
|
||||||
FirkaIconType.majesticons,
|
|
||||||
Majesticon.settingsCogSolid,
|
|
||||||
size: 26.0,
|
|
||||||
color: appStyle.colors.accent,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
showSettingsSheet(
|
|
||||||
context,
|
|
||||||
MediaQuery.of(context).size.height * 0.4,
|
|
||||||
widget.data,
|
|
||||||
widget.data.settings
|
|
||||||
.group("settings")
|
|
||||||
.subGroup("timetable_toast"),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 16),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
GestureDetector(
|
|
||||||
behavior: HitTestBehavior.translucent,
|
|
||||||
child: SizedBox(
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
child: FirkaIconWidget(
|
|
||||||
FirkaIconType.icons,
|
|
||||||
"dropdownLeft",
|
|
||||||
size: 24,
|
|
||||||
color: appStyle.colors.accent,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onTap: () async {
|
|
||||||
var newNow = DateTime(now!.year, now!.month - 1);
|
|
||||||
setState(() {
|
|
||||||
now = newNow;
|
|
||||||
lessons = null;
|
|
||||||
dates = null;
|
|
||||||
});
|
|
||||||
await initForMonth(newNow);
|
|
||||||
setState(() {
|
|
||||||
now = newNow;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
now!
|
|
||||||
.format(widget.data.l10n, FormatMode.yyyymmmm)
|
|
||||||
.toLowerCase(),
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GestureDetector(
|
|
||||||
child: FirkaIconWidget(
|
|
||||||
FirkaIconType.icons,
|
|
||||||
"dropdownRight",
|
|
||||||
size: 24,
|
|
||||||
color: appStyle.colors.accent,
|
|
||||||
),
|
|
||||||
onTap: () async {
|
|
||||||
var newNow = DateTime(now!.year, now!.month + 1);
|
|
||||||
setState(() {
|
|
||||||
now = newNow;
|
|
||||||
lessons = null;
|
|
||||||
dates = null;
|
|
||||||
});
|
|
||||||
await initForMonth(newNow);
|
|
||||||
setState(() {
|
|
||||||
now = newNow;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TransparentPointer(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 74 + 16 + 12),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
StaggeredGrid.count(
|
|
||||||
crossAxisCount: 7,
|
|
||||||
mainAxisSpacing: 8,
|
|
||||||
children: ttDays,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TransparentPointer(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
SizedBox(height: MediaQuery.of(context).size.height / 1.3),
|
|
||||||
SizedBox(
|
|
||||||
width: MediaQuery.of(context).size.width,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
SizedBox(),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
_StatusToast(
|
|
||||||
FirkaIconWidget(
|
|
||||||
FirkaIconType.majesticons,
|
|
||||||
Majesticon.clockSolid,
|
|
||||||
color: appStyle.colors.accent,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
lessons!
|
|
||||||
.where(
|
|
||||||
(lesson) =>
|
|
||||||
lesson.start.isAfter(
|
|
||||||
currentMonthStart,
|
|
||||||
) &&
|
|
||||||
lesson.end.isBefore(currentMonthEnd),
|
|
||||||
)
|
|
||||||
.length,
|
|
||||||
activeFilter == ActiveFilter.lessonNo,
|
|
||||||
() {
|
|
||||||
setState(() {
|
|
||||||
activeFilter = ActiveFilter.lessonNo;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
_StatusToast(
|
|
||||||
FirkaIconWidget(
|
|
||||||
FirkaIconType.majesticons,
|
|
||||||
Majesticon.editPen4Solid,
|
|
||||||
color: appStyle.colors.accent,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
lessons!
|
|
||||||
.where(
|
|
||||||
(lesson) => tests!.any(
|
|
||||||
(test) =>
|
|
||||||
test.lessonNumber ==
|
|
||||||
lesson.lessonNumber &&
|
|
||||||
lesson.start.isAfter(
|
|
||||||
test.date.getMidnight(),
|
|
||||||
) &&
|
|
||||||
lesson.end.isBefore(
|
|
||||||
test.date.getMidnight().add(
|
|
||||||
Duration(hours: 23, minutes: 59),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.length,
|
|
||||||
activeFilter == ActiveFilter.tests,
|
|
||||||
() {
|
|
||||||
setState(() {
|
|
||||||
activeFilter = ActiveFilter.tests;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
_StatusToast(
|
|
||||||
FirkaIconWidget(
|
|
||||||
FirkaIconType.majesticons,
|
|
||||||
Majesticon.timerLine,
|
|
||||||
color: appStyle.colors.accent,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
lessons!
|
|
||||||
.where(
|
|
||||||
(lesson) =>
|
|
||||||
lesson.start.isAfter(
|
|
||||||
currentMonthStart,
|
|
||||||
) &&
|
|
||||||
lesson.end.isBefore(currentMonthEnd) &&
|
|
||||||
lesson.studentPresence != null &&
|
|
||||||
lesson.studentPresence?.name !=
|
|
||||||
OmissionConsts.na &&
|
|
||||||
lesson.studentPresence?.name !=
|
|
||||||
OmissionConsts.present,
|
|
||||||
)
|
|
||||||
.length,
|
|
||||||
activeFilter == ActiveFilter.omissions,
|
|
||||||
() {
|
|
||||||
setState(() {
|
|
||||||
activeFilter = ActiveFilter.omissions;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: appStyle.colors.background,
|
backgroundColor: appStyle.colors.background,
|
||||||
body: Column(
|
body: Column(
|
||||||
@@ -622,6 +134,437 @@ class _HomeTimetableMonthlyScreen
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
List<Widget> ttDays = [];
|
||||||
|
|
||||||
|
final meow = dates![20];
|
||||||
|
final currentMonthStart = DateTime.utc(meow.year, meow.month, 1);
|
||||||
|
final currentMonthEnd = DateTime.utc(
|
||||||
|
meow.year,
|
||||||
|
meow.month + 1,
|
||||||
|
).subtract(Duration(days: 1));
|
||||||
|
|
||||||
|
// column-major -> row-major
|
||||||
|
for (var week = 0; week < 7; week++) {
|
||||||
|
for (var day = 0; day < 7; day++) {
|
||||||
|
final d = dates![week * 7 + day];
|
||||||
|
|
||||||
|
bool outOfRange =
|
||||||
|
d.isBefore(currentMonthStart) || d.isAfter(currentMonthEnd);
|
||||||
|
bool isToday =
|
||||||
|
!outOfRange && timeNow().day == d.day && timeNow().month == d.month;
|
||||||
|
|
||||||
|
Widget body = SizedBox();
|
||||||
|
Color? todayAccent = isToday ? appStyle.colors.textPrimaryLight : null;
|
||||||
|
Color bodyBgColor = appStyle.colors.a15p;
|
||||||
|
|
||||||
|
var lessonsToday = lessons!.where(
|
||||||
|
(lesson) =>
|
||||||
|
lesson.start.isAfter(d.getMidnight()) &&
|
||||||
|
lesson.end.isBefore(
|
||||||
|
d.getMidnight().add(Duration(hours: 23, minutes: 59)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
var omittedLesson = lessonsToday.firstWhereOrNull(
|
||||||
|
(lesson) =>
|
||||||
|
lesson.studentPresence != null &&
|
||||||
|
lesson.studentPresence?.name != OmissionConsts.na &&
|
||||||
|
lesson.studentPresence?.name != OmissionConsts.present,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (lessonsToday.isNotEmpty) {
|
||||||
|
switch (activeFilter) {
|
||||||
|
case ActiveFilter.lessonNo:
|
||||||
|
body = Center(
|
||||||
|
child: Text(
|
||||||
|
lessonsToday.length.toString(),
|
||||||
|
style: appStyle.fonts.H_16px.apply(
|
||||||
|
color:
|
||||||
|
todayAccent ??
|
||||||
|
(omittedLesson != null
|
||||||
|
? appStyle.colors.errorText
|
||||||
|
: appStyle.colors.secondary),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (omittedLesson != null) {
|
||||||
|
bodyBgColor = appStyle.colors.error15p;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ActiveFilter.tests:
|
||||||
|
if (lessonsToday.firstWhereOrNull(
|
||||||
|
(lesson) => tests!.any(
|
||||||
|
(test) =>
|
||||||
|
test.lessonNumber == lesson.lessonNumber &&
|
||||||
|
lesson.start.isAfter(test.date.getMidnight()) &&
|
||||||
|
lesson.end.isBefore(
|
||||||
|
test.date.getMidnight().add(
|
||||||
|
Duration(hours: 23, minutes: 59),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
) !=
|
||||||
|
null) {
|
||||||
|
body = Center(
|
||||||
|
child: FirkaIconWidget(
|
||||||
|
FirkaIconType.majesticons,
|
||||||
|
Majesticon.editPen4Solid,
|
||||||
|
size: 20.0,
|
||||||
|
color: todayAccent ?? appStyle.colors.accent,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ActiveFilter.omissions:
|
||||||
|
if (omittedLesson == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Nézze meg a többi órát is
|
||||||
|
final omission = omissions!.firstWhereOrNull((omission) {
|
||||||
|
return omission.date.getMidnight().millisecondsSinceEpoch ==
|
||||||
|
omittedLesson.start
|
||||||
|
.getMidnight()
|
||||||
|
.millisecondsSinceEpoch &&
|
||||||
|
omission.subject.uid == omittedLesson.subject?.uid;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (omission?.state == "Igazolt") {
|
||||||
|
body = Center(
|
||||||
|
child: FirkaIconWidget(
|
||||||
|
FirkaIconType.majesticons,
|
||||||
|
Majesticon.multiplySolid,
|
||||||
|
size: 20.0,
|
||||||
|
color: todayAccent ?? appStyle.colors.accent,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (omission?.lateForMin != null) {
|
||||||
|
body = Center(
|
||||||
|
child: FirkaIconWidget(
|
||||||
|
FirkaIconType.majesticons,
|
||||||
|
Majesticon.timerLine,
|
||||||
|
size: 20.0,
|
||||||
|
color: todayAccent ?? appStyle.colors.warningAccent,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
bodyBgColor = appStyle.colors.warning15p;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
body = Center(
|
||||||
|
child: FirkaIconWidget(
|
||||||
|
FirkaIconType.majesticons,
|
||||||
|
Majesticon.restrictedSolid,
|
||||||
|
size: 20.0,
|
||||||
|
color: todayAccent ?? appStyle.colors.errorAccent,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
bodyBgColor = appStyle.colors.error15p;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isToday) {
|
||||||
|
bodyBgColor = appStyle.colors.accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outOfRange) {
|
||||||
|
bodyBgColor = appStyle.colors.cardTranslucent;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isWeekend = d.weekday > 5 && lessonsToday.isEmpty;
|
||||||
|
Color textColor = (isWeekend
|
||||||
|
? appStyle.colors.errorText
|
||||||
|
: isToday
|
||||||
|
? appStyle.colors.textPrimary
|
||||||
|
: appStyle.colors.textTertiary);
|
||||||
|
|
||||||
|
if (outOfRange) {
|
||||||
|
textColor = textColor.withAlpha(77);
|
||||||
|
} else if (isWeekend) {
|
||||||
|
textColor = textColor.withAlpha(128);
|
||||||
|
}
|
||||||
|
|
||||||
|
ttDays.add(
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
decoration: ShapeDecoration(
|
||||||
|
color: bodyBgColor,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: body,
|
||||||
|
),
|
||||||
|
SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
d.format(widget.data.l10n, FormatMode.d),
|
||||||
|
style: (isToday ? appStyle.fonts.B_14SB : appStyle.fonts.B_14R)
|
||||||
|
.apply(color: textColor),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: appStyle.colors.background,
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: MediaQuery.of(context).size.width,
|
||||||
|
height: 74 + 16,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.data.l10n.timetable,
|
||||||
|
style: appStyle.fonts.H_H2.apply(
|
||||||
|
color: appStyle.colors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
child: Card(
|
||||||
|
color: appStyle.colors.buttonSecondaryFill,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: FirkaIconWidget(
|
||||||
|
FirkaIconType.majesticons,
|
||||||
|
Majesticon.tableSolid,
|
||||||
|
size: 26.0,
|
||||||
|
color: appStyle.colors.accent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// Nincs elkészítve jelenleg: Dolgozat stb hozzáadása(?)
|
||||||
|
// Card(
|
||||||
|
// color: appStyle.colors.buttonSecondaryFill,
|
||||||
|
// child: Padding(
|
||||||
|
// padding: const EdgeInsets.all(4),
|
||||||
|
// child: FirkaIconWidget(
|
||||||
|
// FirkaIconType.majesticons,
|
||||||
|
// Majesticon.plusLine,
|
||||||
|
// size: 32.0,
|
||||||
|
// color: appStyle.colors.accent,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
GestureDetector(
|
||||||
|
child: Card(
|
||||||
|
color: appStyle.colors.buttonSecondaryFill,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: FirkaIconWidget(
|
||||||
|
FirkaIconType.majesticons,
|
||||||
|
Majesticon.settingsCogSolid,
|
||||||
|
size: 26.0,
|
||||||
|
color: appStyle.colors.accent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
showSettingsSheet(
|
||||||
|
context,
|
||||||
|
MediaQuery.of(context).size.height * 0.4,
|
||||||
|
widget.data,
|
||||||
|
widget.data.settings
|
||||||
|
.group("settings")
|
||||||
|
.subGroup("timetable_toast"),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: FirkaIconWidget(
|
||||||
|
FirkaIconType.icons,
|
||||||
|
"dropdownLeft",
|
||||||
|
size: 24,
|
||||||
|
color: appStyle.colors.accent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
var newNow = DateTime(now!.year, now!.month - 1);
|
||||||
|
setState(() {
|
||||||
|
now = newNow;
|
||||||
|
lessons = null;
|
||||||
|
dates = null;
|
||||||
|
});
|
||||||
|
await initForMonth(newNow);
|
||||||
|
setState(() {
|
||||||
|
now = newNow;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
now!
|
||||||
|
.format(widget.data.l10n, FormatMode.yyyymmmm)
|
||||||
|
.toLowerCase(),
|
||||||
|
style: appStyle.fonts.B_16R.apply(
|
||||||
|
color: appStyle.colors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
child: FirkaIconWidget(
|
||||||
|
FirkaIconType.icons,
|
||||||
|
"dropdownRight",
|
||||||
|
size: 24,
|
||||||
|
color: appStyle.colors.accent,
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
var newNow = DateTime(now!.year, now!.month + 1);
|
||||||
|
setState(() {
|
||||||
|
now = newNow;
|
||||||
|
lessons = null;
|
||||||
|
dates = null;
|
||||||
|
});
|
||||||
|
await initForMonth(newNow);
|
||||||
|
setState(() {
|
||||||
|
now = newNow;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TransparentPointer(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
top: 74 + 16 + 12,
|
||||||
|
left: 20,
|
||||||
|
right: 20,
|
||||||
|
),
|
||||||
|
child: GridView.count(
|
||||||
|
crossAxisCount: 7,
|
||||||
|
mainAxisSpacing: 16,
|
||||||
|
mainAxisExtent: 62,
|
||||||
|
children: ttDays,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TransparentPointer(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
_StatusToast(
|
||||||
|
FirkaIconWidget(
|
||||||
|
FirkaIconType.majesticons,
|
||||||
|
Majesticon.clockSolid,
|
||||||
|
color: appStyle.colors.accent,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
lessons!
|
||||||
|
.where(
|
||||||
|
(lesson) =>
|
||||||
|
lesson.start.isAfter(currentMonthStart) &&
|
||||||
|
lesson.end.isBefore(currentMonthEnd),
|
||||||
|
)
|
||||||
|
.length,
|
||||||
|
activeFilter == ActiveFilter.lessonNo,
|
||||||
|
() {
|
||||||
|
setState(() {
|
||||||
|
activeFilter = ActiveFilter.lessonNo;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_StatusToast(
|
||||||
|
FirkaIconWidget(
|
||||||
|
FirkaIconType.majesticons,
|
||||||
|
Majesticon.editPen4Solid,
|
||||||
|
color: appStyle.colors.accent,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
lessons!
|
||||||
|
.where(
|
||||||
|
(lesson) => tests!.any(
|
||||||
|
(test) =>
|
||||||
|
test.lessonNumber == lesson.lessonNumber &&
|
||||||
|
lesson.start.isAfter(test.date.getMidnight()) &&
|
||||||
|
lesson.end.isBefore(
|
||||||
|
test.date.getMidnight().add(
|
||||||
|
Duration(hours: 23, minutes: 59),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.length,
|
||||||
|
activeFilter == ActiveFilter.tests,
|
||||||
|
() {
|
||||||
|
setState(() {
|
||||||
|
activeFilter = ActiveFilter.tests;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_StatusToast(
|
||||||
|
FirkaIconWidget(
|
||||||
|
FirkaIconType.majesticons,
|
||||||
|
Majesticon.timerLine,
|
||||||
|
color: appStyle.colors.accent,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
lessons!
|
||||||
|
.where(
|
||||||
|
(lesson) =>
|
||||||
|
lesson.start.isAfter(currentMonthStart) &&
|
||||||
|
lesson.end.isBefore(currentMonthEnd) &&
|
||||||
|
lesson.studentPresence != null &&
|
||||||
|
lesson.studentPresence?.name !=
|
||||||
|
OmissionConsts.na &&
|
||||||
|
lesson.studentPresence?.name !=
|
||||||
|
OmissionConsts.present,
|
||||||
|
)
|
||||||
|
.length,
|
||||||
|
activeFilter == ActiveFilter.omissions,
|
||||||
|
() {
|
||||||
|
setState(() {
|
||||||
|
activeFilter = ActiveFilter.omissions;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -638,29 +581,29 @@ class _StatusToast extends StatelessWidget {
|
|||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: _onTap,
|
onTap: _onTap,
|
||||||
child: Container(
|
child: Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
decoration: ShapeDecoration(
|
decoration: ShapeDecoration(
|
||||||
color: _active
|
color: _active
|
||||||
? appStyle.colors.buttonSecondaryFill
|
? appStyle.colors.buttonSecondaryFill
|
||||||
: appStyle.colors.cardTranslucent,
|
: appStyle.colors.cardTranslucent,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Row(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
spacing: 6,
|
||||||
child: Row(
|
children: [
|
||||||
children: [
|
_icon,
|
||||||
_icon,
|
Text(
|
||||||
SizedBox(width: 6),
|
_count.toString(),
|
||||||
Text(
|
style: appStyle.fonts.H_16px.apply(
|
||||||
_count.toString(),
|
color: _active
|
||||||
style: appStyle.fonts.H_16px.apply(
|
? appStyle.colors.textPrimary
|
||||||
color: appStyle.colors.textPrimary,
|
: appStyle.colors.textSecondary,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:carousel_slider/carousel_slider.dart';
|
|||||||
import 'package:firka/core/firka_bundle.dart';
|
import 'package:firka/core/firka_bundle.dart';
|
||||||
import 'package:firka/app/app_state.dart';
|
import 'package:firka/app/app_state.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:firka/ui/phone/widgets/domain_browser_webview.dart';
|
||||||
import 'package:firka/ui/phone/widgets/login_webview.dart';
|
import 'package:firka/ui/phone/widgets/login_webview.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
@@ -12,7 +13,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:sensors_plus/sensors_plus.dart';
|
import 'package:sensors_plus/sensors_plus.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
|
||||||
import 'package:vibration/vibration.dart';
|
import 'package:vibration/vibration.dart';
|
||||||
|
|
||||||
import 'package:firka/core/bloc/theme_cubit.dart';
|
import 'package:firka/core/bloc/theme_cubit.dart';
|
||||||
@@ -21,9 +21,8 @@ import 'package:firka/core/image_preloader.dart';
|
|||||||
import 'package:firka/ui/theme/style.dart';
|
import 'package:firka/ui/theme/style.dart';
|
||||||
import 'package:firka/ui/shared/delayed_spinner.dart';
|
import 'package:firka/ui/shared/delayed_spinner.dart';
|
||||||
|
|
||||||
const String _privacyUrlHungarian =
|
const String _privacyUrlHungarian = 'https://firka.app/privacy-policy';
|
||||||
'https://github.com/QwIT-Development/privacy-policy/blob/master/README.md';
|
const String _privacyUrlOther = 'https://firka.app/privacy-policy';
|
||||||
const String _privacyUrlOther = 'https://firka.app/privacy';
|
|
||||||
|
|
||||||
class LoginScreen extends StatefulWidget {
|
class LoginScreen extends StatefulWidget {
|
||||||
final AppInitialization data;
|
final AppInitialization data;
|
||||||
@@ -45,17 +44,27 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _getPrivacyPolicyUrl() {
|
String _getPrivacyPolicyUrl() {
|
||||||
final locale = Localizations.localeOf(context).languageCode;
|
return "https://firka.app/privacy";
|
||||||
return locale == 'hu' ? _privacyUrlHungarian : _privacyUrlOther;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _launchPrivacyPolicy() async {
|
Future<void> _showPrivacyPolicyWebview() async {
|
||||||
final url = _getPrivacyPolicyUrl();
|
final url = _getPrivacyPolicyUrl();
|
||||||
try {
|
if (!mounted) return;
|
||||||
await launchUrl(Uri.parse(url));
|
|
||||||
} catch (e) {
|
await showModalBottomSheet<void>(
|
||||||
logger.shout('LoginScreen: Error launching privacy policy URL: $e');
|
context: context,
|
||||||
}
|
isScrollControlled: true,
|
||||||
|
showDragHandle: false,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: MediaQuery.sizeOf(context).height,
|
||||||
|
child: DomainBrowserWebviewWidget(
|
||||||
|
data: widget.data,
|
||||||
|
url: url,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _preloadImages() async {
|
Future<void> _preloadImages() async {
|
||||||
@@ -513,7 +522,7 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: _launchPrivacyPolicy,
|
onTap: _showPrivacyPolicyWebview,
|
||||||
child: Text(
|
child: Text(
|
||||||
widget.data.l10n.privacyLabel,
|
widget.data.l10n.privacyLabel,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import 'package:firka/core/extensions.dart';
|
import 'package:firka/core/extensions.dart';
|
||||||
import 'package:firka/app/app_state.dart';
|
import 'package:firka/app/app_state.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_html/flutter_html.dart';
|
||||||
import 'package:majesticons_flutter/majesticons_flutter.dart';
|
import 'package:majesticons_flutter/majesticons_flutter.dart';
|
||||||
|
|
||||||
import 'package:kreta_api/kreta_api.dart';
|
import 'package:kreta_api/kreta_api.dart';
|
||||||
import 'package:firka/core/firka_bundle.dart';
|
import 'package:firka/core/firka_bundle.dart';
|
||||||
import 'package:firka/ui/theme/style.dart';
|
import 'package:firka/ui/theme/style.dart';
|
||||||
import 'package:firka/ui/shared/firka_icon.dart';
|
import 'package:firka/ui/shared/firka_icon.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class MessageScreen extends StatelessWidget {
|
class MessageScreen extends StatelessWidget {
|
||||||
final AppInitialization data;
|
final AppInitialization data;
|
||||||
final InfoBoardItem info;
|
final MessageItem message;
|
||||||
|
|
||||||
const MessageScreen(this.data, this.info, {super.key});
|
const MessageScreen(this.data, this.message, {super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -68,7 +70,7 @@ class MessageScreen extends StatelessWidget {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
width: MediaQuery.of(context).size.width * 0.85,
|
width: MediaQuery.of(context).size.width * 0.85,
|
||||||
child: Text(
|
child: Text(
|
||||||
info.title,
|
message.title,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: appStyle.fonts.H_H2.apply(
|
style: appStyle.fonts.H_H2.apply(
|
||||||
color: appStyle.colors.textPrimary,
|
color: appStyle.colors.textPrimary,
|
||||||
@@ -82,7 +84,7 @@ class MessageScreen extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
info.date.format(data.l10n, FormatMode.yyyymmdd),
|
message.date.format(data.l10n, FormatMode.yyyymmdd),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: appStyle.fonts.B_16R.apply(
|
style: appStyle.fonts.B_16R.apply(
|
||||||
color: appStyle.colors.textSecondary,
|
color: appStyle.colors.textSecondary,
|
||||||
@@ -107,7 +109,7 @@ class MessageScreen extends StatelessWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 6),
|
padding: const EdgeInsets.only(bottom: 6),
|
||||||
child: Text(
|
child: Text(
|
||||||
info.author[0],
|
message.author[0],
|
||||||
style: appStyle.fonts.H_18px.copyWith(
|
style: appStyle.fonts.H_18px.copyWith(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
color: appStyle.colors.textPrimary,
|
color: appStyle.colors.textPrimary,
|
||||||
@@ -125,7 +127,7 @@ class MessageScreen extends StatelessWidget {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
width: MediaQuery.of(context).size.width / 1.4,
|
width: MediaQuery.of(context).size.width / 1.4,
|
||||||
child: Text(
|
child: Text(
|
||||||
info.author,
|
message.author,
|
||||||
style: appStyle.fonts.B_16SB.apply(
|
style: appStyle.fonts.B_16SB.apply(
|
||||||
color: appStyle.colors.textPrimary,
|
color: appStyle.colors.textPrimary,
|
||||||
),
|
),
|
||||||
@@ -136,21 +138,34 @@ class MessageScreen extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 20),
|
SizedBox(height: 20),
|
||||||
Padding(
|
Flexible(
|
||||||
padding: const EdgeInsets.all(4),
|
fit: FlexFit.loose,
|
||||||
child: Container(
|
child: Padding(
|
||||||
decoration: BoxDecoration(
|
padding: const EdgeInsets.all(4),
|
||||||
color: appStyle.colors.card,
|
child: Container(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
decoration: BoxDecoration(
|
||||||
),
|
color: appStyle.colors.card,
|
||||||
child: Padding(
|
borderRadius: BorderRadius.all(
|
||||||
padding: const EdgeInsets.all(12),
|
Radius.circular(16),
|
||||||
child: Text(
|
),
|
||||||
info.contentText,
|
),
|
||||||
style: appStyle.fonts.B_16R.apply(
|
child: Padding(
|
||||||
color: appStyle.colors.textPrimary,
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Html(
|
||||||
|
data: message.contentHTML,
|
||||||
|
onLinkTap: (url, map, element) => {
|
||||||
|
if (url != null) launchUrlString(url),
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
"*": Style.fromTextStyle(
|
||||||
|
appStyle.fonts.B_16R.apply(
|
||||||
|
color: appStyle.colors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.start,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -9,17 +9,13 @@ class BottomNavIconWidget extends StatelessWidget {
|
|||||||
final bool active;
|
final bool active;
|
||||||
final dynamic icon;
|
final dynamic icon;
|
||||||
final String text;
|
final String text;
|
||||||
final Color iconColor;
|
|
||||||
final Color textColor;
|
|
||||||
final bool isProfilePicture;
|
final bool isProfilePicture;
|
||||||
|
|
||||||
const BottomNavIconWidget(
|
const BottomNavIconWidget(
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.active,
|
this.active,
|
||||||
this.icon,
|
this.icon,
|
||||||
this.text,
|
this.text, {
|
||||||
this.iconColor,
|
|
||||||
this.textColor, {
|
|
||||||
this.isProfilePicture = false,
|
this.isProfilePicture = false,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
@@ -33,43 +29,43 @@ class BottomNavIconWidget extends StatelessWidget {
|
|||||||
HapticFeedback.lightImpact();
|
HapticFeedback.lightImpact();
|
||||||
onTap();
|
onTap();
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Column(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
mainAxisSize: MainAxisSize.min,
|
||||||
child: Column(
|
children: [
|
||||||
mainAxisSize: MainAxisSize.min,
|
if (isProfilePicture && icon != null)
|
||||||
children: [
|
Container(
|
||||||
if (isProfilePicture && icon != null)
|
width: 24,
|
||||||
Container(
|
height: 24,
|
||||||
width: 24,
|
decoration: BoxDecoration(
|
||||||
height: 24,
|
shape: BoxShape.circle,
|
||||||
decoration: BoxDecoration(
|
image: DecorationImage(
|
||||||
shape: BoxShape.circle,
|
image: MemoryImage(icon as Uint8List),
|
||||||
image: DecorationImage(
|
fit: BoxFit.cover,
|
||||||
image: MemoryImage(icon as Uint8List),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
else
|
)
|
||||||
FirkaIconWidget(
|
else
|
||||||
FirkaIconType.majesticons,
|
FirkaIconWidget(
|
||||||
icon as Uint8List,
|
FirkaIconType.majesticons,
|
||||||
color: iconColor,
|
icon as Uint8List,
|
||||||
size: 24,
|
color: active
|
||||||
).build(context),
|
? appStyle.colors.accent
|
||||||
const SizedBox(height: 4),
|
: appStyle.colors.secondary.withAlpha(128),
|
||||||
Text(
|
size: 24,
|
||||||
text,
|
|
||||||
style: active
|
|
||||||
? appStyle.fonts.B_12SB.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
)
|
|
||||||
: appStyle.fonts.B_12R.apply(
|
|
||||||
color: appStyle.colors.textSecondary,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
const SizedBox(height: 2),
|
||||||
),
|
FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: appStyle.fonts.B_14R.apply(
|
||||||
|
color: active
|
||||||
|
? appStyle.colors.textPrimary
|
||||||
|
: appStyle.colors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:firka/core/extensions.dart';
|
import 'package:firka/core/extensions.dart';
|
||||||
import 'package:firka/l10n/app_localizations.dart';
|
import 'package:firka/l10n/app_localizations.dart';
|
||||||
|
import 'package:firka_common/ui/components/firka_card.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:firka/ui/theme/style.dart';
|
import 'package:firka/ui/theme/style.dart';
|
||||||
@@ -8,7 +9,7 @@ class BottomTimeTableNavIconWidget extends StatelessWidget {
|
|||||||
final AppLocalizations l10n;
|
final AppLocalizations l10n;
|
||||||
final void Function() onTap;
|
final void Function() onTap;
|
||||||
final bool active;
|
final bool active;
|
||||||
final DateTime date;
|
final DateTime? date;
|
||||||
|
|
||||||
const BottomTimeTableNavIconWidget(
|
const BottomTimeTableNavIconWidget(
|
||||||
this.l10n,
|
this.l10n,
|
||||||
@@ -25,30 +26,48 @@ class BottomTimeTableNavIconWidget extends StatelessWidget {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
onTap();
|
onTap();
|
||||||
},
|
},
|
||||||
child: Card(
|
child: Container(
|
||||||
color: active
|
width: 40,
|
||||||
? appStyle.colors.buttonSecondaryFill
|
height: 54,
|
||||||
: Colors.transparent,
|
decoration: ShapeDecoration(
|
||||||
shadowColor: active ? appStyle.colors.shadowColor : Colors.transparent,
|
shadows: active
|
||||||
child: SizedBox(
|
? [
|
||||||
width: 40,
|
BoxShadow(
|
||||||
height: 54,
|
color: appStyle.colors.shadowColor,
|
||||||
|
offset: const Offset(0, 1),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
color: active
|
||||||
|
? appStyle.colors.buttonSecondaryFill
|
||||||
|
: Colors.transparent,
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
SizedBox(height: 6),
|
children: date != null
|
||||||
Text(
|
? [
|
||||||
date.format(l10n, FormatMode.da),
|
Text(
|
||||||
style: appStyle.fonts.H_16px.apply(
|
date!.format(l10n, FormatMode.da),
|
||||||
color: appStyle.colors.textPrimary,
|
style: appStyle.fonts.H_16px.apply(
|
||||||
),
|
color: active
|
||||||
),
|
? appStyle.colors.textPrimary
|
||||||
Text(
|
: appStyle.colors.textTeritary,
|
||||||
date.format(l10n, FormatMode.dd),
|
),
|
||||||
style: appStyle.fonts.B_16R.apply(
|
),
|
||||||
color: appStyle.colors.textSecondary,
|
Text(
|
||||||
),
|
date!.format(l10n, FormatMode.dd),
|
||||||
),
|
style: appStyle.fonts.B_16R.apply(
|
||||||
],
|
color: active
|
||||||
|
? appStyle.colors.textSecondary
|
||||||
|
: appStyle.colors.textTeritary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -19,12 +19,12 @@ class BubbleTest extends StatelessWidget {
|
|||||||
height: 24,
|
height: 24,
|
||||||
),
|
),
|
||||||
Transform.translate(
|
Transform.translate(
|
||||||
offset: Offset(3, 6),
|
offset: Offset(2, 4),
|
||||||
child: FirkaIconWidget(
|
child: FirkaIconWidget(
|
||||||
FirkaIconType.majesticons,
|
FirkaIconType.majesticons,
|
||||||
Majesticon.editPen4Line,
|
Majesticon.editPen4Solid,
|
||||||
color: appStyle.colors.accent,
|
color: appStyle.colors.accent,
|
||||||
size: 14,
|
size: 16,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
325
firka/lib/ui/phone/widgets/domain_browser_webview.dart
Normal file
325
firka/lib/ui/phone/widgets/domain_browser_webview.dart
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:firka/app/app_state.dart';
|
||||||
|
import 'package:firka/core/state/firka_state.dart';
|
||||||
|
import 'package:firka/ui/theme/style.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
import 'package:majesticons_flutter/majesticons_flutter.dart';
|
||||||
|
import 'package:webview_flutter/webview_flutter.dart';
|
||||||
|
|
||||||
|
/// Lightweight in-app browser used outside the login flow (e.g. privacy policy).
|
||||||
|
///
|
||||||
|
/// This deliberately contains only generic WebView behaviour, keeping all
|
||||||
|
/// login/token handling inside `login_webview.dart`.
|
||||||
|
class DomainBrowserWebviewWidget extends StatefulWidget {
|
||||||
|
final AppInitialization? data;
|
||||||
|
final String? url;
|
||||||
|
|
||||||
|
const DomainBrowserWebviewWidget({
|
||||||
|
super.key,
|
||||||
|
this.data,
|
||||||
|
this.url,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DomainBrowserWebviewWidget> createState() =>
|
||||||
|
_DomainBrowserWebviewWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DomainBrowserWebviewWidgetState
|
||||||
|
extends FirkaState<DomainBrowserWebviewWidget>
|
||||||
|
with TickerProviderStateMixin {
|
||||||
|
late WebViewController _webViewController;
|
||||||
|
bool _isLoading = true;
|
||||||
|
AnimationController? _fadeAnimationController;
|
||||||
|
Animation<double>? _fadeAnimation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_fadeAnimationController = AnimationController(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
|
||||||
|
_fadeAnimation = Tween<double>(
|
||||||
|
begin: 1.0,
|
||||||
|
end: 0.0,
|
||||||
|
).animate(_fadeAnimationController!);
|
||||||
|
|
||||||
|
assert(widget.data != null && widget.url != null,
|
||||||
|
'DomainBrowserWebviewWidget requires non-null data and url');
|
||||||
|
|
||||||
|
_webViewController = WebViewController()
|
||||||
|
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||||
|
..loadRequest(Uri.parse(widget.url!))
|
||||||
|
..setNavigationDelegate(
|
||||||
|
NavigationDelegate(
|
||||||
|
onPageFinished: (String url) {
|
||||||
|
Timer(Duration.zero, () {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
_fadeAnimationController?.forward().then((_) {
|
||||||
|
_fadeAnimationController?.reset();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onPageStarted: (String url) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
_fadeAnimationController?.reset();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_fadeAnimationController?.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (widget.data == null || widget.url == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = widget.data!;
|
||||||
|
final mediaQuery = MediaQuery.of(context);
|
||||||
|
final safePadding = mediaQuery.padding;
|
||||||
|
final displayUrl = (widget.url ?? '').replaceFirst(RegExp(r'^https?://'), '');
|
||||||
|
final displayParts = displayUrl.split('/');
|
||||||
|
final host = displayParts.isNotEmpty ? displayParts.first : displayUrl;
|
||||||
|
final path = displayParts.length > 1
|
||||||
|
? '/${displayParts.sublist(1).join('/')}'
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return Material(
|
||||||
|
color: appStyle.colors.background,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
top: 61 + safePadding.top,
|
||||||
|
left: 12,
|
||||||
|
right: 12,
|
||||||
|
bottom: safePadding.bottom,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 2),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
"assets/icons/dave.svg",
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
widget.data?.l10n.runningInDomainBrowser ??
|
||||||
|
'Domain Browser',
|
||||||
|
style: appStyle.fonts.B_16R.copyWith(
|
||||||
|
color: appStyle.colors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => Navigator.of(context).pop(),
|
||||||
|
child: Container(
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: appStyle.colors.buttonSecondaryFill,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: appStyle.colors.shadowColor,
|
||||||
|
offset: const Offset(0, 1),
|
||||||
|
blurRadius: appStyle.colors.shadowBlur.toDouble(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Majesticon(
|
||||||
|
Majesticon.multiplySolid,
|
||||||
|
color: appStyle.colors.accent,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 22),
|
||||||
|
Expanded(
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
WebViewWidget(
|
||||||
|
controller: _webViewController,
|
||||||
|
gestureRecognizers: {
|
||||||
|
Factory<OneSequenceGestureRecognizer>(
|
||||||
|
() => EagerGestureRecognizer(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (_fadeAnimationController != null &&
|
||||||
|
_fadeAnimation != null)
|
||||||
|
IgnorePointer(
|
||||||
|
ignoring: !_isLoading,
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: _fadeAnimationController!,
|
||||||
|
builder: (context, child) => AnimatedOpacity(
|
||||||
|
opacity: _isLoading
|
||||||
|
? 1.0
|
||||||
|
: _fadeAnimationController!.isAnimating
|
||||||
|
? _fadeAnimation!.value
|
||||||
|
: 0.0,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
child: Container(
|
||||||
|
color: appStyle.colors.background,
|
||||||
|
child: Center(
|
||||||
|
child: Image.asset(
|
||||||
|
"assets/images/logos/loading.gif",
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
height: 42,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: appStyle.colors.card,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
text: host,
|
||||||
|
style: appStyle.fonts.B_14R.copyWith(
|
||||||
|
fontSize: 16,
|
||||||
|
color: appStyle.colors.textPrimary,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: path,
|
||||||
|
style: appStyle.fonts.B_14R.copyWith(
|
||||||
|
fontSize: 16,
|
||||||
|
color: appStyle.colors.textTeritary ??
|
||||||
|
appStyle.colors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Container(
|
||||||
|
width: 34,
|
||||||
|
height: 34,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: appStyle.colors.buttonSecondaryFill,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: appStyle.colors.shadowColor,
|
||||||
|
offset: const Offset(0, 1),
|
||||||
|
blurRadius: appStyle.colors.shadowBlur.toDouble(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Image.asset(
|
||||||
|
"assets/icons/button/colorwheel.png",
|
||||||
|
width: 22,
|
||||||
|
height: 22,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Container(
|
||||||
|
width: 34,
|
||||||
|
height: 34,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: appStyle.colors.buttonSecondaryFill,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: appStyle.colors.shadowColor,
|
||||||
|
offset: const Offset(0, 1),
|
||||||
|
blurRadius: appStyle.colors.shadowBlur.toDouble(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Majesticon(
|
||||||
|
Majesticon.chevronLeftLine,
|
||||||
|
color: appStyle.colors.buttonDisabledIcon,
|
||||||
|
size: 22,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Container(
|
||||||
|
width: 34,
|
||||||
|
height: 34,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: appStyle.colors.buttonSecondaryFill,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: appStyle.colors.shadowColor,
|
||||||
|
offset: const Offset(0, 1),
|
||||||
|
blurRadius: appStyle.colors.shadowBlur.toDouble(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Majesticon(
|
||||||
|
Majesticon.menuLine,
|
||||||
|
color: appStyle.colors.buttonDisabledIcon,
|
||||||
|
size: 22,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,68 +1,44 @@
|
|||||||
|
import 'package:firka/app/app_state.dart';
|
||||||
|
import 'package:firka/core/extensions.dart';
|
||||||
|
import 'package:firka/core/settings.dart';
|
||||||
|
import 'package:firka_common/firka_common.dart';
|
||||||
|
import 'package:firka_common/ui/components/filled_circle.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import 'package:kreta_api/kreta_api.dart';
|
import 'package:kreta_api/kreta_api.dart';
|
||||||
import 'package:firka/routing/chart_interaction_scope.dart';
|
import 'package:firka/routing/chart_interaction_scope.dart';
|
||||||
import 'package:firka/ui/components/grade_helpers.dart';
|
|
||||||
import 'package:firka/ui/theme/style.dart';
|
|
||||||
import 'package:fl_chart/fl_chart.dart';
|
import 'package:fl_chart/fl_chart.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class GradeChart extends StatefulWidget {
|
class GradeChart extends StatefulWidget {
|
||||||
final List<Grade> grades;
|
final List<Grade> grades;
|
||||||
const GradeChart({super.key, required this.grades});
|
final bool halfYearFallback;
|
||||||
|
GradeChart({
|
||||||
|
super.key,
|
||||||
|
required List<Grade> grades,
|
||||||
|
this.halfYearFallback = true,
|
||||||
|
}) : grades = grades.where((g) => g.hasClassicValue()).toList()
|
||||||
|
..sort((a, b) => a.recordDate.compareTo(b.recordDate));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<GradeChart> createState() => _GradeChartState();
|
State<GradeChart> createState() => _GradeChartState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DateSpot extends FlSpot {
|
||||||
|
final DateTime date;
|
||||||
|
|
||||||
|
const DateSpot(super.x, super.y, this.date);
|
||||||
|
|
||||||
|
DateSpot copyWithX(double x) {
|
||||||
|
return DateSpot(x, y, date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _GradeChartState extends State<GradeChart> {
|
class _GradeChartState extends State<GradeChart> {
|
||||||
bool _tooltipActive = false;
|
|
||||||
double? _tooltipY;
|
double? _tooltipY;
|
||||||
int? _touchedIndex;
|
int? _touchedIndex;
|
||||||
|
DateFormat tooltipFormat = DateFormat("MM.dd.");
|
||||||
|
|
||||||
List<Color> gradientColors = [
|
late List<DateSpot> spots;
|
||||||
appStyle.colors.grade5,
|
|
||||||
appStyle.colors.grade4,
|
|
||||||
appStyle.colors.grade3,
|
|
||||||
appStyle.colors.grade2,
|
|
||||||
appStyle.colors.grade1,
|
|
||||||
];
|
|
||||||
|
|
||||||
late List<FlSpot> spots;
|
|
||||||
|
|
||||||
double? _subjectAverageInList(List<Grade> grades, String subjectUid) {
|
|
||||||
double weightedSum = 0;
|
|
||||||
double totalWeight = 0;
|
|
||||||
for (final g in grades) {
|
|
||||||
if (g.subject.uid != subjectUid) continue;
|
|
||||||
final v = g.numericValue;
|
|
||||||
final w = g.weightPercentage;
|
|
||||||
if (v != null && w != null) {
|
|
||||||
final effectiveValue = g.valueType.name == "Szazalekos"
|
|
||||||
? percentageToGrade(v).toDouble()
|
|
||||||
: v.toDouble();
|
|
||||||
weightedSum += effectiveValue * w;
|
|
||||||
totalWeight += w;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return totalWeight > 0 ? weightedSum / totalWeight : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
double _runningSubjectAverage(List<Grade> sortedGrades, int upToInclusive) {
|
|
||||||
final sublist = sortedGrades.sublist(
|
|
||||||
0,
|
|
||||||
(upToInclusive + 1).clamp(0, sortedGrades.length),
|
|
||||||
);
|
|
||||||
final subjectUids = sublist.map((g) => g.subject.uid).toSet();
|
|
||||||
double sum = 0;
|
|
||||||
int count = 0;
|
|
||||||
for (final uid in subjectUids) {
|
|
||||||
final avg = _subjectAverageInList(sublist, uid);
|
|
||||||
if (avg != null) {
|
|
||||||
sum += avg;
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count > 0 ? sum / count : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -71,25 +47,42 @@ class _GradeChartState extends State<GradeChart> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _computeSpots() {
|
void _computeSpots() {
|
||||||
final sortedGrades = List<Grade>.from(widget.grades)
|
spots = [];
|
||||||
..sort((a, b) => a.creationDate.compareTo(b.creationDate));
|
for (var i = 0; i < widget.grades.length; i++) {
|
||||||
|
final grade = widget.grades[i];
|
||||||
|
if (!grade.shouldIncludeInAverage()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (sortedGrades.isEmpty) {
|
final partialAvg = widget.grades
|
||||||
spots = [const FlSpot(0, 0)];
|
.take(i + 1)
|
||||||
return;
|
.getSubjectAverage(halfYearFallback: widget.halfYearFallback);
|
||||||
|
|
||||||
|
spots.add(
|
||||||
|
DateSpot(spots.length.toDouble(), partialAvg!, grade.recordDate),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
spots = [];
|
if (spots.isEmpty) {
|
||||||
for (var i = 0; i < sortedGrades.length; i++) {
|
for (final grade in widget.grades) {
|
||||||
final grade = sortedGrades[i];
|
if (grade.hasClassicValue()) {
|
||||||
if (grade.numericValue != null && grade.weightPercentage != null) {
|
spots.add(
|
||||||
final partialAvg = _runningSubjectAverage(sortedGrades, i);
|
DateSpot(
|
||||||
spots.add(FlSpot(i.toDouble(), partialAvg));
|
spots.length.toDouble(),
|
||||||
|
grade.numericValue!.toDouble(),
|
||||||
|
grade.recordDate,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (spots.isEmpty) {
|
if (spots.isEmpty) {
|
||||||
spots = [const FlSpot(0, 0)];
|
spots.add(DateSpot(0, 0, DateTime.now()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spots.length == 1) {
|
||||||
|
spots = [spots[0], spots[0].copyWithX(1)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,48 +117,57 @@ class _GradeChartState extends State<GradeChart> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ClipRRect(
|
return FirkaCard.single(
|
||||||
borderRadius: BorderRadius.circular(12),
|
margin: EdgeInsets.all(0),
|
||||||
child: DecoratedBox(
|
child: ClipRRect(
|
||||||
decoration: BoxDecoration(color: appStyle.colors.card),
|
borderRadius: BorderRadius.circular(16),
|
||||||
child: AspectRatio(
|
child: AspectRatio(aspectRatio: 1.82, child: LineChart(avgData())),
|
||||||
aspectRatio: 1.90,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
right: 28,
|
|
||||||
left: 12,
|
|
||||||
top: 6,
|
|
||||||
bottom: 12,
|
|
||||||
),
|
|
||||||
child: LineChart(avgData()),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget bottomTitleWidgets(double value, TitleMeta meta) {
|
Widget bottomTitleWidgets(double value, TitleMeta meta) {
|
||||||
final style = TextStyle(
|
|
||||||
fontFamily: appStyle.fonts.B_16R.fontFamily,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 16,
|
|
||||||
color: appStyle.colors.textSecondary,
|
|
||||||
);
|
|
||||||
|
|
||||||
final firstX = spots.first.x.toInt();
|
final firstX = spots.first.x.toInt();
|
||||||
final lastX = spots.last.x.toInt();
|
final lastX = spots.last.x.toInt();
|
||||||
String text = '';
|
String content = '';
|
||||||
const epsilon = 0.01;
|
|
||||||
|
|
||||||
if ((value - firstX).abs() < epsilon) {
|
final shouldHideNow =
|
||||||
text = 'Szeptember';
|
_touchedIndex != null &&
|
||||||
} else if ((value - lastX).abs() < epsilon) {
|
meta.parentAxisSize / lastX * _touchedIndex! > meta.parentAxisSize - 62;
|
||||||
text = 'Most';
|
|
||||||
|
if (value == _touchedIndex) {
|
||||||
|
final date = spots[_touchedIndex!].date;
|
||||||
|
content = tooltipFormat.format(date);
|
||||||
|
} else if (value == firstX) {
|
||||||
|
content = DateFormat("MMMM", initData.l10n.localeName).format(DateTime(DateTime.now().year, DateTime.september)).firstUpper();
|
||||||
|
} else if (value == lastX && !shouldHideNow) {
|
||||||
|
content = initData.l10n.now;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final text = Text(
|
||||||
|
content,
|
||||||
|
style: appStyle.fonts.B_12R.apply(color: appStyle.colors.textSecondary),
|
||||||
|
);
|
||||||
|
|
||||||
return SideTitleWidget(
|
return SideTitleWidget(
|
||||||
meta: meta,
|
meta: meta,
|
||||||
child: Text(text, style: style),
|
space: 0,
|
||||||
|
fitInside: SideTitleFitInsideData.fromTitleMeta(
|
||||||
|
meta,
|
||||||
|
distanceFromEdge: value == lastX ? 12 : 0,
|
||||||
|
),
|
||||||
|
child: value == _touchedIndex
|
||||||
|
? Container(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||||
|
decoration: ShapeDecoration(
|
||||||
|
color: appStyle.colors.background,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(27),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: text,
|
||||||
|
)
|
||||||
|
: text,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,60 +176,66 @@ class _GradeChartState extends State<GradeChart> {
|
|||||||
required Color bgColor,
|
required Color bgColor,
|
||||||
required Color textColor,
|
required Color textColor,
|
||||||
}) {
|
}) {
|
||||||
return SizedBox(
|
return FilledCircle(
|
||||||
width: 24,
|
diameter: 18,
|
||||||
height: 24,
|
color: bgColor,
|
||||||
child: Material(
|
child: Text(
|
||||||
shape: const CircleBorder(),
|
text,
|
||||||
color: bgColor,
|
textAlign: TextAlign.center,
|
||||||
child: Center(
|
style: appStyle.fonts.H_14px.copyWith(color: textColor),
|
||||||
child: Text(
|
|
||||||
text,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(
|
|
||||||
color: textColor,
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontFamily: appStyle.fonts.B_14SB.fontFamily,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Color colorForY(double y) {
|
||||||
|
final rounding = initData.settings
|
||||||
|
.group("settings")
|
||||||
|
.subGroup("application")
|
||||||
|
.subGroup("rounding");
|
||||||
|
return y == 0
|
||||||
|
? appStyle.colors.card
|
||||||
|
: getGradeColor(
|
||||||
|
y,
|
||||||
|
t1: rounding.dbl("1"),
|
||||||
|
t2: rounding.dbl("2"),
|
||||||
|
t3: rounding.dbl("3"),
|
||||||
|
t4: rounding.dbl("4"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget leftTitleWidgets(double value, TitleMeta meta) {
|
Widget leftTitleWidgets(double value, TitleMeta meta) {
|
||||||
String text = switch (value.toInt()) {
|
if (value == 0) {
|
||||||
1 => '1',
|
return SizedBox();
|
||||||
2 => '2',
|
|
||||||
3 => '3',
|
|
||||||
4 => '4',
|
|
||||||
5 => '5',
|
|
||||||
_ => '',
|
|
||||||
};
|
|
||||||
Color gradeColor;
|
|
||||||
if (text != "") {
|
|
||||||
gradeColor = getGradeColor(int.parse(text).toDouble());
|
|
||||||
} else {
|
|
||||||
gradeColor = getGradeColor(0);
|
|
||||||
}
|
}
|
||||||
final currentValue = _tooltipActive && _tooltipY != null
|
|
||||||
? _tooltipY!.round()
|
final rounding = initData.settings
|
||||||
: spots.last.y.round();
|
.group("settings")
|
||||||
final isActive = text == currentValue.toString();
|
.subGroup("application")
|
||||||
|
.subGroup("rounding");
|
||||||
|
final currentValue = spots.first.y == 0
|
||||||
|
? 0
|
||||||
|
: roundGrade(
|
||||||
|
_tooltipY ?? spots.first.y,
|
||||||
|
t1: rounding.dbl("1"),
|
||||||
|
t2: rounding.dbl("2"),
|
||||||
|
t3: rounding.dbl("3"),
|
||||||
|
t4: rounding.dbl("4"),
|
||||||
|
);
|
||||||
|
final isActive = value == currentValue;
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
|
final gradeColor = getGradeColor(value);
|
||||||
return buildCircle(
|
return buildCircle(
|
||||||
text: text,
|
text: value.toStringAsFixed(0),
|
||||||
bgColor: gradeColor.withAlpha(38),
|
bgColor: gradeColor.withAlpha(38),
|
||||||
textColor: gradeColor,
|
textColor: gradeColor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildCircle(
|
return buildCircle(
|
||||||
text: text,
|
text: value.toStringAsFixed(0),
|
||||||
bgColor: appStyle.colors.card,
|
bgColor: appStyle.colors.card,
|
||||||
textColor: appStyle.colors.textPrimary.withValues(alpha: 0.2),
|
textColor: appStyle.colors.textTertiary,
|
||||||
);
|
);
|
||||||
|
|
||||||
// return Text(text, style: style, textAlign: TextAlign.left);
|
// return Text(text, style: style, textAlign: TextAlign.left);
|
||||||
@@ -236,29 +244,6 @@ class _GradeChartState extends State<GradeChart> {
|
|||||||
LineChartData avgData() {
|
LineChartData avgData() {
|
||||||
final smoothedSpots = _smoothSpots(spots);
|
final smoothedSpots = _smoothSpots(spots);
|
||||||
|
|
||||||
var firstX = smoothedSpots.first.x;
|
|
||||||
var lastX = smoothedSpots.last.x;
|
|
||||||
if (firstX == lastX) {
|
|
||||||
lastX = firstX + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Color colorForY(double y) {
|
|
||||||
switch (y.round()) {
|
|
||||||
case 1:
|
|
||||||
return appStyle.colors.grade1;
|
|
||||||
case 2:
|
|
||||||
return appStyle.colors.grade2;
|
|
||||||
case 3:
|
|
||||||
return appStyle.colors.grade3;
|
|
||||||
case 4:
|
|
||||||
return appStyle.colors.grade4;
|
|
||||||
case 5:
|
|
||||||
return appStyle.colors.grade5;
|
|
||||||
default:
|
|
||||||
return appStyle.colors.grade1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return LineChartData(
|
return LineChartData(
|
||||||
lineTouchData: LineTouchData(
|
lineTouchData: LineTouchData(
|
||||||
handleBuiltInTouches: true,
|
handleBuiltInTouches: true,
|
||||||
@@ -266,49 +251,41 @@ class _GradeChartState extends State<GradeChart> {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
touchCallback: (FlTouchEvent event, LineTouchResponse? response) {
|
touchCallback: (FlTouchEvent event, LineTouchResponse? response) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (event is FlLongPressEnd ||
|
if (event.isInterestedForInteractions) {
|
||||||
event is FlPanEndEvent ||
|
_touchedIndex = response!.lineBarSpots!.first.spotIndex;
|
||||||
event is FlTapUpEvent) {
|
_tooltipY = spots[_touchedIndex!].y;
|
||||||
_tooltipActive = false;
|
if (_tooltipY! > 0) {
|
||||||
_tooltipY = null;
|
return;
|
||||||
_touchedIndex = null;
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response?.lineBarSpots != null &&
|
|
||||||
response!.lineBarSpots!.isNotEmpty) {
|
|
||||||
final spot = response.lineBarSpots!.first;
|
|
||||||
|
|
||||||
_tooltipActive = true;
|
|
||||||
_tooltipY = spot.y;
|
|
||||||
_touchedIndex = spot.spotIndex;
|
|
||||||
}
|
}
|
||||||
|
_tooltipY = null;
|
||||||
|
_touchedIndex = null;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
touchTooltipData: LineTouchTooltipData(
|
touchTooltipData: LineTouchTooltipData(
|
||||||
tooltipMargin: 0,
|
tooltipMargin: 4,
|
||||||
getTooltipColor: (touchedSpot) => appStyle.colors.buttonSecondaryFill,
|
getTooltipColor: (touchedSpot) => appStyle.colors.buttonSecondaryFill,
|
||||||
tooltipBorderRadius: BorderRadius.circular(90),
|
tooltipBorderRadius: BorderRadius.circular(27),
|
||||||
fitInsideVertically: true,
|
tooltipPadding: EdgeInsets.symmetric(vertical: 2, horizontal: 6),
|
||||||
|
fitInsideHorizontally: true,
|
||||||
|
|
||||||
showOnTopOfTheChartBoxArea: true,
|
showOnTopOfTheChartBoxArea: false,
|
||||||
getTooltipItems: (touchedSpots) {
|
getTooltipItems: (touchedSpots) {
|
||||||
return touchedSpots.map((LineBarSpot touchedSpot) {
|
return touchedSpots.map((LineBarSpot touchedSpot) {
|
||||||
final textStyle = TextStyle(
|
if (touchedSpot.y == 0) {
|
||||||
color: colorForY(touchedSpot.y),
|
return null;
|
||||||
fontWeight: FontWeight.bold,
|
}
|
||||||
fontSize: 14,
|
final spot = spots[touchedSpot.spotIndex];
|
||||||
);
|
final textStyle = appStyle.fonts.B_14SB.apply(
|
||||||
return LineTooltipItem(
|
color: colorForY(spot.y),
|
||||||
touchedSpot.y.toStringAsFixed(2),
|
|
||||||
textStyle,
|
|
||||||
);
|
);
|
||||||
|
return LineTooltipItem(spot.y.toStringAsFixed(2), textStyle);
|
||||||
}).toList();
|
}).toList();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
getTouchedSpotIndicator: (barData, spotIndexes) {
|
getTouchedSpotIndicator: (barData, spotIndexes) {
|
||||||
return spotIndexes.map((index) {
|
return spotIndexes.map((index) {
|
||||||
final touchedSpot = barData.spots[index];
|
final touchedSpot = spots[index];
|
||||||
return TouchedSpotIndicatorData(
|
return TouchedSpotIndicatorData(
|
||||||
FlLine(color: colorForY(touchedSpot.y), strokeWidth: 3),
|
FlLine(color: colorForY(touchedSpot.y), strokeWidth: 3),
|
||||||
FlDotData(show: false),
|
FlDotData(show: false),
|
||||||
@@ -316,33 +293,24 @@ class _GradeChartState extends State<GradeChart> {
|
|||||||
}).toList();
|
}).toList();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
backgroundColor: appStyle.colors.card,
|
backgroundColor: Colors.transparent,
|
||||||
|
extraLinesData: ExtraLinesData(
|
||||||
|
horizontalLines: [
|
||||||
|
HorizontalLine(
|
||||||
|
y: 5,
|
||||||
|
color: const Color(0xFFC8C8C8),
|
||||||
|
strokeWidth: 1.0,
|
||||||
|
dashArray: [8, 12],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
extraLinesOnTop: false,
|
||||||
|
),
|
||||||
gridData: FlGridData(
|
gridData: FlGridData(
|
||||||
show: true,
|
show: true,
|
||||||
drawHorizontalLine: true,
|
drawHorizontalLine: true,
|
||||||
drawVerticalLine: false,
|
drawVerticalLine: false,
|
||||||
horizontalInterval: 1,
|
horizontalInterval: 1,
|
||||||
getDrawingHorizontalLine: (value) {
|
getDrawingHorizontalLine: (value) {
|
||||||
if (!_tooltipActive || _tooltipY == null) {
|
|
||||||
return FlLine(
|
|
||||||
color: const Color(0xFFC8C8C8),
|
|
||||||
strokeWidth: 1.0,
|
|
||||||
dashArray: [8, 12],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const epsilon = 0.01;
|
|
||||||
if ((value - _tooltipY!.round()).abs() < epsilon) {
|
|
||||||
// return FlLine(
|
|
||||||
// color: const Color(0xFFC8C8C8),
|
|
||||||
// strokeWidth: 1.2,
|
|
||||||
// );
|
|
||||||
return FlLine(
|
|
||||||
color: const Color(0xFFC8C8C8),
|
|
||||||
strokeWidth: 1.0,
|
|
||||||
dashArray: [8, 12],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return FlLine(
|
return FlLine(
|
||||||
color: const Color(0xFFC8C8C8),
|
color: const Color(0xFFC8C8C8),
|
||||||
strokeWidth: 1.0,
|
strokeWidth: 1.0,
|
||||||
@@ -355,30 +323,35 @@ class _GradeChartState extends State<GradeChart> {
|
|||||||
bottomTitles: AxisTitles(
|
bottomTitles: AxisTitles(
|
||||||
sideTitles: SideTitles(
|
sideTitles: SideTitles(
|
||||||
showTitles: true,
|
showTitles: true,
|
||||||
reservedSize: 30,
|
|
||||||
getTitlesWidget: bottomTitleWidgets,
|
getTitlesWidget: bottomTitleWidgets,
|
||||||
interval: 1,
|
interval: 1,
|
||||||
),
|
),
|
||||||
|
drawBelowEverything: false,
|
||||||
|
sideTitleAlignment: SideTitleAlignment.inside,
|
||||||
),
|
),
|
||||||
leftTitles: AxisTitles(
|
leftTitles: AxisTitles(
|
||||||
sideTitles: SideTitles(
|
sideTitles: SideTitles(
|
||||||
showTitles: true,
|
showTitles: true,
|
||||||
getTitlesWidget: leftTitleWidgets,
|
getTitlesWidget: leftTitleWidgets,
|
||||||
reservedSize: 35,
|
reservedSize: 26,
|
||||||
interval: 1,
|
interval: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
topTitles: AxisTitles(
|
||||||
|
sideTitles: SideTitles(
|
||||||
|
showTitles: true,
|
||||||
|
reservedSize: 20,
|
||||||
|
getTitlesWidget: (v, meta) => SizedBox(),
|
||||||
|
),
|
||||||
|
),
|
||||||
rightTitles: const AxisTitles(
|
rightTitles: const AxisTitles(
|
||||||
sideTitles: SideTitles(showTitles: false),
|
sideTitles: SideTitles(showTitles: false),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
borderData: FlBorderData(show: false),
|
borderData: FlBorderData(show: false),
|
||||||
|
|
||||||
minX: firstX,
|
|
||||||
maxX: lastX,
|
|
||||||
minY: 0,
|
minY: 0,
|
||||||
maxY: 6,
|
maxY: 5,
|
||||||
|
|
||||||
lineBarsData: [
|
lineBarsData: [
|
||||||
LineChartBarData(
|
LineChartBarData(
|
||||||
@@ -411,8 +384,13 @@ class _GradeChartState extends State<GradeChart> {
|
|||||||
/// so the navigator does not intercept touch/drag (e.g. for swipe back).
|
/// so the navigator does not intercept touch/drag (e.g. for swipe back).
|
||||||
class GradeChartWithInteraction extends StatelessWidget {
|
class GradeChartWithInteraction extends StatelessWidget {
|
||||||
final List<Grade> grades;
|
final List<Grade> grades;
|
||||||
|
final bool halfYearFallback;
|
||||||
|
|
||||||
const GradeChartWithInteraction({super.key, required this.grades});
|
const GradeChartWithInteraction({
|
||||||
|
super.key,
|
||||||
|
required this.grades,
|
||||||
|
this.halfYearFallback = true,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -421,7 +399,7 @@ class GradeChartWithInteraction extends StatelessWidget {
|
|||||||
onPointerDown: (_) => ChartInteractionScope.of(context).value = true,
|
onPointerDown: (_) => ChartInteractionScope.of(context).value = true,
|
||||||
onPointerUp: (_) => ChartInteractionScope.of(context).value = false,
|
onPointerUp: (_) => ChartInteractionScope.of(context).value = false,
|
||||||
onPointerCancel: (_) => ChartInteractionScope.of(context).value = false,
|
onPointerCancel: (_) => ChartInteractionScope.of(context).value = false,
|
||||||
child: GradeChart(grades: grades),
|
child: GradeChart(grades: grades, halfYearFallback: halfYearFallback),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:firka/core/average_helper.dart';
|
import 'package:firka/ui/components/firka_card.dart';
|
||||||
import 'package:kreta_api/kreta_api.dart';
|
import 'package:kreta_api/kreta_api.dart';
|
||||||
import 'package:firka/ui/components/grade.dart';
|
import 'package:firka/ui/components/grade.dart';
|
||||||
import 'package:firka/ui/components/grade_helpers.dart';
|
import 'package:firka/ui/components/grade_helpers.dart';
|
||||||
@@ -30,98 +30,79 @@ class _GradeSummaryBarState extends State<GradeSummaryBar> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final (total, countsByGrade) = getGradeDistribution(widget.grades);
|
final (total, countsByGrade) = widget.grades.getGradeDistribution();
|
||||||
final gradeColors = [
|
|
||||||
appStyle.colors.grade1,
|
|
||||||
appStyle.colors.grade2,
|
|
||||||
appStyle.colors.grade3,
|
|
||||||
appStyle.colors.grade4,
|
|
||||||
appStyle.colors.grade5,
|
|
||||||
];
|
|
||||||
final totalCounted = countsByGrade.reduce((a, b) => a + b);
|
|
||||||
final averageText = widget.showAverage
|
final averageText = widget.showAverage
|
||||||
? calculateAverage(widget.grades).toStringAsFixed(2)
|
? (widget.grades.getAverage() ?? 0).toStringAsFixed(2)
|
||||||
: '';
|
: '';
|
||||||
|
final bar = ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Row(
|
||||||
|
children: List.generate(5, (i) {
|
||||||
|
final flex = total > 0 ? countsByGrade[i] : 1;
|
||||||
|
return Expanded(
|
||||||
|
flex: flex,
|
||||||
|
child: Container(height: 12, color: getGradeColor(i + 1)),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return Card(
|
return GestureDetector(
|
||||||
shadowColor: Colors.transparent,
|
onTap: () => setState(() => _expanded = !_expanded),
|
||||||
color: appStyle.colors.a15p,
|
child: FirkaCard.single(
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
margin: EdgeInsets.zero,
|
||||||
child: InkWell(
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||||
onTap: () => setState(() => _expanded = !_expanded),
|
height: _expanded ? 115 : 52,
|
||||||
borderRadius: BorderRadius.circular(16),
|
child: Column(
|
||||||
child: Padding(
|
mainAxisSize: MainAxisSize.min,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
child: Column(
|
spacing: 12,
|
||||||
mainAxisSize: MainAxisSize.min,
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
Row(
|
||||||
children: [
|
spacing: 12,
|
||||||
Row(
|
children: [
|
||||||
children: [
|
Text(
|
||||||
Text(
|
widget.showAverage
|
||||||
widget.showAverage
|
? '${widget.l10n.gradesCount(total)} ($averageText)'
|
||||||
? '${widget.l10n.gradesCount(total)} ($averageText)'
|
: widget.l10n.gradesCount(total),
|
||||||
: widget.l10n.gradesCount(total),
|
style: appStyle.fonts.B_16SB.apply(
|
||||||
style: appStyle.fonts.B_16SB.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
child: Row(
|
|
||||||
children: List.generate(5, (i) {
|
|
||||||
final flex = totalCounted > 0 ? countsByGrade[i] : 1;
|
|
||||||
return Expanded(
|
|
||||||
flex: flex,
|
|
||||||
child: Container(height: 10, color: gradeColors[i]),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
FirkaIconWidget(
|
|
||||||
FirkaIconType.majesticons,
|
|
||||||
_expanded
|
|
||||||
? Majesticon.chevronUpLine
|
|
||||||
: Majesticon.chevronDownLine,
|
|
||||||
color: appStyle.colors.textPrimary,
|
color: appStyle.colors.textPrimary,
|
||||||
size: 24,
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
Expanded(child: _expanded ? SizedBox() : bar),
|
||||||
if (_expanded) ...[
|
FirkaIconWidget(
|
||||||
const SizedBox(height: 12),
|
FirkaIconType.majesticons,
|
||||||
Row(
|
_expanded
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
? Majesticon.chevronUpLine
|
||||||
children: List.generate(5, (i) {
|
: Majesticon.chevronDownLine,
|
||||||
final grade = i + 1;
|
color: appStyle.colors.textPrimary,
|
||||||
return Row(
|
size: 24,
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: 32,
|
|
||||||
height: 32,
|
|
||||||
child: FittedBox(
|
|
||||||
child: GradeWidget.gradeValue(grade),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
countsByGrade[i].toString(),
|
|
||||||
style: appStyle.fonts.B_16SB.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
if (_expanded) ...[
|
||||||
|
bar,
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: List.generate(5, (i) {
|
||||||
|
final grade = i + 1;
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
spacing: 6,
|
||||||
|
children: [
|
||||||
|
GradeWidget.gradeValue(grade, size: 27),
|
||||||
|
Text(
|
||||||
|
countsByGrade[i].toString(),
|
||||||
|
style: appStyle.fonts.B_16SB.apply(
|
||||||
|
color: appStyle.colors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import 'package:firka/core/extensions.dart';
|
||||||
import 'package:firka/ui/components/firka_card.dart';
|
import 'package:firka/ui/components/firka_card.dart';
|
||||||
import 'package:firka/l10n/app_localizations.dart';
|
import 'package:firka/l10n/app_localizations.dart';
|
||||||
import 'package:firka/ui/theme/style.dart';
|
import 'package:firka/ui/theme/style.dart';
|
||||||
import 'package:firka/ui/shared/counter_digit.dart';
|
import 'package:firka/ui/shared/counter_digit.dart';
|
||||||
|
import 'package:firka_common/firka_common.dart';
|
||||||
|
import 'package:firka_common/ui/components/filled_circle.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:kreta_api/kreta_api.dart';
|
import 'package:kreta_api/kreta_api.dart';
|
||||||
@@ -15,7 +18,7 @@ class StartingSoonWidget extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var diff = lessons.first.start.difference(now);
|
var diff = lessons.first.start.difference(now.min(lessons.first.start));
|
||||||
var hour = diff.inHours % 60;
|
var hour = diff.inHours % 60;
|
||||||
var min = diff.inMinutes % 60;
|
var min = diff.inMinutes % 60;
|
||||||
var sec = diff.inSeconds % 60;
|
var sec = diff.inSeconds % 60;
|
||||||
@@ -24,89 +27,72 @@ class StartingSoonWidget extends StatelessWidget {
|
|||||||
var minTxt = hour == 1 ? l10n.starting_min : l10n.starting_min_plural;
|
var minTxt = hour == 1 ? l10n.starting_min : l10n.starting_min_plural;
|
||||||
var secTxt = hour == 1 ? l10n.starting_sec : l10n.starting_sec_plural;
|
var secTxt = hour == 1 ? l10n.starting_sec : l10n.starting_sec_plural;
|
||||||
|
|
||||||
return Column(
|
return FirkaCard.single(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
margin: EdgeInsets.only(bottom: 1),
|
||||||
children: [
|
padding: EdgeInsets.all(16),
|
||||||
FirkaCard(
|
child: Column(
|
||||||
attached: Attach.bottom,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
left: [
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
Column(
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
FilledCircle(
|
||||||
children: [
|
diameter: 32,
|
||||||
SizedBox(width: 6),
|
color: appStyle.colors.a15p,
|
||||||
Text(
|
child: FirkaIconWidget(
|
||||||
l10n.starting_soon,
|
FirkaIconType.majesticonsLocal,
|
||||||
style: appStyle.fonts.H_16px.apply(
|
"sunSolid",
|
||||||
color: appStyle.colors.textPrimary,
|
size: 20,
|
||||||
),
|
color: appStyle.colors.accent,
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
Row(
|
),
|
||||||
children: [
|
SizedBox(width: 8),
|
||||||
CounterDigitWidget(
|
Text(
|
||||||
hour.toString(),
|
l10n.starting_soon,
|
||||||
appStyle.fonts.H_16px.apply(
|
style: appStyle.fonts.B_16SB.apply(
|
||||||
color: appStyle.colors.textPrimary,
|
color: appStyle.colors.textPrimary,
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 2),
|
|
||||||
Text(
|
|
||||||
hourTxt,
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 4),
|
|
||||||
CounterDigitWidget(
|
|
||||||
(min / 10).floor().toString(),
|
|
||||||
appStyle.fonts.H_16px.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
CounterDigitWidget(
|
|
||||||
((min % 10)).toString(),
|
|
||||||
appStyle.fonts.H_16px.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 2),
|
|
||||||
Text(
|
|
||||||
minTxt,
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 4),
|
|
||||||
CounterDigitWidget(
|
|
||||||
(sec / 10).floor().toString(),
|
|
||||||
appStyle.fonts.H_16px.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
CounterDigitWidget(
|
|
||||||
((sec % 10)).toString(),
|
|
||||||
appStyle.fonts.H_16px.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 2),
|
|
||||||
Text(
|
|
||||||
secTxt,
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
right: [],
|
Row(
|
||||||
),
|
children: [
|
||||||
],
|
CounterDigitWidget((hour / 10).floor().toString()),
|
||||||
|
SizedBox(width: 4),
|
||||||
|
CounterDigitWidget(((hour % 10)).toString()),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
hourTxt,
|
||||||
|
style: appStyle.fonts.B_16R.apply(
|
||||||
|
color: appStyle.colors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
CounterDigitWidget((min / 10).floor().toString()),
|
||||||
|
SizedBox(width: 4),
|
||||||
|
CounterDigitWidget(((min % 10)).toString()),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
minTxt,
|
||||||
|
style: appStyle.fonts.B_16R.apply(
|
||||||
|
color: appStyle.colors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
CounterDigitWidget((sec / 10).floor().toString()),
|
||||||
|
SizedBox(width: 4),
|
||||||
|
CounterDigitWidget(((sec % 10)).toString()),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
secTxt,
|
||||||
|
style: appStyle.fonts.B_16R.apply(
|
||||||
|
color: appStyle.colors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
import 'package:kreta_api/kreta_api.dart';
|
|
||||||
import 'package:firka/data/models/homework_cache_model.dart';
|
|
||||||
import 'package:firka/ui/components/common_bottom_sheets.dart';
|
|
||||||
import 'package:firka/ui/components/firka_card.dart';
|
|
||||||
import 'package:firka/app/app_state.dart';
|
|
||||||
import 'package:firka/ui/theme/style.dart';
|
|
||||||
import 'package:firka/ui/shared/firka_icon.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:majesticons_flutter/majesticons_flutter.dart';
|
|
||||||
|
|
||||||
class HomeworkWidget extends StatelessWidget {
|
|
||||||
final AppInitialization data;
|
|
||||||
final Homework item;
|
|
||||||
|
|
||||||
const HomeworkWidget(this.data, this.item, {super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return GestureDetector(
|
|
||||||
child: FirkaCard(
|
|
||||||
left: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
FutureBuilder<bool>(
|
|
||||||
future: isHomeworkDone(data.isar, item.uid),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (!snapshot.hasData) {
|
|
||||||
return SizedBox();
|
|
||||||
}
|
|
||||||
final done = snapshot.data!;
|
|
||||||
return done
|
|
||||||
? FirkaIconWidget(
|
|
||||||
FirkaIconType.majesticonsLocal,
|
|
||||||
"homeWithMark",
|
|
||||||
color: appStyle.colors.accent,
|
|
||||||
size: 24,
|
|
||||||
)
|
|
||||||
: FirkaIconWidget(
|
|
||||||
FirkaIconType.majesticons,
|
|
||||||
Majesticon.homeSolid,
|
|
||||||
color: appStyle.colors.accent,
|
|
||||||
size: 24,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
FutureBuilder<bool>(
|
|
||||||
future: isHomeworkDone(data.isar, item.uid),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (!snapshot.hasData) {
|
|
||||||
return SizedBox();
|
|
||||||
}
|
|
||||||
final done = snapshot.data!;
|
|
||||||
return done
|
|
||||||
? Text(
|
|
||||||
data.l10n.homework,
|
|
||||||
style: appStyle.fonts.B_16SB.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
decoration: TextDecoration.lineThrough,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Text(
|
|
||||||
data.l10n.homework,
|
|
||||||
style: appStyle.fonts.B_16SB.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
item.subjectName,
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
showHomeworkBottomSheet(context, data, item);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import 'package:firka/ui/components/firka_card.dart';
|
|
||||||
import 'package:firka/ui/theme/style.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:kreta_api/kreta_api.dart';
|
|
||||||
|
|
||||||
// TODO: Finish
|
|
||||||
class InfoBoardItemWidget extends StatelessWidget {
|
|
||||||
final InfoBoardItem item;
|
|
||||||
|
|
||||||
const InfoBoardItemWidget(this.item, {super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return FirkaCard(
|
|
||||||
left: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
decoration: ShapeDecoration(
|
|
||||||
color: appStyle.colors.accent,
|
|
||||||
shape: CircleBorder(
|
|
||||||
eccentricity: 1,
|
|
||||||
// borderRadius: BorderRadius.circular(6)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: SizedBox(
|
|
||||||
width: 28,
|
|
||||||
height: 28,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 6),
|
|
||||||
child: Text(
|
|
||||||
item.author[0],
|
|
||||||
style: appStyle.fonts.H_18px.copyWith(
|
|
||||||
fontSize: 20,
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: MediaQuery.of(context).size.width / 1.4,
|
|
||||||
child: Text(
|
|
||||||
item.title,
|
|
||||||
style: appStyle.fonts.B_16SB.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
item.author,
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.textSecondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
235
firka/lib/ui/phone/widgets/info_card.dart
Normal file
235
firka/lib/ui/phone/widgets/info_card.dart
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
import 'package:firka/app/app_state.dart';
|
||||||
|
import 'package:firka/core/extensions.dart';
|
||||||
|
import 'package:firka/data/models/homework_cache_model.dart';
|
||||||
|
import 'package:firka/ui/components/common_bottom_sheets.dart';
|
||||||
|
import 'package:firka/ui/components/firka_card.dart';
|
||||||
|
import 'package:firka/ui/components/grade.dart';
|
||||||
|
import 'package:firka/ui/components/grade_helpers.dart';
|
||||||
|
import 'package:firka/ui/shared/class_icon.dart';
|
||||||
|
import 'package:firka/ui/shared/firka_icon.dart';
|
||||||
|
import 'package:firka/ui/theme/style.dart';
|
||||||
|
import 'package:firka_common/ui/components/filled_circle.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
import 'package:kreta_api/kreta_api.dart';
|
||||||
|
import 'package:majesticons_flutter/majesticons_flutter.dart';
|
||||||
|
|
||||||
|
class InfoCard extends StatelessWidget {
|
||||||
|
final void Function(BuildContext)? onTap;
|
||||||
|
final Widget icon;
|
||||||
|
final List<String> texts;
|
||||||
|
final List<Widget> right;
|
||||||
|
|
||||||
|
final List<TextStyle> textSyles = [
|
||||||
|
appStyle.fonts.B_16SB.apply(color: appStyle.colors.textPrimary),
|
||||||
|
appStyle.fonts.B_14R.apply(color: appStyle.colors.textSecondary),
|
||||||
|
];
|
||||||
|
|
||||||
|
InfoCard({
|
||||||
|
required this.icon,
|
||||||
|
required this.texts,
|
||||||
|
this.right = const [],
|
||||||
|
this.onTap,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
static Widget buildSubject(Color color, Subject subject) {
|
||||||
|
return FilledCircle(
|
||||||
|
diameter: 32,
|
||||||
|
color: color.withAlpha(38),
|
||||||
|
child: ClassIconWidget.subject(subject: subject, color: color, size: 20),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory InfoCard.test(Test test) {
|
||||||
|
final color = appStyle.colors.accent;
|
||||||
|
|
||||||
|
return InfoCard(
|
||||||
|
icon: FilledCircle(
|
||||||
|
diameter: 36,
|
||||||
|
color: color.withAlpha(38),
|
||||||
|
child: FirkaIconWidget(
|
||||||
|
FirkaIconType.majesticons,
|
||||||
|
Majesticon.editPen4Solid,
|
||||||
|
color: color,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
texts: [
|
||||||
|
test.theme?.firstUpper() ?? test.method.description,
|
||||||
|
test.subject.name.firstUpper(),
|
||||||
|
],
|
||||||
|
right: [buildSubject(color, test.subject)],
|
||||||
|
onTap: (context) => showTestBottomSheet(context, initData, test),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory InfoCard.testDesc(Test test) {
|
||||||
|
if (test.theme == null) {
|
||||||
|
return InfoCard.test(test);
|
||||||
|
}
|
||||||
|
|
||||||
|
final color = appStyle.colors.accent;
|
||||||
|
|
||||||
|
return InfoCard(
|
||||||
|
icon: FilledCircle(
|
||||||
|
diameter: 36,
|
||||||
|
color: color.withAlpha(38),
|
||||||
|
child: FirkaIconWidget(
|
||||||
|
FirkaIconType.majesticons,
|
||||||
|
Majesticon.editPen4Solid,
|
||||||
|
color: color,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
texts: [test.theme!.firstUpper(), test.method.description.firstUpper()],
|
||||||
|
right: [buildSubject(color, test.subject)],
|
||||||
|
onTap: (context) => showTestBottomSheet(context, initData, test),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory InfoCard.messageItem(MessageItem item) {
|
||||||
|
return InfoCard(
|
||||||
|
icon: FilledCircle(
|
||||||
|
diameter: 36,
|
||||||
|
color: appStyle.colors.accent,
|
||||||
|
child: Text(
|
||||||
|
item.author[0],
|
||||||
|
style: appStyle.fonts.H_H2.apply(color: appStyle.colors.textPrimary),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
texts: [item.title, item.author],
|
||||||
|
onTap: (context) => context.push('/message', extra: item),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory InfoCard.homework(Homework homework) {
|
||||||
|
return InfoCard(
|
||||||
|
icon: FilledCircle(
|
||||||
|
diameter: 36,
|
||||||
|
color: appStyle.colors.accent.withAlpha(38),
|
||||||
|
child: FutureBuilder<bool>(
|
||||||
|
future: isHomeworkDone(initData.isar, homework.uid),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (!snapshot.hasData) {
|
||||||
|
return SizedBox();
|
||||||
|
}
|
||||||
|
final done = snapshot.data!;
|
||||||
|
return done
|
||||||
|
? FirkaIconWidget(
|
||||||
|
FirkaIconType.majesticonsLocal,
|
||||||
|
"homeWithMark",
|
||||||
|
color: appStyle.colors.accent,
|
||||||
|
size: 24,
|
||||||
|
)
|
||||||
|
: FirkaIconWidget(
|
||||||
|
FirkaIconType.majesticons,
|
||||||
|
Majesticon.homeSolid,
|
||||||
|
color: appStyle.colors.accent,
|
||||||
|
size: 24,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
texts: [initData.l10n.homework, homework.subjectName],
|
||||||
|
right: [buildSubject(appStyle.colors.accent, homework.subject)],
|
||||||
|
onTap: (context) => showHomeworkBottomSheet(context, initData, homework),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory InfoCard.gradeSubj(
|
||||||
|
Grade grade, {
|
||||||
|
void Function(BuildContext)? onTap,
|
||||||
|
}) {
|
||||||
|
String? value = grade.numericValue == null ? grade.strValue : null;
|
||||||
|
return InfoCard(
|
||||||
|
icon: GradeWidget(grade),
|
||||||
|
texts: [
|
||||||
|
(value ??
|
||||||
|
grade.topic ??
|
||||||
|
grade.mode?.description ??
|
||||||
|
grade.type.description!)
|
||||||
|
.firstUpper(),
|
||||||
|
grade.subject.name.firstUpper(),
|
||||||
|
],
|
||||||
|
right: [buildSubject(appStyle.colors.accent, grade.subject)],
|
||||||
|
onTap:
|
||||||
|
onTap ?? (context) => showGradeBottomSheet(context, initData, grade),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory InfoCard.gradeGhost(
|
||||||
|
int gradeValue,
|
||||||
|
int gradeWeigth, {
|
||||||
|
void Function(BuildContext)? onTap,
|
||||||
|
}) {
|
||||||
|
return InfoCard(
|
||||||
|
icon: GradeWidget.gradeValue(gradeValue, gradeWeight: gradeWeigth),
|
||||||
|
texts: ["${initData.l10n.ghost_grade} ($gradeWeigth%)"],
|
||||||
|
right: [],
|
||||||
|
onTap: onTap,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory InfoCard.gradeDesc(
|
||||||
|
Grade grade, {
|
||||||
|
void Function(BuildContext)? onTap,
|
||||||
|
}) {
|
||||||
|
List<String> texts = [
|
||||||
|
(grade.mode?.description ?? grade.type.description!).firstUpper(),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (grade.topic != null) {
|
||||||
|
texts = [grade.topic!.firstUpper(), ...texts];
|
||||||
|
}
|
||||||
|
|
||||||
|
return InfoCard(
|
||||||
|
icon: GradeWidget(grade),
|
||||||
|
texts: texts,
|
||||||
|
right: [buildSubject(appStyle.colors.accent, grade.subject)],
|
||||||
|
onTap:
|
||||||
|
onTap ?? (context) => showGradeBottomSheet(context, initData, grade),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
List<Text> children = [];
|
||||||
|
int i = 0;
|
||||||
|
for (var text in texts) {
|
||||||
|
children.add(
|
||||||
|
Text(text, style: textSyles[i], overflow: TextOverflow.ellipsis),
|
||||||
|
);
|
||||||
|
if (i < textSyles.length) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return GestureDetector(
|
||||||
|
child: FirkaCard.single(
|
||||||
|
height: 68,
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
margin: EdgeInsets.all(0),
|
||||||
|
child: Row(
|
||||||
|
spacing: 12,
|
||||||
|
children: [
|
||||||
|
icon,
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
spacing: 2,
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...right,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
if (onTap == null) return;
|
||||||
|
onTap!.call(context);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,12 +3,12 @@ import 'package:firka/core/settings.dart';
|
|||||||
import 'package:firka/ui/components/firka_card.dart';
|
import 'package:firka/ui/components/firka_card.dart';
|
||||||
import 'package:firka/app/app_state.dart';
|
import 'package:firka/app/app_state.dart';
|
||||||
import 'package:firka/ui/theme/style.dart';
|
import 'package:firka/ui/theme/style.dart';
|
||||||
|
import 'package:firka_common/ui/components/filled_circle.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:majesticons_flutter/majesticons_flutter.dart';
|
import 'package:majesticons_flutter/majesticons_flutter.dart';
|
||||||
|
|
||||||
import 'package:kreta_api/kreta_api.dart';
|
import 'package:kreta_api/kreta_api.dart';
|
||||||
import 'package:firka/core/debug_helper.dart';
|
|
||||||
import 'package:firka/ui/components/common_bottom_sheets.dart';
|
import 'package:firka/ui/components/common_bottom_sheets.dart';
|
||||||
import 'package:firka/ui/shared/class_icon.dart';
|
import 'package:firka/ui/shared/class_icon.dart';
|
||||||
import 'package:firka/ui/shared/firka_icon.dart';
|
import 'package:firka/ui/shared/firka_icon.dart';
|
||||||
@@ -16,24 +16,16 @@ import 'bubble_test.dart';
|
|||||||
|
|
||||||
class LessonWidget extends StatelessWidget {
|
class LessonWidget extends StatelessWidget {
|
||||||
final AppInitialization data;
|
final AppInitialization data;
|
||||||
final List<Lesson> week;
|
|
||||||
final List<Lesson> day;
|
|
||||||
final int? lessonNo;
|
|
||||||
final Lesson lesson;
|
final Lesson lesson;
|
||||||
|
final bool active;
|
||||||
final Test? test;
|
final Test? test;
|
||||||
final Lesson? nextLesson;
|
|
||||||
final bool? placeholderMode;
|
|
||||||
|
|
||||||
const LessonWidget(
|
const LessonWidget(
|
||||||
this.data,
|
this.data,
|
||||||
this.week,
|
|
||||||
this.day,
|
|
||||||
this.lessonNo,
|
|
||||||
this.lesson,
|
this.lesson,
|
||||||
this.test,
|
this.test, {
|
||||||
this.nextLesson, {
|
this.active = false,
|
||||||
super.key,
|
super.key,
|
||||||
this.placeholderMode,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -42,26 +34,22 @@ class LessonWidget extends StatelessWidget {
|
|||||||
.group("settings")
|
.group("settings")
|
||||||
.subGroup("timetable_toast")
|
.subGroup("timetable_toast")
|
||||||
.boolean("tests_and_homework");
|
.boolean("tests_and_homework");
|
||||||
final showSubstitutions = data.settings
|
|
||||||
.group("settings")
|
|
||||||
.subGroup("timetable_toast")
|
|
||||||
.boolean("substitution");
|
|
||||||
final showLessonNos = data.settings
|
|
||||||
.group("settings")
|
|
||||||
.subGroup("timetable_toast")
|
|
||||||
.boolean("lesson_no");
|
|
||||||
final isSubstituted = lesson.substituteTeacher != null;
|
final isSubstituted = lesson.substituteTeacher != null;
|
||||||
|
final showSubstitutions =
|
||||||
|
isSubstituted &&
|
||||||
|
data.settings
|
||||||
|
.group("settings")
|
||||||
|
.subGroup("timetable_toast")
|
||||||
|
.boolean("substitution");
|
||||||
|
final showLessonNos =
|
||||||
|
lesson.lessonNumber != null &&
|
||||||
|
data.settings
|
||||||
|
.group("settings")
|
||||||
|
.subGroup("timetable_toast")
|
||||||
|
.boolean("lesson_no");
|
||||||
final isDismissed = lesson.type.name == "UresOra";
|
final isDismissed = lesson.type.name == "UresOra";
|
||||||
|
|
||||||
var showBreak = false;
|
|
||||||
if (week.isNotEmpty) {
|
|
||||||
showBreak =
|
|
||||||
timeNow().isAfter(lesson.start) && timeNow().isBefore(lesson.end) ||
|
|
||||||
timeNow().isAfter(week.last.end) ||
|
|
||||||
lesson.start.getMidnight() != timeNow().getMidnight() ||
|
|
||||||
timeNow().isAfter(day.last.end) ||
|
|
||||||
timeNow().isBefore(day.first.start);
|
|
||||||
}
|
|
||||||
var accent = appStyle.colors.accent;
|
var accent = appStyle.colors.accent;
|
||||||
var secondary = appStyle.colors.secondary;
|
var secondary = appStyle.colors.secondary;
|
||||||
var bgColor = appStyle.colors.a15p;
|
var bgColor = appStyle.colors.a15p;
|
||||||
@@ -79,408 +67,236 @@ class LessonWidget extends StatelessWidget {
|
|||||||
|
|
||||||
List<Widget> elements = [];
|
List<Widget> elements = [];
|
||||||
|
|
||||||
var subjectName = lesson.subject?.name ?? 'N/A';
|
var subjectName = lesson.subject?.name.firstUpper() ?? 'N/A';
|
||||||
if (subjectName.length >= 19) {
|
|
||||||
subjectName = "${subjectName.substring(0, 19 - 3)}...";
|
|
||||||
}
|
|
||||||
subjectName = subjectName.firstUpper();
|
|
||||||
|
|
||||||
var roomName = lesson.roomName ?? 'N/A';
|
var roomName = lesson.roomName ?? 'N/A';
|
||||||
if (roomName.length >= 11) {
|
|
||||||
roomName = "${roomName.substring(0, 11 - 3)}...";
|
|
||||||
}
|
|
||||||
|
|
||||||
elements.add(
|
elements.add(
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (lessonNo == null) return;
|
|
||||||
showLessonBottomSheet(
|
showLessonBottomSheet(
|
||||||
context,
|
context,
|
||||||
data,
|
data,
|
||||||
lesson,
|
lesson,
|
||||||
lessonNo,
|
|
||||||
accent,
|
accent,
|
||||||
secondary,
|
secondary,
|
||||||
bgColor,
|
bgColor,
|
||||||
test,
|
test,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: FirkaCard(
|
child: FirkaCard.single(
|
||||||
|
height: 64,
|
||||||
|
borderColor: active ? appStyle.colors.accent : null,
|
||||||
|
margin: EdgeInsets.all(0),
|
||||||
|
padding: EdgeInsets.only(left: 14, right: 16),
|
||||||
color: isDismissed
|
color: isDismissed
|
||||||
? appStyle.colors.cardTranslucent
|
? appStyle.colors.cardTranslucent
|
||||||
: appStyle.colors.card,
|
: appStyle.colors.card,
|
||||||
|
attached: showTests && test != null ? Attach.bottom : Attach.none,
|
||||||
shadow: !isDismissed,
|
shadow: !isDismissed,
|
||||||
left: [
|
child: Column(
|
||||||
showLessonNos == false || lessonNo == null
|
spacing: 12,
|
||||||
? SizedBox()
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
: SizedBox(
|
children: [
|
||||||
width: 18,
|
Row(
|
||||||
height: 18,
|
children: [
|
||||||
child: Stack(
|
!showLessonNos
|
||||||
alignment: Alignment.center,
|
? SizedBox()
|
||||||
children: [
|
: SizedBox(
|
||||||
SvgPicture.asset(
|
|
||||||
"assets/icons/subtract.svg",
|
|
||||||
color: bgColor,
|
|
||||||
width: 18,
|
width: 18,
|
||||||
height: 18,
|
height: 18,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
SvgPicture.asset(
|
||||||
|
"assets/icons/subtract.svg",
|
||||||
|
color: bgColor,
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
lesson.lessonNumber!.toString(),
|
||||||
|
style: appStyle.fonts.B_14SB.apply(
|
||||||
|
color: secondary,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Text(
|
FilledCircle(
|
||||||
lessonNo.toString(),
|
diameter: 36,
|
||||||
style: appStyle.fonts.B_12R.apply(color: secondary),
|
color: bgColor,
|
||||||
textAlign: TextAlign.center,
|
child: ClassIconWidget(
|
||||||
),
|
uid: lesson.uid,
|
||||||
],
|
className: lesson.name,
|
||||||
|
category: subjectName,
|
||||||
|
color: accent,
|
||||||
|
size: 24,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Transform.translate(
|
SizedOverflowBox(
|
||||||
offset: Offset(-4, 0),
|
size: Size(12, 0),
|
||||||
child: Card(
|
child: !showTests && test != null
|
||||||
shadowColor: Colors.transparent,
|
|
||||||
color: bgColor,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsetsGeometry.all(4),
|
|
||||||
child: ClassIconWidget(
|
|
||||||
color: accent,
|
|
||||||
size: 20,
|
|
||||||
uid: lesson.uid,
|
|
||||||
className: lesson.name,
|
|
||||||
category: lesson.subject?.name != null
|
|
||||||
? lesson.subject!.name.firstUpper()
|
|
||||||
: '',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
!showTests && test != null
|
|
||||||
? Transform.translate(
|
? Transform.translate(
|
||||||
offset: Offset(26, -18),
|
offset: Offset(4, -20),
|
||||||
child: BubbleTest(),
|
child: BubbleTest(),
|
||||||
)
|
)
|
||||||
: SizedBox(),
|
: SizedBox(),
|
||||||
],
|
),
|
||||||
),
|
Expanded(
|
||||||
),
|
child: Row(
|
||||||
),
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
SizedBox(width: !showTests && test != null ? 16 : 8),
|
spacing: 12,
|
||||||
Text(
|
children: [
|
||||||
subjectName,
|
LimitedBox(
|
||||||
style: appStyle.fonts.B_15SB.apply(
|
maxWidth: 155,
|
||||||
color: appStyle.colors.textPrimary,
|
child: Column(
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
),
|
mainAxisSize: MainAxisSize.min,
|
||||||
],
|
children: [
|
||||||
right: [
|
Text(
|
||||||
placeholderMode == true
|
subjectName,
|
||||||
? SizedBox()
|
style: appStyle.fonts.B_16SB.apply(
|
||||||
: Text(
|
color: !isDismissed
|
||||||
isDismissed
|
? appStyle.colors.textPrimary
|
||||||
? data.l10n.class_dismissed
|
: appStyle.colors.textSecondary,
|
||||||
: lesson.start.toLocal().format(
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
showSubstitutions
|
||||||
|
? Text(
|
||||||
|
lesson.substituteTeacher!,
|
||||||
|
style: appStyle.fonts.B_14R.apply(
|
||||||
|
color: appStyle.colors.textSecondary,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: SizedBox(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isDismissed
|
||||||
|
? Text(
|
||||||
|
data.l10n.class_dismissed,
|
||||||
|
style: appStyle.fonts.B_14R.apply(
|
||||||
|
color: appStyle.colors.textSecondary,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Flexible(
|
||||||
|
fit: FlexFit.loose,
|
||||||
|
child: Card(
|
||||||
|
shadowColor: Colors.transparent,
|
||||||
|
color: appStyle.colors.a15p,
|
||||||
|
margin: EdgeInsets.all(0),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 6,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
roomName,
|
||||||
|
style: appStyle.fonts.B_14R.apply(
|
||||||
|
color: appStyle.colors.secondary,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!isDismissed) SizedBox(width: 8),
|
||||||
|
if (!isDismissed)
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
lesson.start.toLocal().format(
|
||||||
data.l10n,
|
data.l10n,
|
||||||
FormatMode.hmm,
|
FormatMode.hmm,
|
||||||
),
|
),
|
||||||
style: appStyle.fonts.B_14R.apply(
|
style: appStyle.fonts.B_14R.apply(
|
||||||
color: appStyle.colors.textPrimary,
|
color: appStyle.colors.textPrimary,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
placeholderMode == true
|
|
||||||
? SizedBox()
|
|
||||||
: isDismissed
|
|
||||||
? SizedBox()
|
|
||||||
: Card(
|
|
||||||
shadowColor: Colors.transparent,
|
|
||||||
color: appStyle.colors.a15p,
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(5),
|
|
||||||
child: Text(
|
|
||||||
roomName,
|
|
||||||
style: appStyle.fonts.B_12R.apply(
|
|
||||||
color: appStyle.colors.secondary,
|
|
||||||
),
|
),
|
||||||
),
|
Text(
|
||||||
|
lesson.end.toLocal().format(
|
||||||
|
data.l10n,
|
||||||
|
FormatMode.hmm,
|
||||||
|
),
|
||||||
|
style: appStyle.fonts.B_14R.apply(
|
||||||
|
color: appStyle.colors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isSubstituted && showSubstitutions) {
|
|
||||||
elements.add(
|
|
||||||
FirkaCard(
|
|
||||||
left: [
|
|
||||||
Text(
|
|
||||||
data.l10n.class_substitution,
|
|
||||||
style: appStyle.fonts.H_16px.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
right: [
|
|
||||||
Text(
|
|
||||||
lesson.substituteTeacher!,
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.textSecondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (test != null && showTests) {
|
if (test != null && showTests) {
|
||||||
var theme = test!.theme;
|
var theme = test!.theme?.firstUpper() ?? "";
|
||||||
if (theme.length >= 20) {
|
var method = test!.method.description.firstUpper();
|
||||||
theme = "${theme.substring(0, 20)}...";
|
|
||||||
}
|
|
||||||
var method = test!.method.description ?? 'N/A';
|
|
||||||
if (method.length >= 15) {
|
|
||||||
method = "${method.substring(0, 15)}...";
|
|
||||||
}
|
|
||||||
theme = theme.firstUpper();
|
|
||||||
method = method.firstUpper();
|
|
||||||
|
|
||||||
elements.add(
|
elements.add(
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
showTestBottomSheet(
|
showTestBottomSheet(context, data, test!);
|
||||||
context,
|
|
||||||
data,
|
|
||||||
lesson,
|
|
||||||
lessonNo,
|
|
||||||
accent,
|
|
||||||
secondary,
|
|
||||||
bgColor,
|
|
||||||
test,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
child: FirkaCard(
|
child: FirkaCard.single(
|
||||||
left: [
|
height: 48,
|
||||||
FirkaIconWidget(
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||||
FirkaIconType.majesticons,
|
margin: EdgeInsets.only(top: 4),
|
||||||
Majesticon.editPen4Solid,
|
attached: Attach.top,
|
||||||
color: appStyle.colors.accent,
|
child: Row(
|
||||||
),
|
children: [
|
||||||
SizedBox(width: 6),
|
FirkaIconWidget(
|
||||||
Text(
|
FirkaIconType.majesticons,
|
||||||
theme,
|
Majesticon.editPen4Solid,
|
||||||
style: appStyle.fonts.B_16SB.apply(
|
color: appStyle.colors.accent,
|
||||||
color: appStyle.colors.textSecondary,
|
size: 20,
|
||||||
),
|
),
|
||||||
),
|
SizedBox(width: 8),
|
||||||
],
|
LimitedBox(
|
||||||
right: [
|
maxWidth: 160,
|
||||||
Text(
|
child: Text(
|
||||||
method,
|
theme,
|
||||||
style: appStyle.fonts.B_16R.apply(
|
style: appStyle.fonts.B_16SB.apply(
|
||||||
color: appStyle.colors.textTertiary,
|
color: appStyle.colors.textPrimary,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
SizedBox(width: 12),
|
||||||
],
|
Flexible(
|
||||||
|
fit: FlexFit.loose,
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Text(
|
||||||
|
method,
|
||||||
|
style: appStyle.fonts.B_14R.apply(
|
||||||
|
color: appStyle.colors.textSecondary,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextLesson != null) {
|
|
||||||
var breakMins = nextLesson!.start.difference(lesson.end).inMinutes;
|
|
||||||
var seqSchedule = week.getAllSeqs(lesson);
|
|
||||||
|
|
||||||
if (breakMins > 45) {
|
|
||||||
final breakEnd = lesson.end.add(Duration(minutes: breakMins));
|
|
||||||
final emptyClass = seqSchedule.firstWhereOrNull(
|
|
||||||
(lesson2) =>
|
|
||||||
lesson2.start.isAfter(lesson.end) &&
|
|
||||||
lesson2.end.isBefore(breakEnd),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (emptyClass != null) {
|
|
||||||
final preBreak = emptyClass.start.difference(lesson.end).inMinutes;
|
|
||||||
final postBreak = breakEnd.difference(emptyClass.end).inMinutes;
|
|
||||||
|
|
||||||
if (data.settings
|
|
||||||
.group("settings")
|
|
||||||
.subGroup("timetable_toast")
|
|
||||||
.boolean("breaks") &&
|
|
||||||
showBreak) {
|
|
||||||
elements.add(
|
|
||||||
FirkaCard(
|
|
||||||
color: appStyle.colors.cardTranslucent,
|
|
||||||
shadow: false,
|
|
||||||
left: [
|
|
||||||
Text(
|
|
||||||
data.l10n.breakTxt,
|
|
||||||
style: appStyle.fonts.B_16SB.apply(
|
|
||||||
color: appStyle.colors.textSecondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
right: [
|
|
||||||
Text(
|
|
||||||
"$preBreak ${preBreak == 1 ? data.l10n.starting_min : data.l10n.starting_min_plural}",
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.textTertiary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
elements.add(
|
|
||||||
FirkaCard(
|
|
||||||
left: [
|
|
||||||
SizedBox(
|
|
||||||
width: 18,
|
|
||||||
height: 18,
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
SvgPicture.asset(
|
|
||||||
"assets/icons/subtract.svg",
|
|
||||||
color: bgColor,
|
|
||||||
width: 18,
|
|
||||||
height: 18,
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.only(left: 5),
|
|
||||||
child: Text(
|
|
||||||
emptyClass.lessonNumber.toString(),
|
|
||||||
style: appStyle.fonts.B_12R.apply(color: secondary),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Transform.translate(
|
|
||||||
offset: Offset(-4, 0),
|
|
||||||
child: Card(
|
|
||||||
shadowColor: Colors.transparent,
|
|
||||||
color: bgColor,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsetsGeometry.all(4),
|
|
||||||
child: FirkaIconWidget(
|
|
||||||
FirkaIconType.majesticonsLocal,
|
|
||||||
'cupFilled',
|
|
||||||
color: appStyle.colors.accent,
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
data.l10n.empty_class,
|
|
||||||
style: appStyle.fonts.B_16SB.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
right: [
|
|
||||||
Text(
|
|
||||||
isDismissed
|
|
||||||
? data.l10n.class_dismissed
|
|
||||||
: "${emptyClass.start.toLocal().format(data.l10n, FormatMode.hmm)} - ${emptyClass.end.toLocal().format(data.l10n, FormatMode.hmm)}",
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (data.settings
|
|
||||||
.group("settings")
|
|
||||||
.subGroup("timetable_toast")
|
|
||||||
.boolean("breaks") &&
|
|
||||||
showBreak) {
|
|
||||||
elements.add(
|
|
||||||
FirkaCard(
|
|
||||||
color: appStyle.colors.cardTranslucent,
|
|
||||||
shadow: false,
|
|
||||||
left: [
|
|
||||||
Text(
|
|
||||||
data.l10n.breakTxt,
|
|
||||||
style: appStyle.fonts.B_16SB.apply(
|
|
||||||
color: appStyle.colors.textSecondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
right: [
|
|
||||||
Text(
|
|
||||||
"$postBreak ${postBreak == 1 ? data.l10n.starting_min : data.l10n.starting_min_plural}",
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.textTertiary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (data.settings
|
|
||||||
.group("settings")
|
|
||||||
.subGroup("timetable_toast")
|
|
||||||
.boolean("breaks") &&
|
|
||||||
showBreak) {
|
|
||||||
elements.add(
|
|
||||||
FirkaCard(
|
|
||||||
color: appStyle.colors.cardTranslucent,
|
|
||||||
shadow: false,
|
|
||||||
left: [
|
|
||||||
Text(
|
|
||||||
data.l10n.breakTxt,
|
|
||||||
style: appStyle.fonts.B_16SB.apply(
|
|
||||||
color: appStyle.colors.textSecondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
right: [
|
|
||||||
Text(
|
|
||||||
"$breakMins ${breakMins == 1 ? data.l10n.starting_min : data.l10n.starting_min_plural}",
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.textTertiary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (data.settings
|
|
||||||
.group("settings")
|
|
||||||
.subGroup("timetable_toast")
|
|
||||||
.boolean("breaks") &&
|
|
||||||
showBreak) {
|
|
||||||
elements.add(
|
|
||||||
FirkaCard(
|
|
||||||
color: appStyle.colors.cardTranslucent,
|
|
||||||
shadow: false,
|
|
||||||
left: [
|
|
||||||
Text(
|
|
||||||
data.l10n.breakTxt,
|
|
||||||
style: appStyle.fonts.B_16SB.apply(
|
|
||||||
color: appStyle.colors.textSecondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
right: [
|
|
||||||
Text(
|
|
||||||
"$breakMins ${breakMins == 1 ? data.l10n.starting_min : data.l10n.starting_min_plural}",
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.textTertiary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: elements,
|
children: elements,
|
||||||
|
|||||||
@@ -1,554 +1,266 @@
|
|||||||
import 'package:kreta_api/kreta_api.dart';
|
|
||||||
import 'package:firka/core/extensions.dart';
|
import 'package:firka/core/extensions.dart';
|
||||||
import 'package:firka/ui/components/firka_card.dart';
|
import 'package:firka/ui/components/firka_card.dart';
|
||||||
import 'package:firka/l10n/app_localizations.dart';
|
import 'package:firka/app/app_state.dart';
|
||||||
import 'package:firka/ui/theme/style.dart';
|
import 'package:firka/ui/theme/style.dart';
|
||||||
import 'package:firka/ui/shared/firka_icon.dart';
|
import 'package:firka_common/ui/components/filled_circle.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:majesticons_flutter/majesticons_flutter.dart';
|
import 'package:majesticons_flutter/majesticons_flutter.dart';
|
||||||
|
|
||||||
|
import 'package:kreta_api/kreta_api.dart';
|
||||||
|
import 'package:firka/core/debug_helper.dart';
|
||||||
|
import 'package:firka/ui/components/common_bottom_sheets.dart';
|
||||||
import 'package:firka/ui/shared/class_icon.dart';
|
import 'package:firka/ui/shared/class_icon.dart';
|
||||||
|
import 'package:firka/ui/shared/firka_icon.dart';
|
||||||
|
|
||||||
class LessonBigWidget extends StatelessWidget {
|
class LessonBigWidget extends StatelessWidget {
|
||||||
final AppLocalizations l10n;
|
final AppInitialization data;
|
||||||
final DateTime now;
|
final int lessonNo;
|
||||||
final int? lessonNo;
|
final Lesson lesson;
|
||||||
final Lesson? lesson;
|
final Test? test;
|
||||||
final Lesson? prevLesson;
|
final bool active;
|
||||||
final Lesson? nextLesson;
|
|
||||||
final List<Lesson> lessons;
|
|
||||||
final List<Test> tests;
|
|
||||||
|
|
||||||
const LessonBigWidget(
|
const LessonBigWidget(
|
||||||
this.l10n,
|
this.data,
|
||||||
this.now,
|
|
||||||
this.lessonNo,
|
this.lessonNo,
|
||||||
this.lesson,
|
this.lesson,
|
||||||
this.prevLesson,
|
this.test, {
|
||||||
this.nextLesson,
|
this.active = false,
|
||||||
this.lessons,
|
|
||||||
this.tests, {
|
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var hasLesson = lesson != null;
|
final isSubstituted = lesson.substituteTeacher != null;
|
||||||
var lessonsLeft = lessons.where((lesson) => lesson.end.isAfter(now)).length;
|
final isDismissed = lesson.type.name == "UresOra";
|
||||||
var hasPrevLesson = prevLesson != null;
|
|
||||||
var hasNextLesson = nextLesson != null;
|
|
||||||
// TODO: holnapi órák száma kiszámolás
|
|
||||||
var lessonsTomorrow = 0;
|
|
||||||
|
|
||||||
var testsTomorrow = tests
|
var accent = appStyle.colors.accent;
|
||||||
.where(
|
var secondary = appStyle.colors.secondary;
|
||||||
(test) =>
|
var bgColor = appStyle.colors.a15p;
|
||||||
test.date.isAfter(now) &&
|
|
||||||
test.date.isBefore(DateTime(now.year, now.month, now.day + 2)),
|
|
||||||
)
|
|
||||||
.length;
|
|
||||||
|
|
||||||
if (lessonsLeft < 1) {
|
if (isSubstituted) {
|
||||||
return Column(
|
accent = appStyle.colors.warningAccent;
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
secondary = appStyle.colors.warningText;
|
||||||
children: [
|
bgColor = appStyle.colors.warning15p;
|
||||||
FirkaCard(
|
}
|
||||||
left: [
|
if (isDismissed) {
|
||||||
Column(
|
accent = appStyle.colors.errorAccent;
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
secondary = appStyle.colors.errorText;
|
||||||
|
bgColor = appStyle.colors.error15p;
|
||||||
|
}
|
||||||
|
|
||||||
|
var subjectName = lesson.subject?.name.firstUpper() ?? 'N/A';
|
||||||
|
|
||||||
|
var roomName = lesson.roomName ?? 'N/A';
|
||||||
|
|
||||||
|
Widget extra = test == null
|
||||||
|
? Column(
|
||||||
|
spacing: 4,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Text(
|
||||||
children: [
|
(timeNow().isAfter(lesson.start)
|
||||||
SizedBox(
|
? lesson.end
|
||||||
width: 40,
|
.difference(timeNow())
|
||||||
height: 40,
|
.timeLeft(initData.l10n)
|
||||||
child: Stack(
|
: null) ??
|
||||||
children: [
|
lesson.start.format(data.l10n, FormatMode.hmm),
|
||||||
Card(
|
style: appStyle.fonts.B_14R.apply(
|
||||||
shadowColor: Colors.transparent,
|
color: appStyle.colors.textSecondary,
|
||||||
color: appStyle.colors.a15p,
|
),
|
||||||
shape: RoundedRectangleBorder(
|
),
|
||||||
borderRadius: BorderRadius.circular(20),
|
Text(
|
||||||
),
|
lesson.end.format(initData.l10n, FormatMode.hmm),
|
||||||
child: Padding(
|
style: appStyle.fonts.B_14R.apply(
|
||||||
padding: EdgeInsets.all(6),
|
color: appStyle.colors.textSecondary,
|
||||||
child: FirkaIconWidget(
|
),
|
||||||
FirkaIconType.majesticons,
|
|
||||||
Majesticon.moonSolid,
|
|
||||||
size: 32.0,
|
|
||||||
color: appStyle.colors.accent,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
testsTomorrow == 0
|
|
||||||
? l10n.tt_no_classes_l2
|
|
||||||
: l10n.get_ready,
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value:
|
||||||
|
timeNow().difference(lesson.start).inMilliseconds /
|
||||||
|
lesson.end.difference(lesson.start).inMilliseconds,
|
||||||
|
backgroundColor: appStyle.colors.a15p,
|
||||||
|
color: appStyle.colors.accent,
|
||||||
|
minHeight: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
extra: Column(
|
)
|
||||||
|
: Container(
|
||||||
|
height: 28,
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
decoration: ShapeDecoration(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
color: appStyle.colors.background,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(height: 4),
|
FirkaIconWidget(
|
||||||
ClipRRect(
|
FirkaIconType.majesticons,
|
||||||
borderRadius: BorderRadius.circular(360),
|
Majesticon.editPen4Solid,
|
||||||
child: Container(
|
size: 12,
|
||||||
width: double.infinity,
|
color: appStyle.colors.accent,
|
||||||
color: appStyle.colors.background,
|
),
|
||||||
padding: EdgeInsets.symmetric(vertical: 6, horizontal: 12),
|
SizedBox(width: 8),
|
||||||
child: Row(
|
Expanded(
|
||||||
children: [
|
child: Text(
|
||||||
SizedBox(
|
test!.theme ?? "",
|
||||||
width: 20,
|
style: appStyle.fonts.B_16R.apply(
|
||||||
height: 20,
|
color: appStyle.colors.textPrimary,
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.all(2),
|
|
||||||
child: FirkaIconWidget(
|
|
||||||
FirkaIconType.majesticons,
|
|
||||||
Majesticon.editPen4Solid,
|
|
||||||
size: 32.0,
|
|
||||||
color: appStyle.colors.accent,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
(lessonsTomorrow == 0 && testsTomorrow == 0)
|
|
||||||
? l10n.no_tests_tomorrow
|
|
||||||
: (testsTomorrow > 1)
|
|
||||||
? l10n.tests_tomorrow(testsTomorrow.toString())
|
|
||||||
: (testsTomorrow < 1 && lessonsTomorrow > 0)
|
|
||||||
? l10n.lessons_tomorrow(
|
|
||||||
lessonsTomorrow.toString(),
|
|
||||||
)
|
|
||||||
: l10n.tests_tomorrow(testsTomorrow.toString()),
|
|
||||||
textAlign: TextAlign.left,
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
],
|
|
||||||
);
|
return GestureDetector(
|
||||||
}
|
onTap: () {
|
||||||
if (!hasLesson && (!hasPrevLesson || !hasNextLesson)) {
|
showLessonBottomSheet(
|
||||||
if (!hasPrevLesson && !hasNextLesson) {
|
context,
|
||||||
return Column(
|
data,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
lesson,
|
||||||
children: [
|
accent,
|
||||||
FirkaCard(
|
secondary,
|
||||||
left: [
|
bgColor,
|
||||||
Column(
|
test,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Card(
|
|
||||||
shadowColor: Colors.transparent,
|
|
||||||
color: appStyle.colors.a15p,
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(4),
|
|
||||||
child: FirkaIconWidget(
|
|
||||||
FirkaIconType.majesticons,
|
|
||||||
'cupFilled',
|
|
||||||
color: appStyle.colors.accent,
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
l10n.breakTxt,
|
|
||||||
style: appStyle.fonts.B_16SB.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
right: [
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'-',
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'-',
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
extra: SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
child: FirkaCard.single(
|
||||||
// Before the first lesson: prev missing but next present. Show countdown
|
height: 104,
|
||||||
// to the next lesson using nextLesson data.
|
borderColor: active ? appStyle.colors.accent : null,
|
||||||
if (!hasPrevLesson && hasNextLesson) {
|
margin: EdgeInsets.only(bottom: active ? 0 : 1),
|
||||||
var timeLeft = nextLesson!.start.difference(now);
|
padding: EdgeInsets.only(left: 16, right: 16),
|
||||||
var timeLeftStr = l10n.timeLeft(timeLeft.inMinutes + 1);
|
color: isDismissed
|
||||||
|
? appStyle.colors.cardTranslucent
|
||||||
return Column(
|
: appStyle.colors.card,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
shadow: !isDismissed,
|
||||||
|
child: Column(
|
||||||
|
spacing: 12,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
FirkaCard(
|
Row(
|
||||||
left: [
|
children: [
|
||||||
Column(
|
SizedBox(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
width: 18,
|
||||||
children: [
|
height: 18,
|
||||||
Row(
|
child: Stack(
|
||||||
children: [
|
alignment: Alignment.center,
|
||||||
Card(
|
|
||||||
shadowColor: Colors.transparent,
|
|
||||||
color: appStyle.colors.a15p,
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(4),
|
|
||||||
child: FirkaIconWidget(
|
|
||||||
FirkaIconType.majesticonsLocal,
|
|
||||||
'cupFilled',
|
|
||||||
color: appStyle.colors.accent,
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
l10n.breakTxt,
|
|
||||||
style: appStyle.fonts.B_16SB.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
timeLeftStr,
|
|
||||||
style: appStyle.fonts.B_12R.apply(
|
|
||||||
color: appStyle.colors.textSecondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
right: [
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'-',
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
nextLesson!.start.toLocal().format(
|
|
||||||
l10n,
|
|
||||||
FormatMode.hmm,
|
|
||||||
),
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
extra: SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// After the last lesson: next missing but prev present. Show a simple
|
|
||||||
// "no more lessons" style card with the previous lesson end time.
|
|
||||||
if (hasPrevLesson && !hasNextLesson) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
// TODO: implement home/today afternoon
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasLesson) {
|
|
||||||
var timeLeft = lesson!.end.difference(now);
|
|
||||||
var duration = lesson!.end.difference(lesson!.start).inMilliseconds;
|
|
||||||
var progress = now.difference(lesson!.start).inMilliseconds;
|
|
||||||
|
|
||||||
var minsLeft = timeLeft.inMinutes;
|
|
||||||
var secsLeft = timeLeft.inSeconds;
|
|
||||||
|
|
||||||
var timeLeftStr =
|
|
||||||
"$minsLeft ${minsLeft == 1 ? l10n.starting_min : l10n.starting_min_plural}";
|
|
||||||
if (minsLeft < 1) {
|
|
||||||
timeLeftStr =
|
|
||||||
"$secsLeft ${secsLeft == 1 ? l10n.starting_sec : l10n.starting_sec_plural}";
|
|
||||||
}
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
FirkaCard(
|
|
||||||
left: [
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SvgPicture.asset(
|
||||||
|
"assets/icons/subtract.svg",
|
||||||
|
color: bgColor,
|
||||||
width: 18,
|
width: 18,
|
||||||
height: 18,
|
height: 18,
|
||||||
child: Stack(
|
),
|
||||||
|
Text(
|
||||||
|
lessonNo.toString(),
|
||||||
|
style: appStyle.fonts.B_14SB.apply(color: secondary),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FilledCircle(
|
||||||
|
diameter: 32,
|
||||||
|
color: bgColor,
|
||||||
|
child: ClassIconWidget(
|
||||||
|
uid: lesson.uid,
|
||||||
|
className: lesson.name,
|
||||||
|
category: subjectName,
|
||||||
|
color: accent,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
LimitedBox(
|
||||||
|
maxWidth: 155,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
SvgPicture.asset(
|
Text(
|
||||||
"assets/icons/subtract.svg",
|
subjectName,
|
||||||
color: appStyle.colors.a15p,
|
style: appStyle.fonts.B_16SB.apply(
|
||||||
width: 18,
|
color: !isDismissed
|
||||||
height: 18,
|
? appStyle.colors.textPrimary
|
||||||
),
|
: appStyle.colors.textSecondary,
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.only(left: 5),
|
|
||||||
child: Text(
|
|
||||||
lessonNo.toString(),
|
|
||||||
style: appStyle.fonts.B_12R.apply(
|
|
||||||
color: appStyle.colors.secondary,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
|
if (isSubstituted)
|
||||||
|
Text(
|
||||||
|
lesson.substituteTeacher!,
|
||||||
|
style: appStyle.fonts.B_14R.apply(
|
||||||
|
color: appStyle.colors.textSecondary,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Transform.translate(
|
Flexible(
|
||||||
offset: Offset(-4, 0),
|
fit: FlexFit.loose,
|
||||||
child: Card(
|
child: Card(
|
||||||
shadowColor: Colors.transparent,
|
shadowColor: Colors.transparent,
|
||||||
color: appStyle.colors.a15p,
|
color: appStyle.colors.a15p,
|
||||||
|
margin: EdgeInsets.all(0),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(4),
|
padding: EdgeInsets.symmetric(horizontal: 6),
|
||||||
child: ClassIconWidget(
|
child: Text(
|
||||||
color: appStyle.colors.accent,
|
roomName,
|
||||||
size: 24,
|
style: appStyle.fonts.B_14R.apply(
|
||||||
uid: lesson!.uid,
|
color: appStyle.colors.secondary,
|
||||||
className: lesson!.name,
|
),
|
||||||
category: lesson!.subject?.name ?? '',
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
textAlign: TextAlign.center,
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
lesson!.subject?.name ?? 'N/A',
|
|
||||||
style: appStyle.fonts.B_16SB.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
timeLeftStr,
|
|
||||||
style: appStyle.fonts.B_12R.apply(
|
|
||||||
color: appStyle.colors.textSecondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
right: [
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
lesson!.start.toLocal().format(l10n, FormatMode.hmm),
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Card(
|
|
||||||
shadowColor: Colors.transparent,
|
|
||||||
color: appStyle.colors.a15p,
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(4),
|
|
||||||
child: Text(
|
|
||||||
lesson!.roomName ?? '?',
|
|
||||||
style: appStyle.fonts.B_12R.apply(
|
|
||||||
color: appStyle.colors.secondary,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Row(
|
),
|
||||||
children: [
|
if (test != null) SizedBox(width: 8),
|
||||||
SizedBox(width: 18),
|
if (test != null)
|
||||||
Text(
|
Column(
|
||||||
lesson!.end.toLocal().format(l10n, FormatMode.hmm),
|
mainAxisSize: MainAxisSize.min,
|
||||||
style: appStyle.fonts.B_12R.apply(
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
color: appStyle.colors.textSecondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
extra: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
child: LinearProgressIndicator(
|
|
||||||
value: progress / duration,
|
|
||||||
backgroundColor: appStyle.colors.a15p,
|
|
||||||
color: appStyle.colors.accent,
|
|
||||||
minHeight: 8,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
var duration = nextLesson!.start
|
|
||||||
.difference(prevLesson!.end)
|
|
||||||
.inMilliseconds;
|
|
||||||
var progress =
|
|
||||||
duration - nextLesson!.start.difference(now).inMilliseconds;
|
|
||||||
var timeLeft = nextLesson!.start.difference(now);
|
|
||||||
|
|
||||||
var timeLeftStr = l10n.timeLeft(timeLeft.inMinutes + 1);
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
FirkaCard(
|
|
||||||
left: [
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Card(
|
|
||||||
shadowColor: Colors.transparent,
|
|
||||||
color: appStyle.colors.a15p,
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(4),
|
|
||||||
child: FirkaIconWidget(
|
|
||||||
FirkaIconType.majesticonsLocal,
|
|
||||||
'cupFilled',
|
|
||||||
color: appStyle.colors.accent,
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
l10n.breakTxt,
|
|
||||||
style: appStyle.fonts.B_16SB.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
timeLeftStr,
|
lesson.start.toLocal().format(
|
||||||
style: appStyle.fonts.B_12R.apply(
|
data.l10n,
|
||||||
color: appStyle.colors.textSecondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
right: [
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
prevLesson!.end.toLocal().format(l10n, FormatMode.hmm),
|
|
||||||
style: appStyle.fonts.B_16R.apply(
|
|
||||||
color: appStyle.colors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
nextLesson!.start.toLocal().format(
|
|
||||||
l10n,
|
|
||||||
FormatMode.hmm,
|
FormatMode.hmm,
|
||||||
),
|
),
|
||||||
style: appStyle.fonts.B_16R.apply(
|
style: appStyle.fonts.B_14R.apply(
|
||||||
color: appStyle.colors.textPrimary,
|
color: appStyle.colors.textPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
],
|
|
||||||
extra: LinearProgressIndicator(
|
|
||||||
// TODO: Make this rounded
|
|
||||||
value: progress / duration,
|
|
||||||
backgroundColor: appStyle.colors.a15p,
|
|
||||||
color: appStyle.colors.accent,
|
|
||||||
),
|
),
|
||||||
),
|
extra,
|
||||||
],
|
],
|
||||||
);
|
),
|
||||||
}
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
240
firka/lib/ui/phone/widgets/lesson_slider.dart
Normal file
240
firka/lib/ui/phone/widgets/lesson_slider.dart
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
|
import 'package:carousel_slider/carousel_slider.dart';
|
||||||
|
import 'package:firka/app/app_state.dart';
|
||||||
|
import 'package:firka/core/extensions.dart';
|
||||||
|
import 'package:firka/ui/phone/widgets/home_main_starting_soon.dart';
|
||||||
|
import 'package:firka/ui/phone/widgets/lesson_big.dart';
|
||||||
|
import 'package:firka/ui/shared/firka_icon.dart';
|
||||||
|
import 'package:firka_common/firka_common.dart';
|
||||||
|
import 'package:firka_common/ui/components/filled_circle.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:kreta_api/kreta_api.dart';
|
||||||
|
import 'package:majesticons_flutter/majesticons_flutter.dart';
|
||||||
|
|
||||||
|
class _LessonSliderState extends State<LessonSlider> {
|
||||||
|
DateTime now = timeNow();
|
||||||
|
int? swipeBack;
|
||||||
|
late Timer timer;
|
||||||
|
int? activeLessonIndex;
|
||||||
|
int? centeredPageIndex;
|
||||||
|
CarouselSliderController controller = CarouselSliderController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
timer = Timer.periodic(Duration(seconds: 1), (timer) async {
|
||||||
|
if (widget.lessons.isEmpty || !mounted) return;
|
||||||
|
setState(() {
|
||||||
|
if (swipeBack != null) swipeBack = swipeBack! - 1;
|
||||||
|
now = timeNow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
timer.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (widget.lessons.isEmpty) {
|
||||||
|
return SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
var lessons = widget.lessons.keys.toList();
|
||||||
|
|
||||||
|
Lesson? currentLesson;
|
||||||
|
int tmpIndex;
|
||||||
|
if (now.isBefore(lessons.first.start)) {
|
||||||
|
tmpIndex = 0;
|
||||||
|
} else {
|
||||||
|
(int, Lesson)? currentIndex = lessons.indexed.firstWhereOrNull(
|
||||||
|
(e) => now.isBefore(e.$2.end),
|
||||||
|
);
|
||||||
|
|
||||||
|
tmpIndex = (currentIndex?.$1 ?? lessons.length) + 1;
|
||||||
|
if (currentIndex != null) {
|
||||||
|
currentLesson = currentIndex.$2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeLessonIndex == null || tmpIndex != activeLessonIndex) {
|
||||||
|
activeLessonIndex = tmpIndex;
|
||||||
|
swipeBack = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
centeredPageIndex ??= activeLessonIndex!;
|
||||||
|
|
||||||
|
if (controller.ready && swipeBack == 0) {
|
||||||
|
controller.animateToPage(activeLessonIndex!);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
spacing: 12,
|
||||||
|
children: [
|
||||||
|
OverflowBox(
|
||||||
|
maxWidth: MediaQuery.widthOf(context),
|
||||||
|
fit: OverflowBoxFit.deferToChild,
|
||||||
|
child: CarouselSlider(
|
||||||
|
items: [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 5),
|
||||||
|
child: StartingSoonWidget(initData.l10n, now, lessons),
|
||||||
|
),
|
||||||
|
...widget.lessons.entries.map(
|
||||||
|
(entry) => Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 5),
|
||||||
|
child: LessonBigWidget(
|
||||||
|
initData,
|
||||||
|
lessons.getLessonNo(entry.key),
|
||||||
|
entry.key,
|
||||||
|
entry.value,
|
||||||
|
active: currentLesson == entry.key,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 5),
|
||||||
|
child: FirkaCard.single(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
margin: EdgeInsets.only(bottom: 1),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
FilledCircle(
|
||||||
|
diameter: 32,
|
||||||
|
color: appStyle.colors.a15p,
|
||||||
|
child: FirkaIconWidget(
|
||||||
|
FirkaIconType.majesticons,
|
||||||
|
Majesticon.moonSolid,
|
||||||
|
size: 20,
|
||||||
|
color: appStyle.colors.accent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
widget.tomorrowTestAmount == 0
|
||||||
|
? initData.l10n.tt_no_classes_l2
|
||||||
|
: initData.l10n.get_ready,
|
||||||
|
style: appStyle.fonts.B_16R.apply(
|
||||||
|
color: appStyle.colors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
height: 28,
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 6),
|
||||||
|
decoration: ShapeDecoration(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
color: appStyle.colors.background,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
FirkaIconWidget(
|
||||||
|
FirkaIconType.majesticons,
|
||||||
|
Majesticon.editPen4Solid,
|
||||||
|
size: 12,
|
||||||
|
color: appStyle.colors.accent,
|
||||||
|
),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
widget.tomorrowTestAmount == 0
|
||||||
|
? initData.l10n.no_tests_tomorrow
|
||||||
|
: initData.l10n.tests_tomorrow(
|
||||||
|
widget.tomorrowTestAmount.toString(),
|
||||||
|
),
|
||||||
|
style: appStyle.fonts.B_16R.apply(
|
||||||
|
color: appStyle.colors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
carouselController: controller,
|
||||||
|
options: CarouselOptions(
|
||||||
|
initialPage: activeLessonIndex!,
|
||||||
|
height: 106,
|
||||||
|
viewportFraction: 346 / 376,
|
||||||
|
enableInfiniteScroll: false,
|
||||||
|
onPageChanged: (index, reason) {
|
||||||
|
centeredPageIndex = index;
|
||||||
|
if (index == activeLessonIndex) {
|
||||||
|
swipeBack = null;
|
||||||
|
} else if (reason == CarouselPageChangedReason.manual) {
|
||||||
|
swipeBack = 5;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 16,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
FirkaIconWidget(
|
||||||
|
FirkaIconType.majesticonsLocal,
|
||||||
|
"sunSolid",
|
||||||
|
color: centeredPageIndex == 0
|
||||||
|
? appStyle.colors.accent
|
||||||
|
: appStyle.colors.accent.withAlpha(128),
|
||||||
|
size: centeredPageIndex == 0 ? 16 : 12,
|
||||||
|
),
|
||||||
|
...widget.lessons.keys.indexed.map(
|
||||||
|
(i) => FilledCircle(
|
||||||
|
diameter: centeredPageIndex == i.$1 + 1 ? 10 : 8,
|
||||||
|
color: centeredPageIndex == i.$1 + 1
|
||||||
|
? appStyle.colors.accent
|
||||||
|
: appStyle.colors.accent.withAlpha(128),
|
||||||
|
child: SizedBox(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FirkaIconWidget(
|
||||||
|
FirkaIconType.majesticons,
|
||||||
|
Majesticon.moonSolid,
|
||||||
|
color: centeredPageIndex == widget.lessons.keys.length + 1
|
||||||
|
? appStyle.colors.accent
|
||||||
|
: appStyle.colors.accent.withAlpha(128),
|
||||||
|
size: centeredPageIndex == widget.lessons.keys.length + 1
|
||||||
|
? 16
|
||||||
|
: 12,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LessonSlider extends StatefulWidget {
|
||||||
|
final LinkedHashMap<Lesson, Test?> lessons;
|
||||||
|
final int tomorrowTestAmount;
|
||||||
|
|
||||||
|
const LessonSlider(this.lessons, this.tomorrowTestAmount, {super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _LessonSliderState();
|
||||||
|
}
|
||||||
@@ -43,9 +43,7 @@ class LessonSmallWidget extends StatelessWidget {
|
|||||||
size: 20,
|
size: 20,
|
||||||
uid: lesson.uid,
|
uid: lesson.uid,
|
||||||
className: lesson.name,
|
className: lesson.name,
|
||||||
category: lesson.subject?.name != null
|
category: lesson.subject?.name.firstUpper() ?? '',
|
||||||
? lesson.subject!.name.firstUpper()
|
|
||||||
: '',
|
|
||||||
),
|
),
|
||||||
SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import 'package:firka/data/models/app_settings_model.dart';
|
|||||||
import 'package:firka/services/live_activity_service.dart';
|
import 'package:firka/services/live_activity_service.dart';
|
||||||
import 'package:firka/app/app_state.dart';
|
import 'package:firka/app/app_state.dart';
|
||||||
import 'package:firka/app/initialization.dart';
|
import 'package:firka/app/initialization.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:isar_community/isar.dart';
|
import 'package:isar_community/isar.dart';
|
||||||
@@ -41,13 +43,15 @@ class _LoginWebviewWidgetState extends FirkaState<LoginWebviewWidget>
|
|||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
AnimationController? _fadeAnimationController;
|
AnimationController? _fadeAnimationController;
|
||||||
Animation<double>? _fadeAnimation;
|
Animation<double>? _fadeAnimation;
|
||||||
|
late String _displayHost;
|
||||||
|
late String _displayPath;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
_fadeAnimationController = AnimationController(
|
_fadeAnimationController = AnimationController(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 300),
|
||||||
vsync: this,
|
vsync: this,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -56,15 +60,20 @@ class _LoginWebviewWidgetState extends FirkaState<LoginWebviewWidget>
|
|||||||
end: 0.0,
|
end: 0.0,
|
||||||
).animate(_fadeAnimationController!);
|
).animate(_fadeAnimationController!);
|
||||||
|
|
||||||
var loginUrl = KretaEndpoints.kretaLoginUrl;
|
var loginUrl = KretaLoginEndpoints.kretaLoginUrl;
|
||||||
|
|
||||||
if (widget.username != null && widget.schoolId != null) {
|
if (widget.username != null && widget.schoolId != null) {
|
||||||
loginUrl = KretaEndpoints.kretaLoginUrlRefresh(
|
loginUrl = KretaLoginEndpoints.kretaLoginUrlRefresh(
|
||||||
widget.username!,
|
widget.username!,
|
||||||
widget.schoolId!,
|
widget.schoolId!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final trimmed = loginUrl.replaceFirst(RegExp(r'^https?://'), '');
|
||||||
|
final parts = trimmed.split('/');
|
||||||
|
_displayHost = parts.isNotEmpty ? parts.first : trimmed;
|
||||||
|
_displayPath = parts.length > 1 ? '/${parts.sublist(1).join('/')}' : '';
|
||||||
|
|
||||||
logger.info("Using loginUrl: $loginUrl");
|
logger.info("Using loginUrl: $loginUrl");
|
||||||
|
|
||||||
_webViewController = WebViewController()
|
_webViewController = WebViewController()
|
||||||
@@ -73,7 +82,7 @@ class _LoginWebviewWidgetState extends FirkaState<LoginWebviewWidget>
|
|||||||
..setNavigationDelegate(
|
..setNavigationDelegate(
|
||||||
NavigationDelegate(
|
NavigationDelegate(
|
||||||
onPageFinished: (String url) {
|
onPageFinished: (String url) {
|
||||||
Timer(const Duration(milliseconds: 500), () {
|
Timer(Duration.zero, () {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
@@ -259,7 +268,14 @@ class _LoginWebviewWidgetState extends FirkaState<LoginWebviewWidget>
|
|||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
WebViewWidget(controller: _webViewController),
|
WebViewWidget(
|
||||||
|
controller: _webViewController,
|
||||||
|
gestureRecognizers: {
|
||||||
|
Factory<OneSequenceGestureRecognizer>(
|
||||||
|
() => EagerGestureRecognizer(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
if (_fadeAnimationController != null &&
|
if (_fadeAnimationController != null &&
|
||||||
_fadeAnimation != null)
|
_fadeAnimation != null)
|
||||||
IgnorePointer(
|
IgnorePointer(
|
||||||
@@ -270,9 +286,9 @@ class _LoginWebviewWidgetState extends FirkaState<LoginWebviewWidget>
|
|||||||
opacity: _isLoading
|
opacity: _isLoading
|
||||||
? 1.0
|
? 1.0
|
||||||
: _fadeAnimationController!.isAnimating
|
: _fadeAnimationController!.isAnimating
|
||||||
? _fadeAnimation!.value
|
? _fadeAnimation!.value
|
||||||
: 0.0,
|
: 0.0,
|
||||||
duration: const Duration(milliseconds: 500),
|
duration: const Duration(milliseconds: 300),
|
||||||
child: Container(
|
child: Container(
|
||||||
color: appStyle.colors.background,
|
color: appStyle.colors.background,
|
||||||
child: Center(
|
child: Center(
|
||||||
@@ -302,16 +318,29 @@ class _LoginWebviewWidgetState extends FirkaState<LoginWebviewWidget>
|
|||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
child: Row(
|
child: Align(
|
||||||
children: [
|
alignment: Alignment.centerLeft,
|
||||||
Text(
|
child: RichText(
|
||||||
"eKréta/Bejelentkezés",
|
text: TextSpan(
|
||||||
|
text: _displayHost,
|
||||||
style: appStyle.fonts.B_14R.copyWith(
|
style: appStyle.fonts.B_14R.copyWith(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: appStyle.colors.textPrimary,
|
color: appStyle.colors.textPrimary,
|
||||||
),
|
),
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: _displayPath,
|
||||||
|
style: appStyle.fonts.B_14R.copyWith(
|
||||||
|
fontSize: 16,
|
||||||
|
color: appStyle.colors.textTeritary ??
|
||||||
|
appStyle.colors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -357,7 +386,7 @@ class _LoginWebviewWidgetState extends FirkaState<LoginWebviewWidget>
|
|||||||
child: Center(
|
child: Center(
|
||||||
child: Majesticon(
|
child: Majesticon(
|
||||||
Majesticon.chevronLeftLine,
|
Majesticon.chevronLeftLine,
|
||||||
color: appStyle.colors.secondary,
|
color: appStyle.colors.buttonDisabledIcon,
|
||||||
size: 22,
|
size: 22,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -380,7 +409,7 @@ class _LoginWebviewWidgetState extends FirkaState<LoginWebviewWidget>
|
|||||||
child: Center(
|
child: Center(
|
||||||
child: Majesticon(
|
child: Majesticon(
|
||||||
Majesticon.menuLine,
|
Majesticon.menuLine,
|
||||||
color: appStyle.colors.secondary,
|
color: appStyle.colors.buttonDisabledIcon,
|
||||||
size: 22,
|
size: 22,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import 'package:firka/ui/components/firka_card.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:kreta_api/kreta_api.dart';
|
|
||||||
|
|
||||||
// TODO: Finish
|
|
||||||
class NoticeBoardItemWidget extends StatelessWidget {
|
|
||||||
final NoticeBoardItem item;
|
|
||||||
|
|
||||||
const NoticeBoardItemWidget(this.item, {super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return FirkaCard(left: [Text(item.title)]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'package:firka/core/settings.dart';
|
||||||
|
import 'package:firka_common/firka_common.dart';
|
||||||
import 'package:kreta_api/kreta_api.dart';
|
import 'package:kreta_api/kreta_api.dart';
|
||||||
import 'package:firka/core/extensions.dart';
|
import 'package:firka/core/extensions.dart';
|
||||||
import 'package:firka/ui/components/firka_card.dart';
|
import 'package:firka/ui/components/firka_card.dart';
|
||||||
@@ -30,11 +32,10 @@ class TimeTableDayWidget extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget noLessonsWidget = SizedBox();
|
Widget ttBody;
|
||||||
List<Widget> ttBody = List.empty(growable: true);
|
|
||||||
|
|
||||||
if (lessons.isEmpty) {
|
if (lessons.isEmpty) {
|
||||||
noLessonsWidget = Column(
|
ttBody = Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@@ -60,7 +61,29 @@ class TimeTableDayWidget extends StatelessWidget {
|
|||||||
...events.map(
|
...events.map(
|
||||||
(event) => Center(
|
(event) => Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
event.name.replaceAll(" (Nem órarendi nap)", ""),
|
event.name
|
||||||
|
.replaceAll(" (Nem órarendi nap)", "")
|
||||||
|
.replaceAll(" (Hétfő)", "")
|
||||||
|
.replaceAll(" (Kedd)", "")
|
||||||
|
.replaceAll(" (Szerda)", "")
|
||||||
|
.replaceAll(" (Csütörtök)", "")
|
||||||
|
.replaceAll(" (Péntek)", "")
|
||||||
|
.replaceAll(" (Szombat)", "")
|
||||||
|
.replaceAll(" (Vasárnap)", "")
|
||||||
|
.replaceAll("Tanítás nélküli munkanap", data.l10n.tt_non_instructional_day)
|
||||||
|
.replaceAll("Munkaszüneti nap", data.l10n.tt_public_holiday)
|
||||||
|
.replaceAll("Tavaszi szünet", data.l10n.tt_spring_break)
|
||||||
|
.replaceAll("Téli szünet", data.l10n.tt_winter_break)
|
||||||
|
.replaceAll("Őszi szünet", data.l10n.tt_autumn_break)
|
||||||
|
.replaceAll("Tanítási nap", data.l10n.tt_instructional_day)
|
||||||
|
.replaceAll("Első félév vége", data.l10n.tt_first_semester_end)
|
||||||
|
.replaceAll("Ünnepnap", data.l10n.tt_holiday)
|
||||||
|
.replaceAll("Pihenőnap", data.l10n.tt_rest_day)
|
||||||
|
.replaceAll("Első tanítási nap", data.l10n.tt_first_instructional_day)
|
||||||
|
.replaceAll("Utolsó tanítási nap", data.l10n.tt_last_instructional_day)
|
||||||
|
.replaceAll("Utolsó tanítási nap a végzős évfolyamokon", data.l10n.tt_last_instructional_day_graduates)
|
||||||
|
.replaceAll("Egész napos kirándulás", data.l10n.tt_full_day_trip)
|
||||||
|
.replaceAll("negyedév vége", data.l10n.tt_quarter_end),
|
||||||
style: appStyle.fonts.B_16R.apply(
|
style: appStyle.fonts.B_16R.apply(
|
||||||
color: appStyle.colors.textSecondary,
|
color: appStyle.colors.textSecondary,
|
||||||
),
|
),
|
||||||
@@ -70,58 +93,94 @@ class TimeTableDayWidget extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
List<Widget> ttLessons = List.empty(growable: true);
|
||||||
for (var i = 0; i < events.length; i++) {
|
for (var i = 0; i < events.length; i++) {
|
||||||
var event = events[i];
|
var event = events[i];
|
||||||
ttBody.add(
|
ttLessons.add(
|
||||||
|
FirkaCard.single(
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: Text(
|
||||||
|
event.name,
|
||||||
|
style: appStyle.fonts.B_16R.apply(
|
||||||
|
color: appStyle.colors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (events.isNotEmpty) {
|
||||||
|
ttLessons.add(SizedBox());
|
||||||
|
}
|
||||||
|
|
||||||
|
var showBreak = data.settings
|
||||||
|
.group("settings")
|
||||||
|
.subGroup("timetable_toast")
|
||||||
|
.boolean("breaks");
|
||||||
|
|
||||||
|
for (var i = 0; i < lessons.length; i++) {
|
||||||
|
var lesson = lessons[i];
|
||||||
|
var nextLesson = lessons.length > i + 1 ? lessons[i + 1] : null;
|
||||||
|
ttLessons.add(
|
||||||
|
LessonWidget(
|
||||||
|
data,
|
||||||
|
lesson,
|
||||||
|
tests.firstWhereOrNull(
|
||||||
|
(test) => test.lessonNumber == lesson.lessonNumber,
|
||||||
|
),
|
||||||
|
active: timeNow().isBetween(
|
||||||
|
i > 0 ? lessons[i - 1].end : lesson.start,
|
||||||
|
lesson.end,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!showBreak || nextLesson == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var breakMins = nextLesson.start.difference(lesson.end).inMinutes;
|
||||||
|
ttLessons.add(
|
||||||
FirkaCard(
|
FirkaCard(
|
||||||
|
color: appStyle.colors.cardTranslucent,
|
||||||
|
margin: EdgeInsets.all(0),
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 11, horizontal: 16),
|
||||||
|
shadow: false,
|
||||||
left: [
|
left: [
|
||||||
Text(
|
Text(
|
||||||
event.name,
|
initData.l10n.breakTxt,
|
||||||
style: appStyle.fonts.B_16R.apply(
|
style: appStyle.fonts.B_14SB.copyWith(
|
||||||
color: appStyle.colors.textPrimary,
|
color: appStyle.colors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
right: [
|
||||||
|
Text(
|
||||||
|
"$breakMins ${breakMins > 1 ? initData.l10n.starting_min_plural : initData.l10n.starting_min}",
|
||||||
|
style: appStyle.fonts.B_14R.copyWith(
|
||||||
|
color: appStyle.colors.textSecondary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
for (var i = 0; i < lessons.length; i++) {
|
|
||||||
var lesson = lessons[i];
|
ttBody = Padding(
|
||||||
Lesson? nextLesson = lessons.length > i + 1 ? lessons[i + 1] : null;
|
padding: const EdgeInsets.only(top: 70 + 16 + 20, left: 20, right: 20),
|
||||||
ttBody.add(
|
child: SingleChildScrollView(
|
||||||
LessonWidget(
|
child: Column(
|
||||||
data,
|
mainAxisSize: MainAxisSize.min,
|
||||||
week,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
day,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
lessons.getLessonNo(lesson),
|
spacing: 16,
|
||||||
lesson,
|
children: [...ttLessons, SizedBox(height: 55)],
|
||||||
tests.firstWhereOrNull(
|
|
||||||
(test) => test.lessonNumber == lesson.lessonNumber,
|
|
||||||
),
|
|
||||||
nextLesson,
|
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SizedBox(
|
return ttBody;
|
||||||
width: MediaQuery.of(context).size.width / 1.1,
|
|
||||||
child: ttBody.isEmpty
|
|
||||||
? noLessonsWidget
|
|
||||||
: Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
top: 70 + 16 + 20,
|
|
||||||
left: 4,
|
|
||||||
right: 4,
|
|
||||||
),
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [...ttBody, SizedBox(height: 24)],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: firka
|
|||||||
description: "Firka, Alternatív e-Kréta kliens."
|
description: "Firka, Alternatív e-Kréta kliens."
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.1.1+2001
|
version: 1.1.2+2001
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.0
|
sdk: ^3.11.0
|
||||||
|
|||||||
31
firka_common/lib/ui/components/filled_circle.dart
Normal file
31
firka_common/lib/ui/components/filled_circle.dart
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:kreta_api/kreta_api.dart';
|
||||||
|
|
||||||
|
import 'package:firka_common/ui/components/grade_helpers.dart';
|
||||||
|
import 'package:firka_common/ui/theme/style.dart';
|
||||||
|
|
||||||
|
class FilledCircle extends StatelessWidget {
|
||||||
|
final double diameter;
|
||||||
|
final Color color;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const FilledCircle({
|
||||||
|
required this.diameter,
|
||||||
|
required this.child,
|
||||||
|
required this.color,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
width: diameter,
|
||||||
|
height: diameter,
|
||||||
|
child: Material(
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
color: color,
|
||||||
|
child: Center(child: child),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,132 +6,108 @@ import 'package:firka_common/ui/theme/style.dart';
|
|||||||
enum Attach { none, bottom, top }
|
enum Attach { none, bottom, top }
|
||||||
|
|
||||||
class FirkaCard extends StatelessWidget {
|
class FirkaCard extends StatelessWidget {
|
||||||
final List<Widget> left;
|
final EdgeInsets padding;
|
||||||
final List<Widget>? center;
|
final EdgeInsets margin;
|
||||||
|
final Color? borderColor;
|
||||||
final double? height;
|
final double? height;
|
||||||
final List<Widget>? right;
|
final double? width;
|
||||||
final bool shadow;
|
final bool shadow;
|
||||||
final Widget? extra;
|
final Attach attached;
|
||||||
final Attach? attached;
|
|
||||||
final Color? color;
|
final Color? color;
|
||||||
final bool? isLightMode;
|
final bool? isLightMode;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
const FirkaCard({
|
factory FirkaCard({
|
||||||
required this.left,
|
required List<Widget> left,
|
||||||
|
EdgeInsets padding = const EdgeInsets.all(12),
|
||||||
|
EdgeInsets margin = const EdgeInsets.all(4),
|
||||||
|
bool shadow = true,
|
||||||
|
List<Widget> center = const [],
|
||||||
|
List<Widget> right = const [],
|
||||||
|
Widget? extra,
|
||||||
|
Attach attached = Attach.none,
|
||||||
|
Color? color,
|
||||||
|
double? height,
|
||||||
|
double? width,
|
||||||
|
bool? isLightMode,
|
||||||
|
}) {
|
||||||
|
final leftRow = Row(children: left);
|
||||||
|
|
||||||
|
final alignedRow = right.isEmpty && center.isEmpty
|
||||||
|
? leftRow
|
||||||
|
: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
leftRow,
|
||||||
|
Row(children: center),
|
||||||
|
Row(children: right),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
return FirkaCard.single(
|
||||||
|
padding: padding,
|
||||||
|
margin: margin,
|
||||||
|
attached: attached,
|
||||||
|
color: color,
|
||||||
|
height: height,
|
||||||
|
shadow: shadow,
|
||||||
|
isLightMode: isLightMode,
|
||||||
|
child: extra == null
|
||||||
|
? alignedRow
|
||||||
|
: Column(children: [alignedRow, extra!]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const FirkaCard.single({
|
||||||
|
this.padding = const EdgeInsets.all(0),
|
||||||
|
this.margin = const EdgeInsets.all(4),
|
||||||
this.shadow = true,
|
this.shadow = true,
|
||||||
this.center,
|
this.attached = Attach.none,
|
||||||
this.right,
|
this.borderColor,
|
||||||
this.extra,
|
|
||||||
this.attached,
|
|
||||||
this.color,
|
this.color,
|
||||||
this.height,
|
this.height,
|
||||||
|
this.width,
|
||||||
this.isLightMode,
|
this.isLightMode,
|
||||||
|
required this.child,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var right = this.right ?? [];
|
|
||||||
|
|
||||||
var attached = this.attached != null ? this.attached! : Attach.none;
|
|
||||||
final defaultRounding = 16.0;
|
final defaultRounding = 16.0;
|
||||||
final attachedRounding = 8.0;
|
final attachedRounding = 8.0;
|
||||||
final isLight =
|
final isLight =
|
||||||
isLightMode ?? Theme.of(context).brightness == Brightness.light;
|
isLightMode ?? Theme.of(context).brightness == Brightness.light;
|
||||||
|
|
||||||
if (extra != null) {
|
return Container(
|
||||||
return SizedBox(
|
height: height,
|
||||||
width: MediaQuery.of(context).size.width,
|
width: width,
|
||||||
height: height,
|
padding: padding,
|
||||||
child: FirkaShadow(
|
margin: margin,
|
||||||
shadow: shadow,
|
decoration: ShapeDecoration(
|
||||||
isLightMode: isLight,
|
shadows: shadow && !isLight
|
||||||
child: Card(
|
? [
|
||||||
color: color ?? appStyle.colors.card,
|
BoxShadow(
|
||||||
shadowColor: isLight && shadow ? null : Colors.transparent,
|
color: appStyle.colors.shadowColor,
|
||||||
shape: RoundedRectangleBorder(
|
offset: const Offset(0, 1),
|
||||||
borderRadius: BorderRadius.only(
|
|
||||||
topLeft: Radius.circular(
|
|
||||||
attached == Attach.top ? attachedRounding : defaultRounding,
|
|
||||||
),
|
),
|
||||||
topRight: Radius.circular(
|
]
|
||||||
attached == Attach.top ? attachedRounding : defaultRounding,
|
: [],
|
||||||
),
|
shape: RoundedRectangleBorder(
|
||||||
bottomLeft: Radius.circular(
|
side: borderColor == null
|
||||||
attached == Attach.bottom
|
? BorderSide.none
|
||||||
? attachedRounding
|
: BorderSide(color: borderColor!),
|
||||||
: defaultRounding,
|
borderRadius: BorderRadius.vertical(
|
||||||
),
|
top: Radius.circular(
|
||||||
bottomRight: Radius.circular(
|
attached == Attach.top ? attachedRounding : defaultRounding,
|
||||||
attached == Attach.bottom
|
|
||||||
? attachedRounding
|
|
||||||
: defaultRounding,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: Padding(
|
bottom: Radius.circular(
|
||||||
padding: const EdgeInsets.all(12.0),
|
attached == Attach.bottom ? attachedRounding : defaultRounding,
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Row(children: left),
|
|
||||||
Row(children: center ?? []),
|
|
||||||
Row(children: right),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
extra ?? const SizedBox(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
color: color ?? appStyle.colors.card,
|
||||||
} else {
|
),
|
||||||
return SizedBox(
|
child: child,
|
||||||
width: MediaQuery.of(context).size.width,
|
);
|
||||||
height: height,
|
|
||||||
child: FirkaShadow(
|
|
||||||
shadow: shadow,
|
|
||||||
isLightMode: isLight,
|
|
||||||
child: Card(
|
|
||||||
color: color ?? appStyle.colors.card,
|
|
||||||
shadowColor: isLight && shadow ? null : Colors.transparent,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.only(
|
|
||||||
topLeft: Radius.circular(
|
|
||||||
attached == Attach.top ? attachedRounding : defaultRounding,
|
|
||||||
),
|
|
||||||
topRight: Radius.circular(
|
|
||||||
attached == Attach.top ? attachedRounding : defaultRounding,
|
|
||||||
),
|
|
||||||
bottomLeft: Radius.circular(
|
|
||||||
attached == Attach.bottom
|
|
||||||
? attachedRounding
|
|
||||||
: defaultRounding,
|
|
||||||
),
|
|
||||||
bottomRight: Radius.circular(
|
|
||||||
attached == Attach.bottom
|
|
||||||
? attachedRounding
|
|
||||||
: defaultRounding,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(12.0),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Row(children: left),
|
|
||||||
Row(children: center ?? []),
|
|
||||||
Row(children: right),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,94 +4,104 @@ import 'package:kreta_api/kreta_api.dart';
|
|||||||
import 'package:firka_common/ui/components/grade_helpers.dart';
|
import 'package:firka_common/ui/components/grade_helpers.dart';
|
||||||
import 'package:firka_common/ui/theme/style.dart';
|
import 'package:firka_common/ui/theme/style.dart';
|
||||||
|
|
||||||
class GradeWidget extends StatelessWidget {
|
import 'filled_circle.dart';
|
||||||
const GradeWidget(this.grade, {super.key})
|
|
||||||
: gradeValue = null,
|
|
||||||
_fromValue = false;
|
|
||||||
|
|
||||||
const GradeWidget.gradeValue(int value, {super.key})
|
class GradeWidget extends StatelessWidget {
|
||||||
: grade = null,
|
const GradeWidget(this.grade, {this.size = 36, super.key})
|
||||||
gradeValue = value,
|
: gradeValue = null,
|
||||||
_fromValue = true;
|
gradeWeight = null;
|
||||||
|
|
||||||
|
const GradeWidget.gradeValue(
|
||||||
|
this.gradeValue, {
|
||||||
|
this.gradeWeight = 100,
|
||||||
|
this.size = 36,
|
||||||
|
super.key,
|
||||||
|
}) : grade = null;
|
||||||
|
|
||||||
final Grade? grade;
|
final Grade? grade;
|
||||||
|
final double size;
|
||||||
final int? gradeValue;
|
final int? gradeValue;
|
||||||
final bool _fromValue;
|
final int? gradeWeight;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_fromValue && gradeValue != null) {
|
if (gradeValue != null && gradeValue != null) {
|
||||||
return _buildNumericCircle(
|
return _buildNumericCircle(gradeValue!, gradeWeight!);
|
||||||
gradeValue!,
|
|
||||||
getGradeColor(gradeValue!.toDouble()),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final g = grade!;
|
final g = grade!;
|
||||||
Color gradeColor = appStyle.colors.grade1;
|
|
||||||
final gradeStr = g.numericValue?.toString() ?? '0';
|
|
||||||
|
|
||||||
if (g.valueType.name == 'Szazalekos') {
|
if (g.numericValue == null) {
|
||||||
if (g.numericValue != null) {
|
final gradeColor = appStyle.colors.accent;
|
||||||
gradeColor = getGradeColor(
|
return FilledCircle(
|
||||||
percentageToGrade(g.numericValue!).toDouble(),
|
diameter: size,
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final str = g.strValue.replaceAll('%', '');
|
|
||||||
return Card(
|
|
||||||
shape: const CircleBorder(),
|
|
||||||
shadowColor: Colors.transparent,
|
|
||||||
color: gradeColor.withAlpha(38),
|
color: gradeColor.withAlpha(38),
|
||||||
child: Padding(
|
child: Text(
|
||||||
padding: const EdgeInsets.all(8),
|
'❝❠',
|
||||||
child: Row(
|
style: appStyle.fonts.B_16SB.copyWith(color: gradeColor),
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(str, style: appStyle.fonts.P_14.copyWith(color: gradeColor)),
|
|
||||||
Text('%', style: appStyle.fonts.P_12.copyWith(color: gradeColor)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (g.numericValue != null) {
|
if (g.isInPercentage()) {
|
||||||
gradeColor = getGradeColor(g.numericValue!.toDouble());
|
final gradeColor = appStyle.colors.accent;
|
||||||
}
|
return FilledCircle(
|
||||||
|
diameter: size,
|
||||||
if (gradeStr == '0') {
|
|
||||||
return Card(
|
|
||||||
shadowColor: Colors.transparent,
|
|
||||||
color: gradeColor.withAlpha(38),
|
color: gradeColor.withAlpha(38),
|
||||||
child: Padding(
|
child: Row(
|
||||||
padding: const EdgeInsets.only(left: 8, right: 8, top: 2, bottom: 2),
|
mainAxisSize: MainAxisSize.min,
|
||||||
child: Text(
|
children: [
|
||||||
g.strValue,
|
Text(
|
||||||
style: appStyle.fonts.H_H1.copyWith(
|
g.numericValue!.toString(),
|
||||||
fontSize: 16,
|
style: appStyle.fonts.P_14.copyWith(color: gradeColor),
|
||||||
color: gradeColor,
|
|
||||||
),
|
),
|
||||||
),
|
Text('%', style: appStyle.fonts.P_12.copyWith(color: gradeColor)),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _buildNumericCircle(g.numericValue!, gradeColor);
|
return _buildNumericCircle(g.numericValue!, g.weightPercentage ?? 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildNumericCircle(int value, Color gradeColor) {
|
Widget _buildNumericCircle(int value, int weight) {
|
||||||
return Card(
|
final gradeColor = getGradeColor(value);
|
||||||
shape: const CircleBorder(),
|
final textStyle = appStyle.fonts.H_H1.copyWith(
|
||||||
shadowColor: Colors.transparent,
|
color: gradeColor,
|
||||||
color: gradeColor.withAlpha(38),
|
fontSize: size * 0.75,
|
||||||
child: Padding(
|
);
|
||||||
padding: const EdgeInsets.only(left: 8, right: 8),
|
final text = Text(value.toString(), style: textStyle);
|
||||||
child: Text(
|
|
||||||
value.toString(),
|
if (weight > 100) {
|
||||||
style: appStyle.fonts.H_H1.copyWith(fontSize: 24, color: gradeColor),
|
final circle_size = size * 0.8;
|
||||||
|
final circle = FilledCircle(
|
||||||
|
diameter: circle_size,
|
||||||
|
color: gradeColor.withAlpha(38),
|
||||||
|
child: SizedBox(),
|
||||||
|
);
|
||||||
|
return SizedBox(
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Transform.translate(
|
||||||
|
offset: Offset(size - circle_size, 0),
|
||||||
|
child: circle,
|
||||||
|
),
|
||||||
|
Transform.translate(
|
||||||
|
offset: Offset(0, size - circle_size),
|
||||||
|
child: circle,
|
||||||
|
),
|
||||||
|
Center(child: text),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return FilledCircle(
|
||||||
|
diameter: size,
|
||||||
|
color: gradeColor.withAlpha(38),
|
||||||
|
child: text,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import 'package:kreta_api/kreta_api.dart';
|
||||||
|
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:firka_common/ui/theme/style.dart';
|
import 'package:firka_common/ui/theme/style.dart';
|
||||||
import 'package:kreta_api/kreta_api.dart';
|
|
||||||
|
|
||||||
int roundGrade(
|
int roundGrade(
|
||||||
double grade, {
|
num grade, {
|
||||||
double t1 = 1,
|
double t1 = 1,
|
||||||
double t2 = 0.5,
|
double t2 = 0.5,
|
||||||
double t3 = 0.5,
|
double t3 = 0.5,
|
||||||
@@ -26,25 +27,8 @@ int roundGrade(
|
|||||||
return 5;
|
return 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
int percentageToGrade(int grade) {
|
|
||||||
if (grade < 50) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (grade < 60) {
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
if (grade < 70) {
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
if (grade < 80) {
|
|
||||||
return 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
Color getGradeColor(
|
Color getGradeColor(
|
||||||
double grade, {
|
num grade, {
|
||||||
double t1 = 1,
|
double t1 = 1,
|
||||||
double t2 = 0.5,
|
double t2 = 0.5,
|
||||||
double t3 = 0.5,
|
double t3 = 0.5,
|
||||||
@@ -64,37 +48,113 @@ Color getGradeColor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(int total, List<int> countsByGrade) getGradeDistribution(List<Grade> grades) {
|
extension GradeListExtension on Iterable<Grade> {
|
||||||
final filtered = grades
|
(int total, List<int> countsByGrade) getGradeDistribution() {
|
||||||
.where((g) => g.type.name != "felevi_jegy_ertekeles")
|
final filtered = where((g) => g.shouldIncludeInAverage());
|
||||||
.toList();
|
final counts = [0, 0, 0, 0, 0];
|
||||||
final counts = [0, 0, 0, 0, 0];
|
for (final g in filtered) {
|
||||||
for (final g in filtered) {
|
counts[g.numericValue! - 1]++;
|
||||||
if (g.numericValue == null) continue;
|
}
|
||||||
final value = g.valueType.name == "Szazalekos"
|
return (filtered.length, counts);
|
||||||
? percentageToGrade(g.numericValue!.round())
|
|
||||||
: g.numericValue!.round().clamp(1, 5);
|
|
||||||
counts[value - 1]++;
|
|
||||||
}
|
}
|
||||||
return (filtered.length, counts);
|
|
||||||
}
|
|
||||||
|
|
||||||
extension GradeListExtension on List<Grade> {
|
double? _getAverageBySubject(String uid, {bool halfYearFallback = true}) {
|
||||||
double getAverageBySubject(Subject subject) {
|
return where(
|
||||||
var weightTotal = 0.00;
|
(g) => g.subject.uid == uid,
|
||||||
var sum = 0.00;
|
).getAverage(halfYearFallback: halfYearFallback);
|
||||||
|
}
|
||||||
|
|
||||||
for (var grade in this) {
|
double? getAverageBySubject(Subject subject, {bool halfYearFallback = true}) {
|
||||||
if (grade.subject.uid == subject.uid) {
|
return _getAverageBySubject(
|
||||||
if (grade.numericValue != null) {
|
subject.uid,
|
||||||
var weight = (grade.weightPercentage ?? 100) / 100.0;
|
halfYearFallback: halfYearFallback,
|
||||||
weightTotal += weight;
|
);
|
||||||
|
}
|
||||||
|
|
||||||
sum += grade.numericValue! * weight;
|
double? getRoundedSubjectAverage({
|
||||||
}
|
bool halfYearFallback = true,
|
||||||
|
double t1 = 1,
|
||||||
|
double t2 = 0.5,
|
||||||
|
double t3 = 0.5,
|
||||||
|
double t4 = 0.5,
|
||||||
|
}) {
|
||||||
|
final averages = map((g) => g.subject).toSet().map((subject) {
|
||||||
|
final average = getAverageBySubject(
|
||||||
|
subject,
|
||||||
|
halfYearFallback: halfYearFallback,
|
||||||
|
);
|
||||||
|
if (average == null) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
return roundGrade(average, t1: t1, t2: t2, t3: t3, t4: t4);
|
||||||
|
}).nonNulls;
|
||||||
|
|
||||||
|
if (averages.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return averages.reduce((sum, avg) => sum + avg) / averages.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
double? getSubjectAverage({bool halfYearFallback = true}) {
|
||||||
|
final averages = where((g) => g.hasClassicValue())
|
||||||
|
.map((g) => g.subject.uid)
|
||||||
|
.toSet()
|
||||||
|
.map(
|
||||||
|
(uid) =>
|
||||||
|
_getAverageBySubject(uid, halfYearFallback: halfYearFallback),
|
||||||
|
)
|
||||||
|
.nonNulls;
|
||||||
|
|
||||||
|
if (averages.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return averages.reduce((sum, avg) => sum + avg) / averages.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
double? getAverage({bool halfYearFallback = true}) {
|
||||||
|
if (isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
double weightTotal = 0;
|
||||||
|
double sum = 0;
|
||||||
|
int? fallback;
|
||||||
|
for (Grade grade in this) {
|
||||||
|
if (grade.hasClassicValue() && halfYearFallback) {
|
||||||
|
fallback = grade.numericValue!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!grade.shouldIncludeInAverage()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
double weight = grade.weightPercentage! / 100.0;
|
||||||
|
weightTotal += weight;
|
||||||
|
sum += grade.numericValue! * weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sum == 0) {
|
||||||
|
// use classification exam results or half-year evaluations
|
||||||
|
return fallback?.toDouble();
|
||||||
}
|
}
|
||||||
|
|
||||||
return sum / weightTotal;
|
return sum / weightTotal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension GradeExtension on Grade {
|
||||||
|
bool isInPercentage() {
|
||||||
|
return valueType.name == 'Szazalekos';
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasClassicValue() {
|
||||||
|
return valueType.name == "Osztalyzat";
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://tudasbazis.ekreta.hu/pages/viewpage.action?pageId=2426172
|
||||||
|
bool shouldIncludeInAverage() {
|
||||||
|
return weightPercentage != null && hasClassicValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:firka_common/core/icon_helper.dart';
|
import 'package:firka_common/core/icon_helper.dart';
|
||||||
import 'package:firka_common/ui/shared/firka_icon.dart';
|
import 'package:firka_common/ui/shared/firka_icon.dart';
|
||||||
|
|
||||||
|
import 'package:kreta_api/kreta_api.dart';
|
||||||
|
|
||||||
class ClassIconWidget extends StatelessWidget {
|
class ClassIconWidget extends StatelessWidget {
|
||||||
final String _uid;
|
final String _uid;
|
||||||
final String _className;
|
final String _className;
|
||||||
@@ -10,6 +12,15 @@ class ClassIconWidget extends StatelessWidget {
|
|||||||
final Color color;
|
final Color color;
|
||||||
final double? size;
|
final double? size;
|
||||||
|
|
||||||
|
ClassIconWidget.subject({
|
||||||
|
super.key,
|
||||||
|
required Subject subject,
|
||||||
|
this.color = Colors.white,
|
||||||
|
this.size,
|
||||||
|
}) : _className = subject.name,
|
||||||
|
_uid = subject.uid,
|
||||||
|
_category = subject.category.name;
|
||||||
|
|
||||||
const ClassIconWidget({
|
const ClassIconWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required String uid,
|
required String uid,
|
||||||
|
|||||||
@@ -2,20 +2,26 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import 'package:firka_common/ui/theme/style.dart';
|
import 'package:firka_common/ui/theme/style.dart';
|
||||||
|
|
||||||
|
import '../../firka_common.dart';
|
||||||
|
|
||||||
class CounterDigitWidget extends StatelessWidget {
|
class CounterDigitWidget extends StatelessWidget {
|
||||||
final String c;
|
final String c;
|
||||||
final TextStyle? style;
|
|
||||||
|
|
||||||
const CounterDigitWidget(this.c, this.style, {super.key});
|
const CounterDigitWidget(this.c, {super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Card(
|
return Container(
|
||||||
shadowColor: Colors.transparent,
|
width: 18,
|
||||||
color: appStyle.colors.buttonSecondaryFill,
|
decoration: ShapeDecoration(
|
||||||
child: Padding(
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
padding: const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 4),
|
color: appStyle.colors.buttonSecondaryFill,
|
||||||
child: Text(c, style: style),
|
),
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 2),
|
||||||
|
child: Text(
|
||||||
|
c,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: appStyle.fonts.H_16px.apply(color: appStyle.colors.textPrimary),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,55 +6,84 @@ import 'package:firka_common/ui/components/grade_helpers.dart';
|
|||||||
import 'package:firka_common/ui/shared/class_icon.dart';
|
import 'package:firka_common/ui/shared/class_icon.dart';
|
||||||
import 'package:firka_common/ui/theme/style.dart';
|
import 'package:firka_common/ui/theme/style.dart';
|
||||||
|
|
||||||
class GradeSmallCard extends FirkaCard {
|
class GradeSmallCard extends StatelessWidget {
|
||||||
final List<Grade> grades;
|
final double? studentAverage;
|
||||||
|
final double? classAverage;
|
||||||
final Subject subject;
|
final Subject subject;
|
||||||
|
|
||||||
GradeSmallCard(this.grades, this.subject, {super.key})
|
GradeSmallCard(
|
||||||
: super(
|
List<Grade> grades,
|
||||||
left: [
|
this.classAverage,
|
||||||
|
this.subject, {
|
||||||
|
super.key,
|
||||||
|
}) : studentAverage = grades.getAverageBySubject(subject);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FirkaCard.single(
|
||||||
|
height: 52,
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
margin: EdgeInsets.all(0),
|
||||||
|
child: Row(
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
ClassIconWidget(
|
ClassIconWidget(
|
||||||
uid: subject.uid,
|
uid: subject.uid,
|
||||||
className: subject.name,
|
className: subject.name,
|
||||||
category: subject.category.name!,
|
category: subject.category.name!,
|
||||||
color: appStyle.colors.accent,
|
color: appStyle.colors.accent,
|
||||||
|
size: 20,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
Expanded(
|
||||||
SizedBox(
|
|
||||||
width: 200,
|
|
||||||
child: Text(
|
child: Text(
|
||||||
subject.name,
|
subject.name,
|
||||||
style: appStyle.fonts.B_16SB.apply(
|
style: appStyle.fonts.B_16SB.apply(
|
||||||
color: appStyle.colors.textPrimary,
|
color: appStyle.colors.textPrimary,
|
||||||
),
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
if (classAverage != null)
|
||||||
right: [
|
Container(
|
||||||
grades.getAverageBySubject(subject).isNaN
|
width: 48,
|
||||||
? const SizedBox()
|
height: 26,
|
||||||
: Card(
|
decoration: ShapeDecoration(
|
||||||
shadowColor: Colors.transparent,
|
color: Colors.transparent,
|
||||||
color: getGradeColor(
|
shape: RoundedRectangleBorder(
|
||||||
grades.getAverageBySubject(subject),
|
side: BorderSide(color: getGradeColor(classAverage!)),
|
||||||
).withAlpha(38),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Padding(
|
),
|
||||||
padding: const EdgeInsets.only(
|
),
|
||||||
left: 8,
|
child: Center(
|
||||||
right: 8,
|
child: Text(
|
||||||
top: 4,
|
classAverage!.toStringAsFixed(2),
|
||||||
bottom: 4,
|
style: appStyle.fonts.B_16R.apply(
|
||||||
),
|
color: getGradeColor(classAverage!),
|
||||||
child: Text(
|
|
||||||
grades.getAverageBySubject(subject).toStringAsFixed(2),
|
|
||||||
style: appStyle.fonts.B_16SB.apply(
|
|
||||||
color: getGradeColor(
|
|
||||||
grades.getAverageBySubject(subject),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (studentAverage != null)
|
||||||
|
Container(
|
||||||
|
width: 48,
|
||||||
|
height: 26,
|
||||||
|
decoration: ShapeDecoration(
|
||||||
|
color: getGradeColor(studentAverage!).withAlpha(38),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
studentAverage!.toStringAsFixed(2),
|
||||||
|
style: appStyle.fonts.B_16R.apply(
|
||||||
|
color: getGradeColor(studentAverage!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ class FirkaColors {
|
|||||||
Color textPrimary;
|
Color textPrimary;
|
||||||
Color textSecondary;
|
Color textSecondary;
|
||||||
Color textTertiary;
|
Color textTertiary;
|
||||||
|
Color? textTeritary;
|
||||||
|
|
||||||
Color textPrimaryLight;
|
Color textPrimaryLight;
|
||||||
Color textSecondaryLight;
|
Color textSecondaryLight;
|
||||||
@@ -65,6 +66,7 @@ class FirkaColors {
|
|||||||
Color cardTranslucent;
|
Color cardTranslucent;
|
||||||
|
|
||||||
Color buttonSecondaryFill;
|
Color buttonSecondaryFill;
|
||||||
|
Color buttonDisabledIcon;
|
||||||
|
|
||||||
Color accent;
|
Color accent;
|
||||||
Color secondary;
|
Color secondary;
|
||||||
@@ -97,12 +99,14 @@ class FirkaColors {
|
|||||||
required this.textPrimary,
|
required this.textPrimary,
|
||||||
required this.textSecondary,
|
required this.textSecondary,
|
||||||
required this.textTertiary,
|
required this.textTertiary,
|
||||||
|
this.textTeritary,
|
||||||
required this.textPrimaryLight,
|
required this.textPrimaryLight,
|
||||||
required this.textSecondaryLight,
|
required this.textSecondaryLight,
|
||||||
required this.textTertiaryLight,
|
required this.textTertiaryLight,
|
||||||
required this.card,
|
required this.card,
|
||||||
required this.cardTranslucent,
|
required this.cardTranslucent,
|
||||||
required this.buttonSecondaryFill,
|
required this.buttonSecondaryFill,
|
||||||
|
required this.buttonDisabledIcon,
|
||||||
required this.accent,
|
required this.accent,
|
||||||
required this.secondary,
|
required this.secondary,
|
||||||
required this.shadowColor,
|
required this.shadowColor,
|
||||||
@@ -176,25 +180,25 @@ final _defaultFonts = FirkaFonts(
|
|||||||
B_16R: TextStyle(
|
B_16R: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontFamily: 'Figtree',
|
fontFamily: 'Figtree',
|
||||||
fontVariations: [FontVariation("wght", 600)],
|
fontVariations: [FontVariation("wght", 500)],
|
||||||
height: 1.3,
|
height: 1.3,
|
||||||
),
|
),
|
||||||
B_16SB: TextStyle(
|
B_16SB: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontFamily: 'Figtree',
|
fontFamily: 'Figtree',
|
||||||
fontVariations: [FontVariation("wght", 700)],
|
fontVariations: [FontVariation("wght", 600)],
|
||||||
height: 1.3,
|
height: 1.3,
|
||||||
),
|
),
|
||||||
B_14R: TextStyle(
|
B_14R: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontFamily: 'Figtree',
|
fontFamily: 'Figtree',
|
||||||
fontVariations: [FontVariation("wght", 600)],
|
fontVariations: [FontVariation("wght", 500)],
|
||||||
height: 1.3,
|
height: 1.3,
|
||||||
),
|
),
|
||||||
B_14SB: TextStyle(
|
B_14SB: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontFamily: 'Figtree',
|
fontFamily: 'Figtree',
|
||||||
fontVariations: [FontVariation("wght", 700)],
|
fontVariations: [FontVariation("wght", 600)],
|
||||||
height: 1.3,
|
height: 1.3,
|
||||||
),
|
),
|
||||||
B_15SB: TextStyle(
|
B_15SB: TextStyle(
|
||||||
@@ -238,12 +242,14 @@ final FirkaStyle lightStyle = FirkaStyle(
|
|||||||
textPrimary: Color(0xFF394C0A),
|
textPrimary: Color(0xFF394C0A),
|
||||||
textSecondary: Color(0xCC394C0A),
|
textSecondary: Color(0xCC394C0A),
|
||||||
textTertiary: Color(0x80394C0A),
|
textTertiary: Color(0x80394C0A),
|
||||||
|
textTeritary: Color(0xFF97A474),
|
||||||
textPrimaryLight: Color(0xFF394C0A),
|
textPrimaryLight: Color(0xFF394C0A),
|
||||||
textSecondaryLight: Color(0xCC394C0A),
|
textSecondaryLight: Color(0xCC394C0A),
|
||||||
textTertiaryLight: Color(0x80394C0A),
|
textTertiaryLight: Color(0x80394C0A),
|
||||||
card: Color(0xFFF3FBDE),
|
card: Color(0xFFF3FBDE),
|
||||||
cardTranslucent: Color(0x80F3FBDE),
|
cardTranslucent: Color(0x80F3FBDE),
|
||||||
buttonSecondaryFill: Color(0xFFFEFFFD),
|
buttonSecondaryFill: Color(0xFFFEFFFD),
|
||||||
|
buttonDisabledIcon: Color(0xFFCDD9B3),
|
||||||
accent: Color(0xFFA7DC22),
|
accent: Color(0xFFA7DC22),
|
||||||
secondary: Color(0xFF6E8F1B),
|
secondary: Color(0xFF6E8F1B),
|
||||||
shadowColor: Color(0x33647e22),
|
shadowColor: Color(0x33647e22),
|
||||||
@@ -277,12 +283,14 @@ final FirkaStyle darkStyle = FirkaStyle(
|
|||||||
textPrimary: Color(0xFFEAF7CC),
|
textPrimary: Color(0xFFEAF7CC),
|
||||||
textSecondary: Color(0xB3EAF7CC),
|
textSecondary: Color(0xB3EAF7CC),
|
||||||
textTertiary: Color(0x80EAF7CC),
|
textTertiary: Color(0x80EAF7CC),
|
||||||
|
textTeritary: Color(0xFF97A474),
|
||||||
textPrimaryLight: Color(0xFF394C0A),
|
textPrimaryLight: Color(0xFF394C0A),
|
||||||
textSecondaryLight: Color(0xCC394C0A),
|
textSecondaryLight: Color(0xCC394C0A),
|
||||||
textTertiaryLight: Color(0x80394C0A),
|
textTertiaryLight: Color(0x80394C0A),
|
||||||
card: Color(0xFF141905),
|
card: Color(0xFF141905),
|
||||||
cardTranslucent: Color(0x80141905),
|
cardTranslucent: Color(0x80141905),
|
||||||
buttonSecondaryFill: Color(0xFF20290B),
|
buttonSecondaryFill: Color(0xFF20290B),
|
||||||
|
buttonDisabledIcon: Color(0xFF465422),
|
||||||
accent: Color(0xFFA7DC22),
|
accent: Color(0xFFA7DC22),
|
||||||
secondary: Color(0xFFCBEE71),
|
secondary: Color(0xFFCBEE71),
|
||||||
shadowColor: Color(0x26CBEE71),
|
shadowColor: Color(0x26CBEE71),
|
||||||
|
|||||||
@@ -296,10 +296,10 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
|
|||||||
if (nextLessonText.length > 10) {
|
if (nextLessonText.length > 10) {
|
||||||
if (nextLesson.roomName!.length > 10) {
|
if (nextLesson.roomName!.length > 10) {
|
||||||
nextLessonText =
|
nextLessonText =
|
||||||
"${nextLesson.name}, ${nextLesson.roomName!.substring(0, 6)}...";
|
"${nextLesson.name}, ${nextLesson.roomName!.substring(0, min(6, nextLesson.roomName!.length))}...";
|
||||||
} else {
|
} else {
|
||||||
nextLessonText =
|
nextLessonText =
|
||||||
"${nextLesson.name.substring(0, 10)}..., ${nextLesson.roomName}";
|
"${nextLesson.name.substring(0, min(10, nextLesson.name.length))}..., ${nextLesson.roomName}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,11 +320,12 @@ class _WearHomeScreenState extends State<WearHomeScreen> {
|
|||||||
"${currentLesson.name}, ${currentLesson.roomName}";
|
"${currentLesson.name}, ${currentLesson.roomName}";
|
||||||
if (currentLessonText.length > 10) {
|
if (currentLessonText.length > 10) {
|
||||||
if (currentLesson.roomName!.length > 10) {
|
if (currentLesson.roomName!.length > 10) {
|
||||||
|
final room = currentLesson.roomName!;
|
||||||
currentLessonText =
|
currentLessonText =
|
||||||
"${currentLesson.name}, ${currentLesson.roomName?.substring(0, 6) ?? ''}...";
|
"${currentLesson.name}, ${room.substring(0, min(6, room.length))}...";
|
||||||
} else {
|
} else {
|
||||||
currentLessonText =
|
currentLessonText =
|
||||||
"${currentLesson.name.substring(0, 10)}..., ${currentLesson.roomName}";
|
"${currentLesson.name.substring(0, min(10, currentLesson.name.length))}..., ${currentLesson.roomName}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.1.1+2000
|
version: 1.1.2+2000
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.0
|
sdk: ^3.11.0
|
||||||
|
|||||||
@@ -1,17 +1,28 @@
|
|||||||
class ApiResponse<T> {
|
class ApiResponse<T> {
|
||||||
T? response;
|
T? response;
|
||||||
int statusCode;
|
int statusCode;
|
||||||
String? err;
|
Object? err;
|
||||||
bool cached;
|
bool cached;
|
||||||
|
|
||||||
ApiResponse(this.response, this.statusCode, this.err, this.cached);
|
ApiResponse(this.response, this.statusCode, this.err, this.cached);
|
||||||
|
|
||||||
|
ApiResponse.fail(this.err) : response = null, statusCode = 0, cached = false;
|
||||||
|
|
||||||
|
ApiResponse.success(this.response, this.statusCode)
|
||||||
|
: err = null,
|
||||||
|
cached = false;
|
||||||
|
|
||||||
|
ApiResponse.cached(this.response)
|
||||||
|
: statusCode = 200,
|
||||||
|
err = null,
|
||||||
|
cached = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return "ApiResponse("
|
return "ApiResponse("
|
||||||
"response: $response, "
|
"response: $response, "
|
||||||
"statusCode: $statusCode, "
|
"statusCode: $statusCode, "
|
||||||
"err: \"$err\", "
|
"err: \"${err.toString()}\", "
|
||||||
"cached: $cached"
|
"cached: $cached"
|
||||||
")";
|
")";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
/// URL builders for Kreta ellenorzo API that depend only on [iss].
|
/// URL builders for Kreta ellenorzo API that depend only on [iss].
|
||||||
/// Auth-related URLs (login, token) and Constants stay in the app (firka).
|
/// Auth-related URLs (login, token) and Constants stay in the app (firka).
|
||||||
class KretaEndpoints {
|
class KretaEndpoints {
|
||||||
static const String kretaBase = "e-kreta.hu";
|
static const String kretaBase = "e-kreta.hu";
|
||||||
|
static DateFormat dateTimeFormat = DateFormat('yyyy-MM-dd');
|
||||||
|
|
||||||
static String kreta(String iss) {
|
static String kreta(String iss) {
|
||||||
if (iss == "firka-test") {
|
if (iss == "firka-test") {
|
||||||
@@ -11,35 +14,58 @@ class KretaEndpoints {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String dateQuery(DateTime? from, DateTime? to) {
|
||||||
|
StringBuffer buffer = StringBuffer();
|
||||||
|
|
||||||
|
if (from == null) {
|
||||||
|
if (to == null) {
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
throw Exception("'from' is required to use 'to'");
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.write("?datumTol=");
|
||||||
|
buffer.write(dateTimeFormat.format(from));
|
||||||
|
|
||||||
|
if (to != null) {
|
||||||
|
buffer.write("&datumIg=");
|
||||||
|
buffer.write(dateTimeFormat.format(to));
|
||||||
|
}
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
|
||||||
static String getStudentUrl(String iss) =>
|
static String getStudentUrl(String iss) =>
|
||||||
"${kreta(iss)}/ellenorzo/v3/sajat/TanuloAdatlap";
|
"${kreta(iss)}/ellenorzo/v3/sajat/TanuloAdatlap";
|
||||||
|
|
||||||
static String getClassGroups(String iss) =>
|
static String getClassGroups(String iss) =>
|
||||||
"${kreta(iss)}/ellenorzo/v3/sajat/OsztalyCsoportok";
|
"${kreta(iss)}/ellenorzo/v3/sajat/OsztalyCsoportok";
|
||||||
|
|
||||||
static String getNoticeBoard(String iss) =>
|
static String getNoticeBoard(String iss, [DateTime? from, DateTime? to]) =>
|
||||||
"${kreta(iss)}/ellenorzo/v3/sajat/FaliujsagElemek";
|
"${kreta(iss)}/ellenorzo/v3/sajat/FaliujsagElemek${dateQuery(from, to)}";
|
||||||
|
|
||||||
static String getInfoBoard(String iss) =>
|
static String getInfoBoard(String iss, [DateTime? from, DateTime? to]) =>
|
||||||
"${kreta(iss)}/ellenorzo/v3/sajat/Feljegyzesek";
|
"${kreta(iss)}/ellenorzo/v3/sajat/Feljegyzesek${dateQuery(from, to)}";
|
||||||
|
|
||||||
static String getGrades(String iss) =>
|
static String getGrades(String iss, [DateTime? from, DateTime? to]) =>
|
||||||
"${kreta(iss)}/ellenorzo/v3/sajat/Ertekelesek";
|
"${kreta(iss)}/ellenorzo/v3/sajat/Ertekelesek${dateQuery(from, to)}";
|
||||||
|
|
||||||
static String getSubjectAvg(String iss, String studyGroupId) =>
|
static String getSubjectAvg(String iss, String studyGroupId) =>
|
||||||
"${kreta(iss)}/ellenorzo/v3/sajat/Ertekelesek/Atlagok/TantargyiAtlagok?oktatasiNevelesiFeladatUid=$studyGroupId&oktatasiNevelesiFeladatUid=$studyGroupId";
|
"${kreta(iss)}/ellenorzo/v3/sajat/Ertekelesek/Atlagok/TantargyiAtlagok?oktatasiNevelesiFeladatUid=$studyGroupId&oktatasiNevelesiFeladatUid=$studyGroupId";
|
||||||
|
|
||||||
static String getTimeTable(String iss) =>
|
static String getClassGroupAvg(String iss, String studyGroupId) =>
|
||||||
"${kreta(iss)}/ellenorzo/v3/sajat/OrarendElemek";
|
"${kreta(iss)}/ellenorzo/v3/sajat/Ertekelesek/Atlagok/OsztalyAtlagok?oktatasiNevelesiFeladatUid=$studyGroupId&oktatasiNevelesiFeladatUid=$studyGroupId";
|
||||||
|
|
||||||
static String getOmissions(String iss) =>
|
static String getTimeTable(String iss, [DateTime? from, DateTime? to]) =>
|
||||||
"${kreta(iss)}/ellenorzo/v3/sajat/Mulasztasok";
|
"${kreta(iss)}/ellenorzo/v3/sajat/OrarendElemek${dateQuery(from, to)}";
|
||||||
|
|
||||||
static String getHomework(String iss) =>
|
static String getOmissions(String iss, [DateTime? from, DateTime? to]) =>
|
||||||
"${kreta(iss)}/ellenorzo/v3/sajat/HaziFeladatok";
|
"${kreta(iss)}/ellenorzo/v3/sajat/Mulasztasok${dateQuery(from, to)}";
|
||||||
|
|
||||||
static String getTests(String iss) =>
|
static String getHomework(String iss, [DateTime? from, DateTime? to]) =>
|
||||||
"${kreta(iss)}/ellenorzo/v3/sajat/BejelentettSzamonkeresek";
|
"${kreta(iss)}/ellenorzo/v3/sajat/HaziFeladatok${dateQuery(from, to)}";
|
||||||
|
|
||||||
|
static String getTests(String iss, [DateTime? from, DateTime? to]) =>
|
||||||
|
"${kreta(iss)}/ellenorzo/v3/sajat/BejelentettSzamonkeresek${dateQuery(from, to)}";
|
||||||
|
|
||||||
static String getLessons(String iss) =>
|
static String getLessons(String iss) =>
|
||||||
"${kreta(iss)}/dktapi/intezmenyek/munkaterek/tanulok";
|
"${kreta(iss)}/dktapi/intezmenyek/munkaterek/tanulok";
|
||||||
|
|||||||
17
kreta_api/lib/src/extensions.dart
Normal file
17
kreta_api/lib/src/extensions.dart
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
extension JsonHelper on Map<String, dynamic> {
|
||||||
|
double? dbl(String key) {
|
||||||
|
final value = this[key];
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (value as num).toDouble();
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime? localDate(String key) {
|
||||||
|
final value = this[key];
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return DateTime.parse(value as String).toLocal();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@ class AllLessons {
|
|||||||
final String teacherAvatarTypePath;
|
final String teacherAvatarTypePath;
|
||||||
final dynamic taskGroupId;
|
final dynamic taskGroupId;
|
||||||
|
|
||||||
AllLessons({
|
const AllLessons({
|
||||||
required this.schoolId,
|
required this.schoolId,
|
||||||
required this.yearId,
|
required this.yearId,
|
||||||
this.classId,
|
this.classId,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import '../extensions.dart';
|
||||||
import 'generic.dart';
|
import 'generic.dart';
|
||||||
|
import 'subject.dart';
|
||||||
|
|
||||||
class ClassGroup {
|
class ClassGroup extends NameUid {
|
||||||
final String uid;
|
|
||||||
final String name;
|
|
||||||
final UidObj? headTeacher;
|
final UidObj? headTeacher;
|
||||||
final UidObj? substituteHeadTeacher;
|
final UidObj? substituteHeadTeacher;
|
||||||
final NameUidDesc studyGroup;
|
final NameUidDesc studyGroup;
|
||||||
@@ -11,9 +11,9 @@ class ClassGroup {
|
|||||||
final bool isActive;
|
final bool isActive;
|
||||||
final String type;
|
final String type;
|
||||||
|
|
||||||
ClassGroup({
|
const ClassGroup({
|
||||||
required this.uid,
|
required super.uid,
|
||||||
required this.name,
|
required super.name,
|
||||||
required this.headTeacher,
|
required this.headTeacher,
|
||||||
required this.substituteHeadTeacher,
|
required this.substituteHeadTeacher,
|
||||||
required this.studyGroup,
|
required this.studyGroup,
|
||||||
@@ -27,17 +27,11 @@ class ClassGroup {
|
|||||||
return ClassGroup(
|
return ClassGroup(
|
||||||
uid: json['Uid'],
|
uid: json['Uid'],
|
||||||
name: json['Nev'],
|
name: json['Nev'],
|
||||||
headTeacher: json['OsztalyFonok'] != null
|
headTeacher: json.uid('OsztalyFonok'),
|
||||||
? UidObj.fromJson(json['OsztalyFonok'])
|
substituteHeadTeacher: json.uid('OsztalyFonokHelyettes'),
|
||||||
: null,
|
studyGroup: json.nameUidDesc('OktatasNevelesiKategoria')!,
|
||||||
substituteHeadTeacher: json['OsztalyFonokHelyettes'] != null
|
|
||||||
? UidObj.fromJson(json['OsztalyFonokHelyettes'])
|
|
||||||
: null,
|
|
||||||
studyGroup: NameUidDesc.fromJson(json['OktatasNevelesiKategoria']),
|
|
||||||
studyGroupSortIndex: json['OktatasNevelesiKategoriaSortIndex'],
|
studyGroupSortIndex: json['OktatasNevelesiKategoriaSortIndex'],
|
||||||
studyTask: json['OktatasNevelesiFeladat'] != null
|
studyTask: json.nameUidDesc('OktatasNevelesiFeladat'),
|
||||||
? NameUidDesc.fromJson(json['OktatasNevelesiFeladat'])
|
|
||||||
: null,
|
|
||||||
isActive: json['IsAktiv'],
|
isActive: json['IsAktiv'],
|
||||||
type: json['Tipus'],
|
type: json['Tipus'],
|
||||||
);
|
);
|
||||||
@@ -59,55 +53,59 @@ class ClassGroup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SubjectAverage {
|
class SubjectAverage extends UidObj {
|
||||||
final String uid;
|
final Subject subject;
|
||||||
final String name;
|
|
||||||
final String? teacherName;
|
|
||||||
final String subjectCategoryId;
|
|
||||||
final String subjectCategoryName;
|
|
||||||
final String subjectCategoryDescription;
|
|
||||||
final double? average;
|
final double? average;
|
||||||
final double? weightedSum;
|
final double? weightedSum;
|
||||||
final double? weightedCount;
|
final double? weightedCount;
|
||||||
final int sortIndex;
|
|
||||||
|
|
||||||
SubjectAverage({
|
const SubjectAverage({
|
||||||
required this.uid,
|
required super.uid,
|
||||||
required this.name,
|
required this.subject,
|
||||||
this.teacherName,
|
|
||||||
required this.subjectCategoryId,
|
|
||||||
required this.subjectCategoryName,
|
|
||||||
required this.subjectCategoryDescription,
|
|
||||||
this.average,
|
this.average,
|
||||||
this.weightedSum,
|
this.weightedSum,
|
||||||
this.weightedCount,
|
this.weightedCount,
|
||||||
required this.sortIndex,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
factory SubjectAverage.fromJson(Map<String, dynamic> json) {
|
factory SubjectAverage.fromJson(Map<String, dynamic> json) {
|
||||||
final tantargy = json['Tantargy'] ?? {};
|
|
||||||
final kategori = tantargy['Kategoria'] ?? {};
|
|
||||||
|
|
||||||
return SubjectAverage(
|
return SubjectAverage(
|
||||||
uid: json['Uid'] ?? '',
|
uid: json['Uid'],
|
||||||
name: tantargy['Nev'] ?? '',
|
subject: Subject.fromJson(json['Tantargy']),
|
||||||
teacherName: json['TeacherName'],
|
average: json.dbl('Atlag'),
|
||||||
subjectCategoryId: kategori['Uid'] ?? '',
|
weightedSum: json.dbl('SulyozottOsztalyzatOsszege'),
|
||||||
subjectCategoryName: kategori['Nev'] ?? '',
|
weightedCount: json.dbl('SulyozottOsztalyzatSzama'),
|
||||||
subjectCategoryDescription: kategori['Leiras'] ?? '',
|
|
||||||
average: json['Atlag'] != null ? (json['Atlag'] as num).toDouble() : null,
|
|
||||||
weightedSum: json['SulyozottOsztalyzatOsszege'] != null
|
|
||||||
? (json['SulyozottOsztalyzatOsszege'] as num).toDouble()
|
|
||||||
: null,
|
|
||||||
weightedCount: json['SulyozottOsztalyzatSzama'] != null
|
|
||||||
? (json['SulyozottOsztalyzatSzama'] as num).toDouble()
|
|
||||||
: null,
|
|
||||||
sortIndex: tantargy['SortIndex'] ?? 0,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SubjectAverage(uid: "$uid", name: "$name", category: "$subjectCategoryName", average: $average)';
|
return 'SubjectAverage(uid: "$uid", name: "${subject.name}", category: "${subject.category.name}", average: $average)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ClassGroupSubjectAverage extends UidObj {
|
||||||
|
final Subject subject;
|
||||||
|
final double? studentAverage;
|
||||||
|
final double? classGroupAverage;
|
||||||
|
|
||||||
|
const ClassGroupSubjectAverage({
|
||||||
|
required super.uid,
|
||||||
|
required this.subject,
|
||||||
|
this.classGroupAverage,
|
||||||
|
this.studentAverage,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ClassGroupSubjectAverage.fromJson(Map<String, dynamic> json) {
|
||||||
|
return ClassGroupSubjectAverage(
|
||||||
|
uid: json['Uid'],
|
||||||
|
subject: Subject.fromJson(json['Tantargy']),
|
||||||
|
studentAverage: json.dbl('TanuloAtlag'),
|
||||||
|
classGroupAverage: json.dbl('OsztalyCsoportAtlag'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'ClassGroupSubjectAverage(uid: "$uid", subject: $subject)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
class NameUidDesc {
|
class NameUidDesc extends NameUid {
|
||||||
final String uid;
|
static final EMPTY = NameUidDesc(name: "", uid: "", description: "");
|
||||||
final String? name;
|
final String description;
|
||||||
final String? description;
|
|
||||||
|
|
||||||
NameUidDesc({
|
const NameUidDesc({
|
||||||
required this.uid,
|
required super.uid,
|
||||||
required this.name,
|
required super.name,
|
||||||
required this.description,
|
required this.description,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -31,11 +30,10 @@ class NameUidDesc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NameUid {
|
class NameUid extends UidObj {
|
||||||
final String uid;
|
|
||||||
final String name;
|
final String name;
|
||||||
|
|
||||||
NameUid({required this.uid, required this.name});
|
const NameUid({required super.uid, required this.name});
|
||||||
|
|
||||||
factory NameUid.fromJson(Map<String, dynamic> json) {
|
factory NameUid.fromJson(Map<String, dynamic> json) {
|
||||||
return NameUid(uid: json['Uid'], name: json['Nev']);
|
return NameUid(uid: json['Uid'], name: json['Nev']);
|
||||||
@@ -49,7 +47,7 @@ class NameUid {
|
|||||||
class UidObj {
|
class UidObj {
|
||||||
final String uid;
|
final String uid;
|
||||||
|
|
||||||
UidObj({required this.uid});
|
const UidObj({required this.uid});
|
||||||
|
|
||||||
factory UidObj.fromJson(Map<String, dynamic> json) {
|
factory UidObj.fromJson(Map<String, dynamic> json) {
|
||||||
return UidObj(uid: json['Uid']);
|
return UidObj(uid: json['Uid']);
|
||||||
@@ -62,3 +60,29 @@ class UidObj {
|
|||||||
')';
|
')';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ToUidObj on Map<String, dynamic> {
|
||||||
|
UidObj? uid(String key) {
|
||||||
|
final value = this[key];
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return UidObj.fromJson(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
NameUid? nameUid(String key) {
|
||||||
|
final value = this[key];
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return NameUid.fromJson(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
NameUidDesc? nameUidDesc(String key) {
|
||||||
|
final value = this[key];
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return NameUidDesc.fromJson(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import 'generic.dart';
|
import 'generic.dart';
|
||||||
import 'subject.dart';
|
import 'subject.dart';
|
||||||
|
|
||||||
class Grade {
|
class Grade extends UidObj {
|
||||||
final String uid;
|
|
||||||
final DateTime recordDate;
|
final DateTime recordDate;
|
||||||
final DateTime creationDate;
|
final DateTime creationDate;
|
||||||
final DateTime? ackDate;
|
final DateTime? ackDate;
|
||||||
@@ -10,18 +9,18 @@ class Grade {
|
|||||||
final String? topic;
|
final String? topic;
|
||||||
final NameUidDesc type;
|
final NameUidDesc type;
|
||||||
final NameUidDesc? mode;
|
final NameUidDesc? mode;
|
||||||
NameUidDesc valueType;
|
final NameUidDesc valueType;
|
||||||
final String teacher;
|
final String teacher;
|
||||||
final String? kind;
|
final String? kind;
|
||||||
int? numericValue;
|
final int? numericValue;
|
||||||
final String strValue;
|
final String strValue;
|
||||||
final int? weightPercentage;
|
final int? weightPercentage;
|
||||||
final String? shortStrValue;
|
final String? shortStrValue;
|
||||||
final UidObj? classGroup;
|
final UidObj? classGroup;
|
||||||
final int sortIndex;
|
final int sortIndex;
|
||||||
|
|
||||||
Grade({
|
const Grade({
|
||||||
required this.uid,
|
required super.uid,
|
||||||
required this.recordDate,
|
required this.recordDate,
|
||||||
required this.creationDate,
|
required this.creationDate,
|
||||||
this.ackDate,
|
this.ackDate,
|
||||||
@@ -50,18 +49,16 @@ class Grade {
|
|||||||
: null,
|
: null,
|
||||||
subject: Subject.fromJson(json['Tantargy']),
|
subject: Subject.fromJson(json['Tantargy']),
|
||||||
topic: json['Tema'],
|
topic: json['Tema'],
|
||||||
type: NameUidDesc.fromJson(json['Tipus']),
|
type: json.nameUidDesc('Tipus')!,
|
||||||
mode: json['Mod'] != null ? NameUidDesc.fromJson(json['Mod']) : null,
|
mode: json.nameUidDesc('Mod'),
|
||||||
valueType: NameUidDesc.fromJson(json['ErtekFajta']),
|
valueType: json.nameUidDesc('ErtekFajta')!,
|
||||||
teacher: json['ErtekeloTanarNeve'],
|
teacher: json['ErtekeloTanarNeve'],
|
||||||
kind: json['Kind'],
|
kind: json['Kind'],
|
||||||
numericValue: json['SzamErtek'],
|
numericValue: json['SzamErtek'],
|
||||||
strValue: json['SzovegesErtek'],
|
strValue: json['SzovegesErtek'],
|
||||||
weightPercentage: json['SulySzazalekErteke'],
|
weightPercentage: json['SulySzazalekErteke'],
|
||||||
shortStrValue: json['SzovegesErtekelesRovidNev'],
|
shortStrValue: json['SzovegesErtekelesRovidNev'],
|
||||||
classGroup: json['OsztalyCsoport'] != null
|
classGroup: json.uid('OsztalyCsoport'),
|
||||||
? UidObj.fromJson(json['OsztalyCsoport'])
|
|
||||||
: null,
|
|
||||||
sortIndex: json['SortIndex'],
|
sortIndex: json['SortIndex'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
class Guardian {
|
import 'generic.dart';
|
||||||
|
|
||||||
|
class Guardian extends NameUid {
|
||||||
final String? email;
|
final String? email;
|
||||||
final bool isLegalRepresentative;
|
final bool isLegalRepresentative;
|
||||||
final String? name;
|
|
||||||
final String? phoneNumber;
|
final String? phoneNumber;
|
||||||
final String uid;
|
|
||||||
|
|
||||||
Guardian({
|
const Guardian({
|
||||||
required this.email,
|
required this.email,
|
||||||
required this.isLegalRepresentative,
|
required this.isLegalRepresentative,
|
||||||
required this.name,
|
required super.name,
|
||||||
required this.phoneNumber,
|
required this.phoneNumber,
|
||||||
required this.uid,
|
required super.uid,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Guardian.fromJson(Map<String, dynamic> json) {
|
factory Guardian.fromJson(Map<String, dynamic> json) {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import '../extensions.dart';
|
||||||
import 'generic.dart';
|
import 'generic.dart';
|
||||||
import 'subject.dart';
|
import 'subject.dart';
|
||||||
|
|
||||||
class Homework {
|
class Homework extends UidObj {
|
||||||
final String uid;
|
|
||||||
final Subject subject;
|
final Subject subject;
|
||||||
final String subjectName;
|
final String subjectName;
|
||||||
final String teacherName;
|
final String teacherName;
|
||||||
@@ -16,8 +16,8 @@ class Homework {
|
|||||||
final UidObj classGroup;
|
final UidObj classGroup;
|
||||||
final bool canAttach;
|
final bool canAttach;
|
||||||
|
|
||||||
Homework({
|
const Homework({
|
||||||
required this.uid,
|
required super.uid,
|
||||||
required this.subject,
|
required this.subject,
|
||||||
required this.subjectName,
|
required this.subjectName,
|
||||||
required this.teacherName,
|
required this.teacherName,
|
||||||
@@ -39,13 +39,13 @@ class Homework {
|
|||||||
subjectName: json["TantargyNeve"],
|
subjectName: json["TantargyNeve"],
|
||||||
teacherName: json["RogzitoTanarNeve"],
|
teacherName: json["RogzitoTanarNeve"],
|
||||||
description: json["Szoveg"],
|
description: json["Szoveg"],
|
||||||
startDate: DateTime.parse(json["FeladasDatuma"]).toLocal(),
|
startDate: json.localDate("FeladasDatuma")!,
|
||||||
dueDate: DateTime.parse(json["HataridoDatuma"]).toLocal(),
|
dueDate: json.localDate("HataridoDatuma")!,
|
||||||
creationDate: DateTime.parse(json["RogzitesIdopontja"]).toLocal(),
|
creationDate: json.localDate("RogzitesIdopontja")!,
|
||||||
isCreatedByTeacher: json["IsTanarRogzitette"],
|
isCreatedByTeacher: json["IsTanarRogzitette"],
|
||||||
isDone: json["IsMegoldva"],
|
isDone: json["IsMegoldva"],
|
||||||
canBeSubmitted: json["IsBeadhato"],
|
canBeSubmitted: json["IsBeadhato"],
|
||||||
classGroup: UidObj.fromJson(json["OsztalyCsoport"]),
|
classGroup: json.uid("OsztalyCsoport")!,
|
||||||
canAttach: json["IsCsatolasEngedelyezes"],
|
canAttach: json["IsCsatolasEngedelyezes"],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
class Institution {
|
import 'generic.dart';
|
||||||
|
|
||||||
|
class Institution extends UidObj {
|
||||||
final CustomizationSettings customizationSettings;
|
final CustomizationSettings customizationSettings;
|
||||||
final String shortName;
|
final String shortName;
|
||||||
final List<SystemModule> systemModuleList;
|
final List<SystemModule> systemModuleList;
|
||||||
final String uid;
|
|
||||||
|
|
||||||
Institution({
|
const Institution({
|
||||||
required this.customizationSettings,
|
required this.customizationSettings,
|
||||||
required this.shortName,
|
required this.shortName,
|
||||||
required this.systemModuleList,
|
required this.systemModuleList,
|
||||||
required this.uid,
|
required super.uid,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Institution.fromJson(Map<String, dynamic> json) {
|
factory Institution.fromJson(Map<String, dynamic> json) {
|
||||||
@@ -35,7 +36,7 @@ class CustomizationSettings {
|
|||||||
final bool isLessonsThemeVisible;
|
final bool isLessonsThemeVisible;
|
||||||
final String nextServerDeployAsString;
|
final String nextServerDeployAsString;
|
||||||
|
|
||||||
CustomizationSettings({
|
const CustomizationSettings({
|
||||||
required this.delayForNotifications,
|
required this.delayForNotifications,
|
||||||
required this.isClassAverageVisible,
|
required this.isClassAverageVisible,
|
||||||
required this.isLessonsThemeVisible,
|
required this.isLessonsThemeVisible,
|
||||||
@@ -68,7 +69,11 @@ class SystemModule {
|
|||||||
final String type;
|
final String type;
|
||||||
final String? url;
|
final String? url;
|
||||||
|
|
||||||
SystemModule({required this.isActive, required this.type, required this.url});
|
const SystemModule({
|
||||||
|
required this.isActive,
|
||||||
|
required this.type,
|
||||||
|
required this.url,
|
||||||
|
});
|
||||||
|
|
||||||
factory SystemModule.fromJson(Map<String, dynamic> json) {
|
factory SystemModule.fromJson(Map<String, dynamic> json) {
|
||||||
return SystemModule(
|
return SystemModule(
|
||||||
|
|||||||
@@ -1,30 +1,46 @@
|
|||||||
|
import '../extensions.dart';
|
||||||
import 'generic.dart';
|
import 'generic.dart';
|
||||||
|
|
||||||
class NoticeBoardItem {
|
abstract class MessageItem extends UidObj {
|
||||||
final String uid;
|
|
||||||
final String author;
|
|
||||||
final DateTime validFrom;
|
|
||||||
final DateTime validTo;
|
|
||||||
final String title;
|
final String title;
|
||||||
|
final String author;
|
||||||
final String contentHTML;
|
final String contentHTML;
|
||||||
final String contentText;
|
final String contentText;
|
||||||
|
|
||||||
NoticeBoardItem({
|
const MessageItem({
|
||||||
required this.uid,
|
required super.uid,
|
||||||
required this.author,
|
|
||||||
required this.validFrom,
|
|
||||||
required this.validTo,
|
|
||||||
required this.title,
|
required this.title,
|
||||||
|
required this.author,
|
||||||
required this.contentHTML,
|
required this.contentHTML,
|
||||||
required this.contentText,
|
required this.contentText,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
DateTime get date;
|
||||||
|
}
|
||||||
|
|
||||||
|
class NoticeBoardItem extends MessageItem {
|
||||||
|
final DateTime validFrom;
|
||||||
|
final DateTime validTo;
|
||||||
|
|
||||||
|
@override
|
||||||
|
DateTime get date => validFrom;
|
||||||
|
|
||||||
|
const NoticeBoardItem({
|
||||||
|
required super.uid,
|
||||||
|
required super.title,
|
||||||
|
required super.author,
|
||||||
|
required super.contentHTML,
|
||||||
|
required super.contentText,
|
||||||
|
required this.validFrom,
|
||||||
|
required this.validTo,
|
||||||
|
});
|
||||||
|
|
||||||
factory NoticeBoardItem.fromJson(Map<String, dynamic> json) {
|
factory NoticeBoardItem.fromJson(Map<String, dynamic> json) {
|
||||||
return NoticeBoardItem(
|
return NoticeBoardItem(
|
||||||
uid: json['Uid'],
|
uid: json['Uid'],
|
||||||
author: json['RogzitoNeve'],
|
author: json['RogzitoNeve'],
|
||||||
validFrom: DateTime.parse(json['ErvenyessegKezdete']),
|
validFrom: json.localDate('ErvenyessegKezdete')!,
|
||||||
validTo: DateTime.parse(json['ErvenyessegVege']),
|
validTo: json.localDate('ErvenyessegVege')!,
|
||||||
title: json['Cim'],
|
title: json['Cim'],
|
||||||
contentHTML: json['Tartalom'],
|
contentHTML: json['Tartalom'],
|
||||||
contentText: json['TartalomText'],
|
contentText: json['TartalomText'],
|
||||||
@@ -45,24 +61,19 @@ class NoticeBoardItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class InfoBoardItem {
|
class InfoBoardItem extends MessageItem {
|
||||||
final String uid;
|
|
||||||
final String title;
|
|
||||||
final DateTime date;
|
final DateTime date;
|
||||||
final String author;
|
|
||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
final String contentHTML;
|
|
||||||
final String contentText;
|
|
||||||
final NameUidDesc type;
|
final NameUidDesc type;
|
||||||
|
|
||||||
InfoBoardItem({
|
const InfoBoardItem({
|
||||||
required this.uid,
|
required super.uid,
|
||||||
required this.title,
|
required super.title,
|
||||||
required this.date,
|
required super.author,
|
||||||
required this.author,
|
required super.contentHTML,
|
||||||
|
required super.contentText,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.contentHTML,
|
required this.date,
|
||||||
required this.contentText,
|
|
||||||
required this.type,
|
required this.type,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -70,12 +81,12 @@ class InfoBoardItem {
|
|||||||
return InfoBoardItem(
|
return InfoBoardItem(
|
||||||
uid: json['Uid'],
|
uid: json['Uid'],
|
||||||
title: json['Cim'],
|
title: json['Cim'],
|
||||||
date: DateTime.parse(json['Datum']),
|
date: json.localDate('Datum')!,
|
||||||
author: json['KeszitoTanarNeve'],
|
author: json['KeszitoTanarNeve'],
|
||||||
createdAt: DateTime.parse(json['KeszitesDatuma']),
|
createdAt: json.localDate('KeszitesDatuma')!,
|
||||||
contentText: json['Tartalom'],
|
contentText: json['Tartalom'],
|
||||||
contentHTML: json['TartalomFormazott'],
|
contentHTML: json['TartalomFormazott'],
|
||||||
type: NameUidDesc.fromJson(json['Tipus']),
|
type: json.nameUidDesc('Tipus')!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import '../extensions.dart';
|
||||||
import 'generic.dart';
|
import 'generic.dart';
|
||||||
import 'subject.dart';
|
import 'subject.dart';
|
||||||
|
|
||||||
class Omission {
|
class Omission extends UidObj {
|
||||||
final String uid;
|
|
||||||
final Subject subject;
|
final Subject subject;
|
||||||
final Class? c;
|
final Class? c;
|
||||||
final DateTime date;
|
final DateTime date;
|
||||||
@@ -15,8 +15,8 @@ class Omission {
|
|||||||
final NameUidDesc? proofType;
|
final NameUidDesc? proofType;
|
||||||
final UidObj? classGroup;
|
final UidObj? classGroup;
|
||||||
|
|
||||||
Omission({
|
const Omission({
|
||||||
required this.uid,
|
required super.uid,
|
||||||
required this.subject,
|
required this.subject,
|
||||||
required this.c,
|
required this.c,
|
||||||
required this.date,
|
required this.date,
|
||||||
@@ -35,19 +35,15 @@ class Omission {
|
|||||||
uid: json['Uid'],
|
uid: json['Uid'],
|
||||||
subject: Subject.fromJson(json['Tantargy']),
|
subject: Subject.fromJson(json['Tantargy']),
|
||||||
c: json['Osztaly'] != null ? Class.fromJson(json['Osztaly']) : null,
|
c: json['Osztaly'] != null ? Class.fromJson(json['Osztaly']) : null,
|
||||||
date: DateTime.parse(json['Datum']).toLocal(),
|
date: json.localDate('Datum')!,
|
||||||
teacher: json['RogzitoTanarNeve'],
|
teacher: json['RogzitoTanarNeve'],
|
||||||
type: json['Tipus'] != null ? NameUidDesc.fromJson(json['Tipus']) : null,
|
type: json.nameUidDesc('Tipus'),
|
||||||
mode: json['Mod'] != null ? NameUidDesc.fromJson(json['Mod']) : null,
|
mode: json.nameUidDesc('Mod'),
|
||||||
lateForMin: json['KesesPercben'],
|
lateForMin: json['KesesPercben'],
|
||||||
createdAt: DateTime.parse(json['KeszitesDatuma']).toLocal(),
|
createdAt: json.localDate('KeszitesDatuma')!,
|
||||||
state: json['IgazolasAllapota'],
|
state: json['IgazolasAllapota'],
|
||||||
proofType: json['IgazolasTipusa'] != null
|
proofType: json.nameUidDesc('IgazolasTipusa'),
|
||||||
? NameUidDesc.fromJson(json['IgazolasTipusa'])
|
classGroup: json.uid('OsztalyCsoport'),
|
||||||
: null,
|
|
||||||
classGroup: json['OsztalyCsoport'] != null
|
|
||||||
? UidObj.fromJson(json['OsztalyCsoport'])
|
|
||||||
: null,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,12 +71,12 @@ class Class {
|
|||||||
final DateTime end;
|
final DateTime end;
|
||||||
final int classNo;
|
final int classNo;
|
||||||
|
|
||||||
Class({required this.start, required this.end, required this.classNo});
|
const Class({required this.start, required this.end, required this.classNo});
|
||||||
|
|
||||||
factory Class.fromJson(Map<String, dynamic> json) {
|
factory Class.fromJson(Map<String, dynamic> json) {
|
||||||
return Class(
|
return Class(
|
||||||
start: DateTime.parse(json['KezdoDatum']).toLocal(),
|
start: json.localDate('KezdoDatum')!,
|
||||||
end: DateTime.parse(json['VegDatum']).toLocal(),
|
end: json.localDate('VegDatum')!,
|
||||||
classNo: json['Oraszam'],
|
classNo: json['Oraszam'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
import 'generic.dart';
|
||||||
import 'guardian.dart';
|
import 'guardian.dart';
|
||||||
import 'institution.dart';
|
import 'institution.dart';
|
||||||
|
|
||||||
class Student {
|
class Student extends NameUid {
|
||||||
final List<String> addressDataList;
|
final List<String> addressDataList;
|
||||||
final BankAccount bankAccount;
|
final BankAccount bankAccount;
|
||||||
|
|
||||||
final DateTime birthdate;
|
final DateTime birthdate;
|
||||||
|
|
||||||
final String? emailAddress;
|
final String? emailAddress;
|
||||||
final String name;
|
|
||||||
final String? phoneNumber;
|
final String? phoneNumber;
|
||||||
|
|
||||||
final String schoolYearUID;
|
final String schoolYearUID;
|
||||||
final String uid;
|
|
||||||
|
|
||||||
final List<Guardian> guardianList;
|
final List<Guardian> guardianList;
|
||||||
final String instituteCode;
|
final String instituteCode;
|
||||||
@@ -22,15 +21,15 @@ class Student {
|
|||||||
|
|
||||||
final Institution institution;
|
final Institution institution;
|
||||||
|
|
||||||
Student({
|
const Student({
|
||||||
required this.addressDataList,
|
required this.addressDataList,
|
||||||
required this.bankAccount,
|
required this.bankAccount,
|
||||||
required this.birthdate,
|
required this.birthdate,
|
||||||
required this.emailAddress,
|
required this.emailAddress,
|
||||||
required this.name,
|
required super.name,
|
||||||
required this.phoneNumber,
|
required this.phoneNumber,
|
||||||
required this.schoolYearUID,
|
required this.schoolYearUID,
|
||||||
required this.uid,
|
required super.uid,
|
||||||
required this.guardianList,
|
required this.guardianList,
|
||||||
required this.instituteCode,
|
required this.instituteCode,
|
||||||
required this.instituteName,
|
required this.instituteName,
|
||||||
@@ -86,7 +85,7 @@ class BankAccount {
|
|||||||
final String? ownerName;
|
final String? ownerName;
|
||||||
final int? ownerType;
|
final int? ownerType;
|
||||||
|
|
||||||
BankAccount({
|
const BankAccount({
|
||||||
required this.accountNumber,
|
required this.accountNumber,
|
||||||
required this.isReadOnly,
|
required this.isReadOnly,
|
||||||
required this.ownerName,
|
required this.ownerName,
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import 'generic.dart';
|
import 'generic.dart';
|
||||||
|
|
||||||
class Subject {
|
class Subject extends NameUid {
|
||||||
final String uid;
|
|
||||||
final String name;
|
|
||||||
final NameUidDesc category;
|
final NameUidDesc category;
|
||||||
final int sortIndex;
|
final int sortIndex;
|
||||||
final String? teacherName;
|
final String? teacherName;
|
||||||
|
|
||||||
Subject({
|
const Subject({
|
||||||
required this.uid,
|
required super.uid,
|
||||||
required this.name,
|
required super.name,
|
||||||
required this.category,
|
required this.category,
|
||||||
required this.sortIndex,
|
required this.sortIndex,
|
||||||
this.teacherName,
|
this.teacherName,
|
||||||
@@ -19,7 +17,7 @@ class Subject {
|
|||||||
return Subject(
|
return Subject(
|
||||||
uid: json['Uid'],
|
uid: json['Uid'],
|
||||||
name: json['Nev'],
|
name: json['Nev'],
|
||||||
category: NameUidDesc.fromJson(json['Kategoria']),
|
category: json.nameUidDesc('Kategoria')!,
|
||||||
sortIndex: json['SortIndex'],
|
sortIndex: json['SortIndex'],
|
||||||
teacherName: json['alkalmazottNev'],
|
teacherName: json['alkalmazottNev'],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
|
import '../extensions.dart';
|
||||||
import 'generic.dart';
|
import 'generic.dart';
|
||||||
import 'subject.dart';
|
import 'subject.dart';
|
||||||
|
|
||||||
class Test {
|
class Test extends UidObj {
|
||||||
final String uid;
|
|
||||||
final DateTime date;
|
final DateTime date;
|
||||||
final DateTime reportDate;
|
final DateTime reportDate;
|
||||||
final String teacherName;
|
final String teacherName;
|
||||||
final int lessonNumber;
|
final int lessonNumber;
|
||||||
final Subject subject;
|
final Subject subject;
|
||||||
final String subjectName;
|
final String subjectName;
|
||||||
final String theme;
|
final String? theme;
|
||||||
final NameUidDesc method;
|
final NameUidDesc method;
|
||||||
final UidObj classGroup;
|
final UidObj classGroup;
|
||||||
|
|
||||||
Test({
|
const Test({
|
||||||
required this.uid,
|
required super.uid,
|
||||||
required this.date,
|
required this.date,
|
||||||
required this.reportDate,
|
required this.reportDate,
|
||||||
required this.teacherName,
|
required this.teacherName,
|
||||||
@@ -29,15 +29,15 @@ class Test {
|
|||||||
factory Test.fromJson(Map<String, dynamic> json) {
|
factory Test.fromJson(Map<String, dynamic> json) {
|
||||||
return Test(
|
return Test(
|
||||||
uid: json['Uid'],
|
uid: json['Uid'],
|
||||||
date: DateTime.parse(json['Datum']).toLocal(),
|
date: json.localDate('Datum')!,
|
||||||
reportDate: DateTime.parse(json['BejelentesDatuma']).toLocal(),
|
reportDate: json.localDate('BejelentesDatuma')!,
|
||||||
teacherName: json['RogzitoTanarNeve'],
|
teacherName: json['RogzitoTanarNeve'],
|
||||||
lessonNumber: json['OrarendiOraOraszama'],
|
lessonNumber: json['OrarendiOraOraszama'],
|
||||||
subject: Subject.fromJson(json['Tantargy']),
|
subject: Subject.fromJson(json['Tantargy']),
|
||||||
subjectName: json['TantargyNeve'],
|
subjectName: json['TantargyNeve'],
|
||||||
theme: json['Temaja'],
|
theme: json['Temaja'],
|
||||||
method: NameUidDesc.fromJson(json['Modja']),
|
method: json.nameUidDesc('Modja')!,
|
||||||
classGroup: UidObj.fromJson(json['OsztalyCsoport']),
|
classGroup: json.uid('OsztalyCsoport')!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
|
import '../extensions.dart';
|
||||||
import 'generic.dart';
|
import 'generic.dart';
|
||||||
import 'subject.dart';
|
import 'subject.dart';
|
||||||
|
|
||||||
class Lesson {
|
class Lesson extends NameUid {
|
||||||
final String uid;
|
|
||||||
final String date;
|
final String date;
|
||||||
final DateTime start;
|
final DateTime start;
|
||||||
final DateTime end;
|
final DateTime end;
|
||||||
final String name;
|
|
||||||
final int? lessonNumber;
|
final int? lessonNumber;
|
||||||
final int? lessonSeqNumber;
|
final int? lessonSeqNumber;
|
||||||
final NameUid? classGroup;
|
final NameUid? classGroup;
|
||||||
@@ -32,12 +31,12 @@ class Lesson {
|
|||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
final DateTime lastModifiedAt;
|
final DateTime lastModifiedAt;
|
||||||
|
|
||||||
Lesson({
|
const Lesson({
|
||||||
required this.uid,
|
required super.uid,
|
||||||
required this.date,
|
required this.date,
|
||||||
required this.start,
|
required this.start,
|
||||||
required this.end,
|
required this.end,
|
||||||
required this.name,
|
required super.name,
|
||||||
this.lessonNumber,
|
this.lessonNumber,
|
||||||
this.lessonSeqNumber,
|
this.lessonSeqNumber,
|
||||||
this.classGroup,
|
this.classGroup,
|
||||||
@@ -74,25 +73,21 @@ class Lesson {
|
|||||||
return Lesson(
|
return Lesson(
|
||||||
uid: json['Uid'],
|
uid: json['Uid'],
|
||||||
date: json['Datum'],
|
date: json['Datum'],
|
||||||
start: DateTime.parse(json['KezdetIdopont']).toLocal(),
|
start: json.localDate('KezdetIdopont')!,
|
||||||
end: DateTime.parse(json['VegIdopont']).toLocal(),
|
end: json.localDate('VegIdopont')!,
|
||||||
name: json['Nev'],
|
name: json['Nev'],
|
||||||
lessonNumber: json['Oraszam'],
|
lessonNumber: json['Oraszam'],
|
||||||
lessonSeqNumber: json['OraEvesSorszama'],
|
lessonSeqNumber: json['OraEvesSorszama'],
|
||||||
classGroup: json['OsztalyCsoport'] != null
|
classGroup: json.nameUid('OsztalyCsoport'),
|
||||||
? NameUid.fromJson(json['OsztalyCsoport'])
|
|
||||||
: null,
|
|
||||||
teacher: json['TanarNeve'],
|
teacher: json['TanarNeve'],
|
||||||
subject: json['Tantargy'] != null
|
subject: json['Tantargy'] != null
|
||||||
? Subject.fromJson(json['Tantargy'])
|
? Subject.fromJson(json['Tantargy'])
|
||||||
: null,
|
: null,
|
||||||
theme: json['Tema'],
|
theme: json['Tema'],
|
||||||
roomName: json['TeremNeve'],
|
roomName: json['TeremNeve'],
|
||||||
type: NameUidDesc.fromJson(json['Tipus']),
|
type: json.nameUidDesc('Tipus')!,
|
||||||
studentPresence: json['TanuloJelenlet'] != null
|
studentPresence: json.nameUidDesc('TanuloJelenlet'),
|
||||||
? NameUidDesc.fromJson(json['TanuloJelenlet'])
|
state: json.nameUidDesc('Allapot')!,
|
||||||
: null,
|
|
||||||
state: NameUidDesc.fromJson(json['Allapot']),
|
|
||||||
substituteTeacher: json['HelyettesTanarNeve'],
|
substituteTeacher: json['HelyettesTanarNeve'],
|
||||||
homeworkUid: json['HaziFeladatUid'],
|
homeworkUid: json['HaziFeladatUid'],
|
||||||
taskGroupUid: json['FeladatGroupUid'],
|
taskGroupUid: json['FeladatGroupUid'],
|
||||||
@@ -108,8 +103,8 @@ class Lesson {
|
|||||||
json['DigitalisTamogatoEszkozTipusList'] != null
|
json['DigitalisTamogatoEszkozTipusList'] != null
|
||||||
? List<String>.from(json['DigitalisTamogatoEszkozTipusList'])
|
? List<String>.from(json['DigitalisTamogatoEszkozTipusList'])
|
||||||
: List<String>.empty(),
|
: List<String>.empty(),
|
||||||
createdAt: DateTime.parse(json['Letrehozas']).toLocal(),
|
createdAt: json.localDate('Letrehozas')!,
|
||||||
lastModifiedAt: DateTime.parse(json['UtolsoModositas']).toLocal(),
|
lastModifiedAt: json.localDate('UtolsoModositas')!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user