183 Commits
dev ... dev

Author SHA1 Message Date
5a42884d20 Merge pull request 'Fixek + szöveg edit' (#31) from balint1414/firka:dev into dev
Reviewed-on: firka/firka#31
2026-06-11 18:13:02 +02:00
3fb0fd55de gitmodules: github>gitea 2026-06-11 16:00:36 +02:00
4c06daf02f Órarend szövegek szerkesztése 2026-06-11 15:52:50 +02:00
3d07b2b2ce Fordítás fix 2026-06-11 15:52:04 +02:00
9a45c5c456 Fix: Német órarend 2026-06-11 15:31:11 +02:00
checkedear
858d558cc3 feat: classroom name on lesson bottom sheet 2026-06-11 13:51:43 +02:00
checkedear
76f345cd6b fix: space under lesson slider 2026-06-11 13:29:55 +02:00
checkedear
e211d64ffb feat: shorten teacher's name 2026-06-11 13:29:20 +02:00
checkedear
c67ce7472a fix: event card on timetable 2026-06-11 12:25:13 +02:00
checkedear
2b9ac14d6e fix: active lesson on homepage 2026-06-11 11:50:12 +02:00
checkedear
eea10a4878 fix: lesson slider clipping 2026-06-11 11:27:13 +02:00
checkedear
1ef6ae6e87 fix: homework state update on homepage 2026-06-11 11:02:29 +02:00
checkedear
141431f378 fix: made whole homepage scrollable 2026-06-11 10:42:46 +02:00
checkedear
f0114e3976 fix: average calculations 2026-06-10 14:14:23 +02:00
checkedear
414a1ad254 feat: remove class average widget if there is no data 2026-06-09 13:21:41 +02:00
checkedear
d858c38bb1 fix: first lesson countdown 2026-06-09 12:49:52 +02:00
checkedear
87e3331428 revert: rounded subject average 2026-06-09 12:48:16 +02:00
checkedear
1d912abc7d tmp fix: themeless tests 2026-06-09 00:50:01 +02:00
3a2cdfce05 Update Jenkinsfile 2026-06-09 00:18:43 +02:00
c5ba318c3b Update Jenkinsfile 2026-06-09 00:15:02 +02:00
fe9a2b36f1 Update Jenkinsfile 2026-06-09 00:12:50 +02:00
80b05c1500 Update Jenkinsfile 2026-06-09 00:00:21 +02:00
2ed6a04bf5 Update Jenkinsfile 2026-06-08 23:40:57 +02:00
2d5ff89cad final shit 2026-06-08 23:13:11 +02:00
16b11564cc Update Jenkinsfile 2026-06-08 22:57:17 +02:00
1ca975f0bd Update Jenkinsfile 2026-06-08 22:50:44 +02:00
7985a11f99 Update Jenkinsfile 2026-06-08 22:32:02 +02:00
2cf95375cb jsdjsjsdjsdjsddjsdjsd 2026-06-08 22:24:34 +02:00
163b3a3135 i hate jenkins 2026-06-08 22:18:09 +02:00
e839b8fa70 Update Jenkinsfile 2026-06-08 22:12:48 +02:00
c6378c9d46 jsjsjdjjsjdj 2026-06-08 22:10:08 +02:00
51456b3b6e fuck you 2026-06-08 21:55:23 +02:00
checkedear
28f8f1d6f8 tmp fix: 50% weight grade style 2026-06-08 21:53:20 +02:00
d51c9d6411 Update Jenkinsfile 2026-06-08 21:52:12 +02:00
4ef2f479d6 Update Jenkinsfile 2026-06-08 21:42:18 +02:00
d81d261c52 Update Jenkinsfile 2026-06-08 21:35:15 +02:00
5baec2e65f Update Jenkinsfile 2026-06-08 21:33:52 +02:00
48ea97ecef Update Jenkinsfile 2026-06-08 21:33:02 +02:00
checkedear
1154997531 fix: gradle build 2026-06-08 20:55:55 +02:00
checkedear
dde05ce1db fix: navbar design 2026-06-08 20:40:57 +02:00
99ab6c07f7 update jenkins to that it only builds with developer keys for public access 2026-06-08 20:19:35 +02:00
checkedear
40c6b83627 fix: test bottom sheet design 2026-06-08 19:59:10 +02:00
checkedear
f4f6ddac72 fix: home_main 2026-06-08 19:57:46 +02:00
checkedear
938f2a56c6 fix: dismissed lesson design 2026-06-08 19:52:17 +02:00
checkedear
e1b571afa0 feat: test bottom sheet and design fixes 2026-06-04 12:20:43 +02:00
checkedear
729c281f1e ref: const constructors 2026-06-04 11:56:23 +02:00
checkedear
a61a41e677 ref: kreta_client 2026-05-17 19:40:49 +02:00
checkedear
e8f1f18725 feat: from, to params 2026-05-15 13:59:46 +02:00
checkedear
613b66bc22 ref: lesson widget 2026-04-29 21:21:36 +02:00
checkedear
1807d96895 fix: time left text for current lesson 2026-04-29 21:20:46 +02:00
checkedear
e06d2168c0 fix: display current lesson on homepage 2026-04-29 15:12:54 +02:00
checkedear
91603923bc fix: eplased lesson duration style pt. 2 2026-04-29 14:23:01 +02:00
checkedear
67e9ded2ed fix: dismiss modal by tapping on empty space 2026-04-27 17:08:27 +02:00
checkedear
e9aaa26e26 fix: currently viewed lesson indicator 2026-04-27 16:56:51 +02:00
checkedear
56f15a681c fix: timetable event overflow 2026-04-27 16:39:58 +02:00
checkedear
178ebf7271 fix: elapsed lesson duration style 2026-04-27 16:26:18 +02:00
checkedear
70c1397262 fix: subject grades overlap on switch 2026-04-27 16:20:18 +02:00
checkedear
a334901f84 fix: overall class average 2026-04-27 16:03:46 +02:00
checkedear
ddba6ad888 feat: show class grades 2026-04-27 15:49:31 +02:00
checkedear
780aaee1dd ref: grade utils 2026-04-26 23:53:51 +02:00
checkedear
09c408a6be feat: main page lessons carousel slider 2026-04-25 17:05:20 +02:00
checkedear
7847878551 feat: active and expanded lesson card 2026-04-25 17:04:32 +02:00
checkedear
09ef386eab fix: countdown widget design 2026-04-25 17:00:13 +02:00
checkedear
c19dd2f7eb feat: add borderColor to firka card 2026-04-25 16:58:33 +02:00
checkedear
39d4f49e99 fix: date format in test bottom sheet 2026-04-25 16:57:34 +02:00
checkedear
bbdbf406d4 fix: sun solid icon 2026-04-25 16:55:09 +02:00
checkedear
b17509f9db fix: timetable monthly view design 2026-04-21 22:41:48 +02:00
checkedear
9ec164daac ref: remove redundant files 2026-04-21 22:37:53 +02:00
checkedear
08f691d559 fix: subject card design 2026-04-21 22:36:30 +02:00
checkedear
ad9e2c6676 fix: grade subjects page design 2026-04-21 22:34:58 +02:00
checkedear
55dbf12a2f feat: main page with dates 2026-04-21 22:33:46 +02:00
checkedear
4fc13efc52 fix: bottom sheets 2026-04-19 16:30:26 +02:00
checkedear
5f16128bb0 feat: changable grade widget size 2026-04-19 15:53:56 +02:00
checkedear
4c8d73aa06 fix: B_16R font 2026-04-19 15:53:28 +02:00
checkedear
e4629d8489 fix: message content style and links 2026-04-19 15:52:33 +02:00
checkedear
f806dd8143 fix: test type alignment 2026-04-19 11:48:39 +02:00
checkedear
f653dc9fb4 fix: test type oveflow 2026-04-18 14:04:34 +02:00
checkedear
06249bfc3d fix: timetable style 2026-04-18 13:38:44 +02:00
checkedear
607f49bafc fix: test bubble style 2026-04-18 13:36:49 +02:00
checkedear
251e8de446 ref: lesson card 2026-04-18 13:36:09 +02:00
checkedear
21845f89a8 fix: firka_card margin and shadow 2026-04-18 13:32:19 +02:00
checkedear
25b1d06c06 fix: some fonts 2026-04-18 13:28:51 +02:00
checkedear
dd20aa6138 fix: get prev lesson 2026-04-15 22:17:45 +02:00
checkedear
9aa84c6fa1 fix: text grade title once again 2026-04-15 21:53:12 +02:00
checkedear
6c75acad66 fix: text grades title 2026-04-15 21:46:50 +02:00
checkedear
6311cafd51 fix: grade design 2026-04-15 18:57:03 +02:00
checkedear
1de32a35a4 fix: show grades by creation date 2026-04-15 16:29:02 +02:00
checkedear
f8a69a7561 feat: show tests on main home page 2026-04-15 16:26:35 +02:00
checkedear
f73c1127e7 feat: show notice board items as messages 2026-04-15 15:14:04 +02:00
checkedear
a369e4ab14 ref: kreta_api 2026-04-15 01:16:48 +02:00
checkedear
8b04b77327 ref: remove unused debug log 2026-04-15 01:00:37 +02:00
checkedear
d56db4fe3a fix: grade chart style 2026-04-14 14:58:18 +02:00
checkedear
e7c0a95638 fix: grade style 2026-04-13 15:30:32 +02:00
checkedear
fa66d9af14 ref: firka_card 2026-04-13 15:25:48 +02:00
checkedear
90c334d859 ref: getGradeColor and rounding accepts any num as grade 2026-04-13 15:23:34 +02:00
checkedear
cc9ebcf6b0 ref: kreta_api 2026-04-11 19:25:23 +02:00
checkedear
ac914aa02e Merge branch 'fix-long-message' into dev 2026-03-28 18:29:36 +01:00
checkedear
061f803f93 fix: long message overflow 2026-03-28 18:29:00 +01:00
checkedear
57c5e17b1c Merge branch 'fix-issue-29' into dev 2026-03-28 16:29:34 +01:00
checkedear
fa6f5390b1 Merge branch 'fix-issue-16' into dev 2026-03-28 16:27:18 +01:00
checkedear
21ff8f082f ref: firka_card 2026-03-28 13:23:51 +01:00
checkedear
3b09d072f0 feat: added 400% to the calculator 2026-03-27 20:15:01 +01:00
checkedear
84f8e75694 Merge branch 'fix-issue-16' into dev 2026-03-27 19:44:04 +01:00
checkedear
1a05cce49b fix: issue #16 2026-03-27 19:32:20 +01:00
checkedear
e732168a1b fix + ref: nav tap animation 2026-03-26 20:03:18 +01:00
zypherift
68035140b9 fix 2026-03-05 19:20:19 +01:00
zypherift
e004b7ee35 fix: higher res color wheel 2026-03-05 18:21:41 +01:00
zypherift
34718ed8ae Adjust webview loading fade timing 2026-03-05 18:19:37 +01:00
zypherift
03a5b5e767 feat: add disabled colors 2026-03-05 17:56:05 +01:00
zypherift
d30dc67626 push l10n 2026-03-05 17:55:50 +01:00
zypherift
d0a517ae1e feat: add privacy policy to domain browser, and add color change for url, and show url 2026-03-05 17:23:10 +01:00
32a6452c1b bump version string to 1.1.2 2026-03-05 14:52:55 +01:00
579bf6bcad firka(android): make room number chips have the same length
Closes: #13
2026-03-05 14:51:06 +01:00
4ecf0d1a3f firka_wear: fix RangeError in home_screen substring
Closes: #14
2026-03-05 13:29:02 +01:00
0675a5109a firka: ignore % grades and half year / end year grades
Closes: #22
2026-03-05 13:22:37 +01:00
bfb06d3e5f update l10n 2026-03-05 10:17:18 +01:00
71b4b6aec7 firka: ignore final and percent grades in averages 2026-03-05 10:15:55 +01:00
be6d28cfe3 firka: ignore subjects without grades in avg 2026-03-05 09:52:19 +01:00
8cfca13d1a firka: fix text style for hw bottom sheet
Closes: #19
2026-03-05 09:40:11 +01:00
632bb81408 firka: use textPrimaryLight for mark hw as done btn
Closes: #18
2026-03-05 09:33:47 +01:00
zypherift
f953dbd49f fix: null-safe percentage detection 2026-03-05 09:22:28 +01:00
zypherift
01e7e559ba fix: maybe fixed percentages being calculated into the grade? pls review 2026-03-05 09:22:28 +01:00
b6bfef7715 update docs 2026-03-05 09:06:26 +01:00
bf75f72bcd update codegen lock 2026-03-05 09:01:11 +01:00
zypherift
b0cb020d76 feat: smoother graph using 3 weighted avarages :O 2026-03-05 08:40:50 +01:00
zypherift
4239ffa00c chore: stream home data cache-first then refresh 2026-03-05 08:40:50 +01:00
zypherift
427b6f8086 fix: snap grade calculator weights to common percentages 2026-03-05 08:40:50 +01:00
zypherift
fbd2351073 fix: flashbang on new screen pushes 2026-03-05 08:40:50 +01:00
zypherift
150e90d19b edit: replace spinner with dave 2026-03-05 08:40:50 +01:00
zypherift
a6cf8b13c6 edit: fine tune wall collision vibration 2026-03-05 08:40:50 +01:00
zypherift
d8ae8471ab edit: l10n 2026-03-05 08:40:45 +01:00
zypherift
0e65f8e68c add: debug screen for acc 2026-03-04 23:02:25 +01:00
zypherift
3912ad593b edit: l10n 2026-03-04 23:02:25 +01:00
zypherift
1f6eaaeccc add: icons 2026-03-04 23:02:25 +01:00
40f188c2e2 codegen(firka): restore AndroidManifest.xml
flutter_launcher_icons replaces every icon inside
the AndroidManifest.xml which isn't ideal when having
multiple app icons
2026-03-04 21:34:27 +01:00
0aae3801b7 chore: update l10n for firka_wear 2026-03-04 21:17:17 +01:00
26902b7616 add build script for windows 2026-03-04 21:10:09 +01:00
ffaf2c77e0 bump version string to 1.1.1 2026-03-04 18:51:55 +01:00
23f7f7cd48 firka_wear: add error screen
Closes #12
2026-03-04 15:20:15 +01:00
c2879766eb build: run pub get and codegen 2026-03-04 10:55:40 +01:00
01cc08d5f3 codegen: generate missing .g.dart files 2026-03-04 09:56:52 +01:00
c386e1194b auto generate version code 2026-03-04 09:35:17 +01:00
67ed4e03eb firka: remov brotli again 2026-03-04 06:38:01 +01:00
c7d1f80e79 Merge pull request 'IOS Build fix' (#10) from devbeni/firka:dev into dev
Reviewed-on: firka/firka#10
2026-03-04 06:29:07 +01:00
7531e58114 Update project version to 1102 in Info.plist and project.pbxproj 2026-03-04 00:23:15 +01:00
c1e329cb5a Update subproject commit reference in l10n directory 2026-03-03 23:49:35 +01:00
dad52bf20e Merge branch 'dev' of https://git.firka.app/devbeni/firka into dev 2026-03-03 23:46:38 +01:00
5fb6d03d9c Remove Brotli subproject from Android app directory 2026-03-03 23:46:37 +01:00
1ef757d10f merge upstream 2026-03-03 23:46:10 +01:00
444abb83c2 Remove fmb_dart subproject from vendor directory 2026-03-03 23:44:24 +01:00
e835dcf6b1 firka: fix app icon changing
Closes: #9
2026-03-03 23:26:02 +01:00
e61a19fbbf Merge branch 'dev' of https://git.firka.app/devbeni/firka into dev 2026-03-03 23:23:30 +01:00
a937b854cd Update localization files and enhance build script for native assets handling 2026-03-03 23:22:43 +01:00
6d8f17ac00 Update object version and set development team for RunnerTests configuration 2026-03-03 23:11:25 +01:00
78dd4239cc merge upstream 2026-03-03 23:10:56 +01:00
39c5ca357e firka: remove package: 'firka'
Closes: #5
2026-03-03 23:08:45 +01:00
8249dbf03e remove dependency on brotli 2026-03-03 23:05:40 +01:00
c4e30ee4a6 Refactor project settings to improve folder exception handling and update object version 2026-03-03 22:31:16 +01:00
1291d20e55 Update app group identifiers to unify naming convention across the project 2026-03-03 22:14:04 +01:00
fb8d57c0ee Refactor entitlements and project settings to unify app group identifiers and update team identifiers 2026-03-03 22:11:16 +01:00
e79de0326c bump version codes for firka and firka_wear 2026-03-03 21:43:20 +01:00
de335af3c1 add build script 2026-03-03 20:29:56 +01:00
4be0bcd813 bump version to 1.1.0 and version code to 1100 2026-03-03 20:28:31 +01:00
zypherift
b3f46d8e84 Merge branch 'dev' of https://git.firka.app/firka/firka into dev 2026-03-03 20:17:38 +01:00
zypherift
e97246ee55 more vibration 2026-03-03 20:17:36 +01:00
zypherift
159fb73919 codegen 2026-03-03 20:17:01 +01:00
7c35a5675c Revert "accidentaly deleted"
This reverts commit 4c9eca217e.
2026-03-03 20:14:53 +01:00
45adddb7f7 add nix devshell 2026-03-03 20:11:10 +01:00
zypherift
4aad2bb292 l10n 2026-03-03 20:05:53 +01:00
zypherift
4c9eca217e accidentaly deleted 2026-03-03 20:05:46 +01:00
zypherift
ec6700e1cb Merge branch 'dev' of https://git.firka.app/firka/firka into dev 2026-03-03 20:00:42 +01:00
zypherift
b55595108f add assets to simulation 2026-03-03 20:00:24 +01:00
zypherift
8b3ab4a3a9 add colorwheel asset for new
loginscreen
2026-03-03 20:00:08 +01:00
zypherift
046b7926c4 nightmare: add simulation from scratch for collision and movement 2026-03-03 19:59:49 +01:00
zypherift
5626466107 disable drag bc login is fullscreen 2026-03-03 19:55:15 +01:00
zypherift
6c674bd596 finish webview 2026-03-03 19:53:34 +01:00
zypherift
9465a2b2a7 begin changing webview 2026-03-03 19:53:05 +01:00
484d8cf4cb codegen: add lock files 2026-03-03 18:40:59 +01:00
863f9c8077 firka_wear: add codegen script 2026-03-03 18:15:52 +01:00
a8983074dd firka: show weighted avg when adding ghost grades 2026-03-03 18:09:35 +01:00
e031c18ecb firka: fix Live Activity registration with fallback on resume and delayed retry 2026-03-03 17:37:39 +01:00
ba075c3b14 firka: add a section for ghost grades 2026-03-03 17:34:44 +01:00
32936c2aa5 firka: extract firka_common package with shared widgets (Isar kept separate)
- Create firka_common package with core helpers (debug, json, icon), theme,
  and shared widgets (FirkaCard, FirkaShadow, GradeWidget, GradeSmallCard,
  ClassIconWidget, FirkaIconWidget, DelayedSpinnerWidget, CounterDigitWidget)
- Keep Isar models (GenericCacheModel, TimetableCacheModel, HomeworkCacheModel,
  DatedCacheEntry, util) in firka and firka_wear - not moved to firka_common
- Update firka and firka_wear to depend on firka_common for shared UI only
- Add configurable roundGrade thresholds for firka settings
- Add package param to FirkaIconWidget for app asset paths
2026-03-03 15:33:11 +01:00
175 changed files with 7759 additions and 8654 deletions

7
.gitmodules vendored
View File

@@ -1,9 +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/android/app/src/main/java/org/brotli"]
path = firka/android/app/src/main/java/org/brotli
url = https://git.firka.app/firka/org_brotli
[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

View File

@@ -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
``` ```

View File

@@ -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
```

195
Jenkinsfile vendored
View File

@@ -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()
}
}
}

52
build.ps1 Normal file
View File

@@ -0,0 +1,52 @@
$ErrorActionPreference = 'Stop'
$ROOT = $PSScriptRoot
$SHA = (git -C $ROOT rev-parse --short HEAD)
$COMMIT_COUNT = [int](git -C $ROOT rev-list --count HEAD)
function Build-App {
param([string]$App)
$pubspec = Join-Path $ROOT $App "pubspec.yaml"
if (-not (Test-Path $pubspec)) {
Write-Error "Not found: $pubspec"
}
$versionLine = Get-Content $pubspec | Select-String -Pattern '^\s*version:\s*' | Select-Object -First 1
if (-not $versionLine) {
Write-Error "No version line in $pubspec"
}
$line = $versionLine.Line
if ($line -match '^\s*version:\s*([^+\s]+)') {
$baseVersion = $Matches[1].Trim()
} else {
Write-Error "Could not parse version from: $line"
}
$buildName = "${baseVersion}-${SHA}"
$versionCode = 2000 + $COMMIT_COUNT
if ($App -eq "firka_wear") {
$versionCode += 1
}
Write-Host "Building $App : version $buildName (version code: $versionCode)"
Push-Location (Join-Path $ROOT $App)
try {
flutter pub get
dart run scripts/codegen.dart
flutter build appbundle --build-name="$buildName" --build-number="$versionCode" --verbose
} finally {
Pop-Location
}
}
$target = if ($args.Count -gt 0) { $args[0] } else { "all" }
switch ($target) {
"firka" { Build-App firka }
"firka_wear" { Build-App firka_wear }
"all" { Build-App firka; Build-App firka_wear }
default {
Write-Error "Usage: $MyInvocation.MyCommand.Name [firka|firka_wear|all]"
}
}

41
build.sh Normal file
View File

@@ -0,0 +1,41 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")" && pwd)"
SHA=$(git -C "$ROOT" rev-parse --short HEAD)
COMMIT_COUNT=$(git -C "$ROOT" rev-list --count HEAD)
build_app() {
local APP="$1"
local PUBSPEC="$ROOT/$APP/pubspec.yaml"
if [[ ! -f "$PUBSPEC" ]]; then
echo "Not found: $PUBSPEC" >&2
return 1
fi
local VERSION_LINE BASE_VERSION BUILD_NAME VERSION_CODE
VERSION_LINE=$(grep -E '^\s*version:\s*' "$PUBSPEC" | head -1)
BASE_VERSION=$(echo "$VERSION_LINE" | sed -E 's/^[[:space:]]*version:[[:space:]]*([^+]+).*/\1/' | tr -d ' ')
BUILD_NAME="${BASE_VERSION}-${SHA}"
VERSION_CODE=$((2000 + COMMIT_COUNT))
[[ "$APP" == "firka_wear" ]] && VERSION_CODE=$((VERSION_CODE + 1))
echo "Building $APP: version $BUILD_NAME (version code: $VERSION_CODE)"
cd "$ROOT/$APP"
flutter pub get
dart run scripts/codegen.dart
flutter build appbundle --build-name="$BUILD_NAME" --build-number="$VERSION_CODE" --verbose
}
case "${1:-all}" in
firka) build_app firka ;;
firka_wear) build_app firka_wear ;;
all) build_app firka && build_app firka_wear ;;
*)
echo "Usage: $0 [firka|firka_wear|all]" >&2
exit 1
;;
esac

View File

@@ -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")
@@ -96,4 +98,4 @@ tasks.matching { it.name.startsWith("compileFlutterBuild") }.configureEach {
flutter { flutter {
source = "../.." source = "../.."
} }

View File

@@ -1,2 +1 @@
-keep class org.brotli.** { *; }
-keep class app.firka.naplo.glance.** { *; } -keep class app.firka.naplo.glance.** { *; }

View File

@@ -11,7 +11,6 @@
android:name=".AppMain" android:name=".AppMain"
android:icon="@mipmap/launcher_icon"> android:icon="@mipmap/launcher_icon">
<service <service
android:name=".WearSyncForegroundService" android:name=".WearSyncForegroundService"
android:exported="false" android:exported="false"

View File

@@ -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),
) )
} }
} }

View File

@@ -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))
} }
} }

View File

@@ -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"
} }
} }

View File

@@ -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

View File

@@ -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

View File

@@ -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.

After

Width:  |  Height:  |  Size: 3.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,6 @@
<svg width="108" height="48" viewBox="0 0 108 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="108" height="48" rx="12" fill="#F3FBDE"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M30.6555 15.6657C30.968 15.3533 31.3919 15.1777 31.8338 15.1777C32.2758 15.1777 32.6996 15.3533 33.0122 15.6657L34.3338 16.9874C34.6463 17.2999 34.8218 17.7238 34.8218 18.1657C34.8218 18.6077 34.6463 19.0315 34.3338 19.344L33.0122 20.6657L29.3338 16.9874L30.6555 15.6657ZM28.1555 18.1657L23.9888 22.3324C23.6762 22.6449 23.5006 23.0687 23.5005 23.5107V24.8324C23.5005 25.2744 23.6761 25.6983 23.9886 26.0109C24.3012 26.3235 24.7251 26.499 25.1672 26.499H26.4888C26.9308 26.499 27.3547 26.3233 27.6672 26.0107L31.8338 21.844L28.1555 18.1657Z" fill="#A7DC22"/>
<path d="M21.0005 25.666H20.1672C19.7251 25.666 19.3012 25.8416 18.9886 26.1542C18.6761 26.4667 18.5005 26.8907 18.5005 27.3327C18.5005 27.7747 18.6761 28.1986 18.9886 28.5112C19.3012 28.8238 19.7251 28.9993 20.1672 28.9993H31.8338C32.2758 28.9993 32.6998 29.1749 33.0123 29.4875C33.3249 29.8001 33.5005 30.224 33.5005 30.666C33.5005 31.108 33.3249 31.532 33.0123 31.8445C32.6998 32.1571 32.2758 32.3327 31.8338 32.3327H28.5005" stroke="#A7DC22" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M47.648 29.5V19.996H44.208V18.3H52.944V19.996H49.504V29.5H47.648ZM56.7006 29.692C55.922 29.692 55.2286 29.516 54.6206 29.164C54.0233 28.8013 53.5486 28.3053 53.1966 27.676C52.8553 27.0467 52.6846 26.3213 52.6846 25.5C52.6846 24.6787 52.8606 23.9533 53.2126 23.324C53.5646 22.6947 54.0446 22.204 54.6526 21.852C55.2713 21.4893 55.9753 21.308 56.7646 21.308C57.4793 21.308 58.1246 21.4947 58.7006 21.868C59.2766 22.2307 59.73 22.7587 60.0606 23.452C60.402 24.1453 60.5726 24.972 60.5726 25.932H54.2846L54.5246 25.708C54.5246 26.1987 54.6313 26.6253 54.8446 26.988C55.058 27.34 55.3406 27.612 55.6926 27.804C56.0446 27.996 56.434 28.092 56.8606 28.092C57.3513 28.092 57.7566 27.9853 58.0766 27.772C58.3966 27.548 58.6473 27.26 58.8286 26.908L60.4126 27.58C60.1886 28.0067 59.9006 28.38 59.5486 28.7C59.2073 29.02 58.7966 29.2653 58.3166 29.436C57.8473 29.6067 57.3086 29.692 56.7006 29.692ZM54.6366 24.78L54.3806 24.556H58.8926L58.6526 24.78C58.6526 24.3427 58.5566 23.9853 58.3646 23.708C58.1726 23.42 57.9273 23.2067 57.6286 23.068C57.3406 22.9187 57.0366 22.844 56.7166 22.844C56.3966 22.844 56.0766 22.9187 55.7566 23.068C55.4366 23.2067 55.17 23.42 54.9566 23.708C54.7433 23.9853 54.6366 24.3427 54.6366 24.78ZM55.5326 20.46L57.3886 18.3H59.4686L57.4046 20.46H55.5326ZM61.9764 29.5V21.5H63.6564L63.7364 22.572C63.9817 22.156 64.2964 21.8413 64.6804 21.628C65.0644 21.4147 65.5017 21.308 65.9924 21.308C66.6324 21.308 67.1764 21.452 67.6244 21.74C68.0724 22.028 68.3977 22.4653 68.6004 23.052C68.835 22.4867 69.1657 22.0547 69.5924 21.756C70.019 21.4573 70.5204 21.308 71.0964 21.308C72.0244 21.308 72.739 21.6067 73.2404 22.204C73.7417 22.7907 73.987 23.6973 73.9764 24.924V29.5H72.2004V25.404C72.2004 24.764 72.131 24.2733 71.9924 23.932C71.8537 23.58 71.667 23.3347 71.4324 23.196C71.1977 23.0573 70.9257 22.988 70.6164 22.988C70.0617 22.9773 69.6297 23.1747 69.3204 23.58C69.0217 23.9853 68.8724 24.5667 68.8724 25.324V29.5H67.0804V25.404C67.0804 24.764 67.011 24.2733 66.8724 23.932C66.7444 23.58 66.563 23.3347 66.3284 23.196C66.0937 23.0573 65.8217 22.988 65.5124 22.988C64.9577 22.9773 64.5257 23.1747 64.2164 23.58C63.9177 23.9853 63.7684 24.5667 63.7684 25.324V29.5H61.9764ZM80.6845 29.5L80.6045 27.996V25.388C80.6045 24.844 80.5458 24.3907 80.4285 24.028C80.3218 23.6547 80.1405 23.372 79.8845 23.18C79.6392 22.9773 79.3085 22.876 78.8925 22.876C78.5085 22.876 78.1725 22.956 77.8845 23.116C77.5965 23.276 77.3512 23.5267 77.1485 23.868L75.5805 23.292C75.7512 22.94 75.9752 22.6147 76.2525 22.316C76.5405 22.0067 76.8978 21.7613 77.3245 21.58C77.7618 21.3987 78.2845 21.308 78.8925 21.308C79.6712 21.308 80.3218 21.4627 80.8445 21.772C81.3672 22.0707 81.7512 22.5027 81.9965 23.068C82.2525 23.6333 82.3805 24.316 82.3805 25.116L82.3325 29.5H80.6845ZM78.3805 29.692C77.4205 29.692 76.6738 29.4787 76.1405 29.052C75.6178 28.6253 75.3565 28.0227 75.3565 27.244C75.3565 26.412 75.6338 25.7773 76.1885 25.34C76.7538 24.9027 77.5378 24.684 78.5405 24.684H80.6845V26.06H79.1165C78.4018 26.06 77.9005 26.1613 77.6125 26.364C77.3245 26.556 77.1805 26.8333 77.1805 27.196C77.1805 27.5053 77.3032 27.7507 77.5485 27.932C77.8045 28.1027 78.1565 28.188 78.6045 28.188C79.0098 28.188 79.3618 28.0973 79.6605 27.916C79.9592 27.7347 80.1885 27.4947 80.3485 27.196C80.5192 26.8973 80.6045 26.5613 80.6045 26.188H81.1325C81.1325 27.276 80.9138 28.1347 80.4765 28.764C80.0392 29.3827 79.3405 29.692 78.3805 29.692ZM77.5965 20.46L79.4525 18.3H81.5325L79.4685 20.46H77.5965ZM85.829 27.26L84.741 26.028L88.901 21.5H91.061L85.829 27.26ZM84.117 29.5V18.3H85.909V29.5H84.117ZM89.205 29.5L86.389 25.356L87.557 24.108L91.333 29.5H89.205Z" fill="#394C0A"/>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,5 @@
<svg width="108" height="48" viewBox="0 0 108 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="108" height="48" rx="12" fill="#F3FBDE"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.8026 15.9055C26.7524 15.8382 26.6891 15.7817 26.6166 15.7394C26.544 15.6971 26.4637 15.6699 26.3804 15.6593C26.2971 15.6487 26.2125 15.655 26.1317 15.6778C26.0509 15.7007 25.9755 15.7396 25.9101 15.7922C24.3129 17.0742 23.2594 18.9127 22.9609 20.9388C22.4141 20.5413 21.9344 20.0589 21.5401 19.5097C21.4866 19.4349 21.4173 19.3729 21.3371 19.328C21.2569 19.2831 21.1678 19.2564 21.0761 19.2499C20.9845 19.2434 20.8925 19.2572 20.8067 19.2902C20.721 19.3233 20.6436 19.3749 20.5801 19.4413C19.4795 20.5926 18.774 22.0644 18.5658 23.6435C18.3576 25.2225 18.6575 26.8268 19.4221 28.2241C20.1866 29.6213 21.3761 30.7388 22.8182 31.4148C24.2604 32.0908 25.8803 32.2902 27.4433 31.984C29.0063 31.6778 30.4312 30.882 31.5117 29.7118C32.5922 28.5416 33.2721 27.0579 33.453 25.4755C33.6338 23.893 33.3062 22.2941 32.5176 20.9104C31.729 19.5266 30.5204 18.4298 29.0667 17.7788C28.1732 17.3443 27.3967 16.7019 26.8026 15.9055ZM29.1251 25.8755C29.1248 26.3275 29.0264 26.7741 28.8367 27.1844C28.6471 27.5948 28.3707 27.959 28.0266 28.2522C27.6825 28.5453 27.2789 28.7603 26.8437 28.8823C26.4084 29.0044 25.9519 29.0305 25.5056 28.959C25.0592 28.8875 24.6337 28.7199 24.2584 28.468C23.8831 28.2161 23.5669 27.8857 23.3317 27.4998C23.0964 27.1138 22.9477 26.6814 22.8958 26.2323C22.8438 25.7833 22.8899 25.3283 23.0309 24.8988C23.5542 25.2863 24.1559 25.5738 24.8084 25.7322C24.986 24.5915 25.5528 23.5475 26.4126 22.7772C27.1634 22.8772 27.8524 23.2465 28.3513 23.8164C28.8503 24.3863 29.1252 25.118 29.1251 25.8755Z" fill="#A7DC22"/>
<path d="M49.008 29.692C48.4213 29.692 47.8827 29.6227 47.392 29.484C46.912 29.3453 46.4853 29.1533 46.112 28.908C45.7493 28.6627 45.4453 28.3907 45.2 28.092C44.9653 27.7827 44.8 27.4627 44.704 27.132L46.528 26.572C46.6667 26.9667 46.9387 27.3133 47.344 27.612C47.7493 27.9107 48.2507 28.0653 48.848 28.076C49.5413 28.076 50.0907 27.932 50.496 27.644C50.9013 27.356 51.104 26.9773 51.104 26.508C51.104 26.0813 50.9333 25.7347 50.592 25.468C50.2507 25.1907 49.792 24.9773 49.216 24.828L47.84 24.476C47.3173 24.3373 46.8427 24.1347 46.416 23.868C46 23.6013 45.6693 23.2653 45.424 22.86C45.1893 22.4547 45.072 21.9747 45.072 21.42C45.072 20.3747 45.4133 19.564 46.096 18.988C46.7787 18.4013 47.7547 18.108 49.024 18.108C49.7387 18.108 50.3627 18.22 50.896 18.444C51.44 18.6573 51.888 18.956 52.24 19.34C52.592 19.7133 52.8533 20.14 53.024 20.62L51.232 21.196C51.072 20.7693 50.7947 20.4173 50.4 20.14C50.0053 19.8627 49.5147 19.724 48.928 19.724C48.32 19.724 47.84 19.868 47.488 20.156C47.1467 20.444 46.976 20.844 46.976 21.356C46.976 21.772 47.1093 22.0973 47.376 22.332C47.6533 22.556 48.0267 22.7267 48.496 22.844L49.872 23.18C50.8747 23.4253 51.6533 23.8467 52.208 24.444C52.7627 25.0413 53.04 25.7027 53.04 26.428C53.04 27.068 52.8853 27.6333 52.576 28.124C52.2667 28.6147 51.808 28.9987 51.2 29.276C50.6027 29.5533 49.872 29.692 49.008 29.692ZM57.9416 29.692C57.099 29.692 56.4536 29.484 56.0056 29.068C55.5683 28.6413 55.3496 28.0333 55.3496 27.244V19.004H57.1256V26.908C57.1256 27.2813 57.211 27.564 57.3816 27.756C57.563 27.948 57.8243 28.044 58.1656 28.044C58.2723 28.044 58.3896 28.0227 58.5176 27.98C58.6456 27.9373 58.7896 27.8573 58.9496 27.74L59.6056 29.1C59.3283 29.292 59.051 29.436 58.7736 29.532C58.4963 29.6387 58.219 29.692 57.9416 29.692ZM54.0216 23.036V21.5H59.2856V23.036H54.0216ZM62.2229 25.244C62.2229 24.38 62.3882 23.6707 62.7189 23.116C63.0495 22.5613 63.4762 22.1507 63.9989 21.884C64.5322 21.6067 65.0869 21.468 65.6629 21.468V23.18C65.1722 23.18 64.7082 23.2493 64.2709 23.388C63.8442 23.516 63.4975 23.7293 63.2309 24.028C62.9642 24.3267 62.8309 24.7213 62.8309 25.212L62.2229 25.244ZM61.0389 29.5V21.5H62.8309V29.5H61.0389ZM70.4194 29.692C69.6407 29.692 68.9474 29.516 68.3394 29.164C67.742 28.8013 67.2674 28.3053 66.9154 27.676C66.574 27.0467 66.4034 26.3213 66.4034 25.5C66.4034 24.6787 66.5794 23.9533 66.9314 23.324C67.2834 22.6947 67.7634 22.204 68.3714 21.852C68.99 21.4893 69.694 21.308 70.4834 21.308C71.198 21.308 71.8434 21.4947 72.4194 21.868C72.9954 22.2307 73.4487 22.7587 73.7794 23.452C74.1207 24.1453 74.2914 24.972 74.2914 25.932H68.0034L68.2434 25.708C68.2434 26.1987 68.35 26.6253 68.5634 26.988C68.7767 27.34 69.0594 27.612 69.4114 27.804C69.7634 27.996 70.1527 28.092 70.5794 28.092C71.07 28.092 71.4754 27.9853 71.7954 27.772C72.1154 27.548 72.366 27.26 72.5474 26.908L74.1314 27.58C73.9074 28.0067 73.6194 28.38 73.2674 28.7C72.926 29.02 72.5154 29.2653 72.0354 29.436C71.566 29.6067 71.0274 29.692 70.4194 29.692ZM68.3554 24.78L68.0994 24.556H72.6114L72.3714 24.78C72.3714 24.3427 72.2754 23.9853 72.0834 23.708C71.8914 23.42 71.646 23.2067 71.3474 23.068C71.0594 22.9187 70.7554 22.844 70.4354 22.844C70.1154 22.844 69.7954 22.9187 69.4754 23.068C69.1554 23.2067 68.8887 23.42 68.6754 23.708C68.462 23.9853 68.3554 24.3427 68.3554 24.78ZM80.5439 29.5L80.4639 27.996V25.388C80.4639 24.844 80.4052 24.3907 80.2879 24.028C80.1812 23.6547 79.9999 23.372 79.7439 23.18C79.4985 22.9773 79.1679 22.876 78.7519 22.876C78.3679 22.876 78.0319 22.956 77.7439 23.116C77.4559 23.276 77.2105 23.5267 77.0079 23.868L75.4399 23.292C75.6105 22.94 75.8345 22.6147 76.1119 22.316C76.3999 22.0067 76.7572 21.7613 77.1839 21.58C77.6212 21.3987 78.1439 21.308 78.7519 21.308C79.5305 21.308 80.1812 21.4627 80.7039 21.772C81.2265 22.0707 81.6105 22.5027 81.8559 23.068C82.1119 23.6333 82.2399 24.316 82.2399 25.116L82.1919 29.5H80.5439ZM78.2399 29.692C77.2799 29.692 76.5332 29.4787 75.9999 29.052C75.4772 28.6253 75.2159 28.0227 75.2159 27.244C75.2159 26.412 75.4932 25.7773 76.0479 25.34C76.6132 24.9027 77.3972 24.684 78.3999 24.684H80.5439V26.06H78.9759C78.2612 26.06 77.7599 26.1613 77.4719 26.364C77.1839 26.556 77.0399 26.8333 77.0399 27.196C77.0399 27.5053 77.1625 27.7507 77.4079 27.932C77.6639 28.1027 78.0159 28.188 78.4639 28.188C78.8692 28.188 79.2212 28.0973 79.5199 27.916C79.8185 27.7347 80.0479 27.4947 80.2079 27.196C80.3785 26.8973 80.4639 26.5613 80.4639 26.188H80.9919C80.9919 27.276 80.7732 28.1347 80.3359 28.764C79.8985 29.3827 79.1999 29.692 78.2399 29.692ZM85.7665 27.26L84.6785 26.028L88.8385 21.5H90.9985L85.7665 27.26ZM84.0545 29.5V18.3H85.8465V29.5H84.0545ZM89.1425 29.5L86.3265 25.356L87.4945 24.108L91.2705 29.5H89.1425Z" fill="#394C0A"/>
</svg>

After

Width:  |  Height:  |  Size: 6.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -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

21
firka/codegen-lock.yaml Normal file
View File

@@ -0,0 +1,21 @@
icons:
"flutter_launcher_icons.yaml": "c600507ca0df7cebd0f708124842512a14ed3d597b779176200d6ba25b1335b1"
"pubspec.yaml": "a6ae0bd67a2b6226bec2dfd55437b3db2b7ca4a03f315c1a0a8c2f4b505c7c87"
"assets/images/logos/colored_logo.webp": "4b4fa99d144fe6694aa4487ba1b26aeecafae41e3c877836cd7da28d61a77983"
"assets/images/logos/monochrome_logo.png": "188d2b0a64c70323b09bcee721663d6698fb557066f20ddaec97bba6869c1c6c"
"assets/images/logos/colored_logo_without_mustache.png": "d11cff9f38985885873bfdd2d84e61f8fab03803eada94d4caac1545ef3685f3"
"assets/images/logos/colored_logo_only_mustache.png": "bad6220c11bdfb1dfe04e5173bd2ebedd3999689d4b3a68fc63dc520c96dd33b"
l10n:
"l10n.yml": "a57bc304cac4a2b0235593586f17f400a5165d67fc9aadeaa11893cfa36ee082"
"lib/l10n/app_de.arb": "ecfbf13bd33be9d27a2b54bfd8fb61e46c1a1dce905869d3f30cd05b4aecf258"
"lib/l10n/app_en.arb": "7c43928f67b5b735283da04e3741f1afa2e9d41cdeb2e91c740e77fc84e7f046"
"lib/l10n/app_hu.arb": "696a1ea2e86be364e9c815e5f739d3a2f5f3da9c05066084d9d26defe5018e2c"
isar:
"lib/data/models/app_settings_model.dart": "5eb5af345f1347f104257f0999763650fe2673f9da1754bd12d3f756fe5c9723"
"lib/data/models/generic_cache_model.dart": "79151d0467fb5d40c532eaaa08ad7c7e24a34304199280fbf49cf6e5adcce6bc"
"lib/data/models/homework_cache_model.dart": "45789970b27d5790cdc54c292ef2f5feaa5f4e293b8a8862fd676d5eb3e25d29"
"lib/data/models/timetable_cache_model.dart": "b972bf51e399f8d20d4f9ad660082d4cc4a9798df9ac9d6ec9ef6ac640205572"
"lib/data/models/token_model.dart": "8c957cd07e473827d78fd8fd4fb6c1336b636a69c25c93618e1e7f94b7cf0683"
splash:
"flutter_native_splash.yaml": "0fd4a85d6f950d97298e99916927649940ffcfdadfc136ceee126fed0dbaa9f2"
"assets/images/logos/splash.png": "88fbebc3d686cb9095bcce362029b69978b1b14270e465e91d962b1425db1152"

View File

@@ -2,15 +2,15 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
<string>$(TeamIdentifierPrefix)app.firka.firka</string>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>group.app.firka.firkaa</string> <string>group.app.firka.firka</string>
</array> </array>
<key>keychain-access-groups</key> <key>keychain-access-groups</key>
<array> <array>
<string>$(AppIdentifierPrefix)app.firka.shared</string> <string>$(AppIdentifierPrefix)app.firka.shared</string>
</array> </array>
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
<string>$(TeamIdentifierPrefix)app.firka.firkaa</string>
</dict> </dict>
</plist> </plist>

View File

@@ -31,7 +31,7 @@ class WatchL10n {
private let languageKey = "watch_language" private let languageKey = "watch_language"
private let syncWithiPhoneKey = "watch_sync_language_with_iphone" private let syncWithiPhoneKey = "watch_sync_language_with_iphone"
private let lastAppliedSharedLanguageVersionKey = "watch_last_applied_shared_language_version" private let lastAppliedSharedLanguageVersionKey = "watch_last_applied_shared_language_version"
private static let appGroupID = "group.app.firka.firkaa" private static let appGroupID = "group.app.firka.firka"
private var appGroupDefaults: UserDefaults? { private var appGroupDefaults: UserDefaults? {
UserDefaults(suiteName: Self.appGroupID) UserDefaults(suiteName: Self.appGroupID)
} }

View File

@@ -30,7 +30,7 @@ class DataStore {
(error == "token_expired" || error == "no_token") && recoveryAttempted && !isRecoveringToken (error == "token_expired" || error == "no_token") && recoveryAttempted && !isRecoveringToken
} }
private let appGroupID = "group.app.firka.firkaa" private let appGroupID = "group.app.firka.firka"
private let cacheFileName = "watch_data.json" private let cacheFileName = "watch_data.json"
private let lastHandledSessionStateVersionKey = "firka.watch.last_handled_session_state_version" private let lastHandledSessionStateVersionKey = "firka.watch.last_handled_session_state_version"
private let lastHandledSessionActiveStudentIdNormKey = "firka.watch.last_handled_session_active_student_id_norm" private let lastHandledSessionActiveStudentIdNormKey = "firka.watch.last_handled_session_active_student_id_norm"

View File

@@ -5,7 +5,7 @@ import SwiftUI
// MARK: - Complication Localization Helper // MARK: - Complication Localization Helper
private struct ComplicationL10n { private struct ComplicationL10n {
private static let appGroupID = "group.app.firka.firkaa" private static let appGroupID = "group.app.firka.firka"
enum Language: String { enum Language: String {
case hungarian = "hu" case hungarian = "hu"
@@ -63,7 +63,7 @@ private struct ComplicationL10n {
// MARK: - Watch Cache Loader // MARK: - Watch Cache Loader
private struct WatchCacheLoader { private struct WatchCacheLoader {
private static let appGroupID = "group.app.firka.firkaa" private static let appGroupID = "group.app.firka.firka"
private static let cacheFileName = "watch_data.json" private static let cacheFileName = "watch_data.json"
static func loadWidgetData() -> WidgetData? { static func loadWidgetData() -> WidgetData? {

View File

@@ -2,11 +2,11 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
<string>$(TeamIdentifierPrefix)app.firka.firka</string>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>group.app.firka.firkaa</string> <string>group.app.firka.firka</string>
</array> </array>
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
<string>$(TeamIdentifierPrefix)app.firka.firkaa</string>
</dict> </dict>
</plist> </plist>

View File

@@ -2,11 +2,11 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.app.firka.firkaa</string>
</array>
<key>com.apple.developer.ubiquity-kvstore-identifier</key> <key>com.apple.developer.ubiquity-kvstore-identifier</key>
<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string> <string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>
<key>com.apple.security.application-groups</key>
<array>
<string>group.app.firka.firka</string>
</array>
</dict> </dict>
</plist> </plist>

View File

@@ -2,7 +2,7 @@ import WidgetKit
import SwiftUI import SwiftUI
import AppIntents import AppIntents
private let appGroup = "group.app.firka.firkaa" private let appGroup = "group.app.firka.firka"
// MARK: - Navigation Intents (iOS 16+, used by Controls and Shortcuts) // MARK: - Navigation Intents (iOS 16+, used by Controls and Shortcuts)
@@ -46,7 +46,7 @@ struct OpenTimetableIntent: AppIntent {
@available(iOS 18.0, *) @available(iOS 18.0, *)
struct HomeControl: ControlWidget { struct HomeControl: ControlWidget {
static let kind = "app.firka.firkaa.control.home" static let kind = "app.firka.firka.control.home"
var body: some ControlWidgetConfiguration { var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: Self.kind) { StaticControlConfiguration(kind: Self.kind) {
@@ -63,7 +63,7 @@ struct HomeControl: ControlWidget {
@available(iOS 18.0, *) @available(iOS 18.0, *)
struct GradesControl: ControlWidget { struct GradesControl: ControlWidget {
static let kind = "app.firka.firkaa.control.grades" static let kind = "app.firka.firka.control.grades"
var body: some ControlWidgetConfiguration { var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: Self.kind) { StaticControlConfiguration(kind: Self.kind) {
@@ -80,7 +80,7 @@ struct GradesControl: ControlWidget {
@available(iOS 18.0, *) @available(iOS 18.0, *)
struct TimetableControl: ControlWidget { struct TimetableControl: ControlWidget {
static let kind = "app.firka.firkaa.control.timetable" static let kind = "app.firka.firka.control.timetable"
var body: some ControlWidgetConfiguration { var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: Self.kind) { StaticControlConfiguration(kind: Self.kind) {

View File

@@ -4,11 +4,11 @@
<dict> <dict>
<key>aps-environment</key> <key>aps-environment</key>
<string>development</string> <string>development</string>
<key>com.apple.security.application-groups</key>
<array>
<string>group.app.firka.firkaa</string>
</array>
<key>com.apple.developer.ubiquity-kvstore-identifier</key> <key>com.apple.developer.ubiquity-kvstore-identifier</key>
<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string> <string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>
<key>com.apple.security.application-groups</key>
<array>
<string>group.app.firka.firka</string>
</array>
</dict> </dict>
</plist> </plist>

View File

@@ -3,7 +3,7 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 70; objectVersion = 54;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
@@ -222,35 +222,35 @@
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
4F0EA0512F2BD2A2003CC89E /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { 4F0EA0512F2BD2A2003CC89E /* Exceptions for "HomeWidgetsExtension" folder in "Runner" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet; isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = ( membershipExceptions = (
Controls/AppControls.swift, Controls/AppControls.swift,
); );
target = 97C146ED1CF9000F007C117D /* Runner */; target = 97C146ED1CF9000F007C117D /* Runner */;
}; };
4F4E70D02EF565FF00C90AD1 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { 4F4E70D02EF565FF00C90AD1 /* Exceptions for "LiveActivityWidget" folder in "Runner" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet; isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = ( membershipExceptions = (
ActivityAttributes.swift, ActivityAttributes.swift,
); );
target = 97C146ED1CF9000F007C117D /* Runner */; target = 97C146ED1CF9000F007C117D /* Runner */;
}; };
4F5966082F2F0EB100A3DB03 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { 4F5966082F2F0EB100A3DB03 /* Exceptions for "FirkaWatchComplications" folder in "FirkaWatchComplicationsExtension" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet; isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = ( membershipExceptions = (
Info.plist, Info.plist,
); );
target = 4F5965FC2F2F0EAF00A3DB03 /* FirkaWatchComplicationsExtension */; target = 4F5965FC2F2F0EAF00A3DB03 /* FirkaWatchComplicationsExtension */;
}; };
4F6C1D3E2ECD3FBD00F819D7 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { 4F6C1D3E2ECD3FBD00F819D7 /* Exceptions for "LiveActivityWidget" folder in "LiveActivityWidget" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet; isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = ( membershipExceptions = (
Info.plist, Info.plist,
); );
target = 4F30C7642E8FBF9D008BB46C /* LiveActivityWidget */; target = 4F30C7642E8FBF9D008BB46C /* LiveActivityWidget */;
}; };
4FE64E472F27B07B006F9205 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { 4FE64E472F27B07B006F9205 /* Exceptions for "HomeWidgetsExtension" folder in "HomeWidgetsExtension" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet; isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = ( membershipExceptions = (
Info.plist, Info.plist,
@@ -260,10 +260,55 @@
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFileSystemSynchronizedRootGroup section */
4F30C76A2E8FBF9D008BB46C /* LiveActivityWidget */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (4F4E70D02EF565FF00C90AD1 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 4F6C1D3E2ECD3FBD00F819D7 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = LiveActivityWidget; sourceTree = "<group>"; }; 4F30C76A2E8FBF9D008BB46C /* LiveActivityWidget */ = {
4F5966002F2F0EAF00A3DB03 /* FirkaWatchComplications */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (4F5966082F2F0EB100A3DB03 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = FirkaWatchComplications; sourceTree = "<group>"; }; isa = PBXFileSystemSynchronizedRootGroup;
4FE64E362F27B07A006F9205 /* HomeWidgetsExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (4F0EA0512F2BD2A2003CC89E /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 4FE64E472F27B07B006F9205 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = HomeWidgetsExtension; sourceTree = "<group>"; }; exceptions = (
4FF81B7B2F2EB4C100E95BA0 /* FirkaWatch Watch App */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = "FirkaWatch Watch App"; sourceTree = "<group>"; }; 4F4E70D02EF565FF00C90AD1 /* Exceptions for "LiveActivityWidget" folder in "Runner" target */,
4F6C1D3E2ECD3FBD00F819D7 /* Exceptions for "LiveActivityWidget" folder in "LiveActivityWidget" target */,
);
explicitFileTypes = {
};
explicitFolders = (
);
path = LiveActivityWidget;
sourceTree = "<group>";
};
4F5966002F2F0EAF00A3DB03 /* FirkaWatchComplications */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
4F5966082F2F0EB100A3DB03 /* Exceptions for "FirkaWatchComplications" folder in "FirkaWatchComplicationsExtension" target */,
);
explicitFileTypes = {
};
explicitFolders = (
);
path = FirkaWatchComplications;
sourceTree = "<group>";
};
4FE64E362F27B07A006F9205 /* HomeWidgetsExtension */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
4F0EA0512F2BD2A2003CC89E /* Exceptions for "HomeWidgetsExtension" folder in "Runner" target */,
4FE64E472F27B07B006F9205 /* Exceptions for "HomeWidgetsExtension" folder in "HomeWidgetsExtension" target */,
);
explicitFileTypes = {
};
explicitFolders = (
);
path = HomeWidgetsExtension;
sourceTree = "<group>";
};
4FF81B7B2F2EB4C100E95BA0 /* FirkaWatch Watch App */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
explicitFileTypes = {
};
explicitFolders = (
);
path = "FirkaWatch Watch App";
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */ /* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -711,14 +756,10 @@
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
); );
inputPaths = (
);
name = "[CP] Copy Pods Resources"; name = "[CP] Copy Pods Resources";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
); );
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@@ -775,7 +816,7 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; shellScript = "set -e\nNATIVE_ASSETS_DIR=\"$FLUTTER_APPLICATION_PATH/$FLUTTER_BUILD_DIR/native_assets\"\ncase \"$FLUTTER_BUILD_DIR\" in\n /*) NATIVE_ASSETS_DIR=\"$FLUTTER_BUILD_DIR/native_assets\" ;;\nesac\nif [ -d \"$FLUTTER_APPLICATION_PATH/.dart_tool/hooks_runner\" ]; then\n find \"$FLUTTER_APPLICATION_PATH/.dart_tool/hooks_runner\" -exec xattr -c {} \\; 2>/dev/null || true\nfi\nif [ -d \"$NATIVE_ASSETS_DIR\" ]; then\n find \"$NATIVE_ASSETS_DIR\" -exec xattr -c {} \\; 2>/dev/null || true\nfi\n/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n";
}; };
D576F90540C8E625A9A12317 /* [CP] Check Pods Manifest.lock */ = { D576F90540C8E625A9A12317 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
@@ -807,14 +848,10 @@
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
); );
inputPaths = (
);
name = "[CP] Embed Pods Frameworks"; name = "[CP] Embed Pods Frameworks";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
); );
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
@@ -1030,27 +1067,27 @@
}; };
name = Profile; name = Profile;
}; };
249021D4217E4FDB00AE95B9 /* Profile */ = { 249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1101; CURRENT_PROJECT_VERSION = 1102;
DEVELOPMENT_TEAM = UT7MSP4GWZ; DEVELOPMENT_TEAM = R9PZGUCNJ3;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Firka Testing"; INFOPLIST_KEY_CFBundleDisplayName = Firka;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.9; MARKETING_VERSION = 1.0.9;
PRODUCT_BUNDLE_IDENTIFIER = app.firka.firkaa; PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@@ -1071,7 +1108,8 @@
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1101; CURRENT_PROJECT_VERSION = 1102;
DEVELOPMENT_TEAM = R9PZGUCNJ3;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka.RunnerTests;
@@ -1090,7 +1128,8 @@
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1101; CURRENT_PROJECT_VERSION = 1102;
DEVELOPMENT_TEAM = R9PZGUCNJ3;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka.RunnerTests;
@@ -1107,7 +1146,8 @@
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1101; CURRENT_PROJECT_VERSION = 1102;
DEVELOPMENT_TEAM = R9PZGUCNJ3;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka.RunnerTests;
@@ -1122,9 +1162,9 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB41CF90195004384FC /* WidgetExtension.xcconfig */; baseConfigurationReference = 9740EEB41CF90195004384FC /* WidgetExtension.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AppIcon;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = AppIcon;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES; CLANG_ENABLE_OBJC_WEAK = YES;
@@ -1134,8 +1174,8 @@
CODE_SIGN_ENTITLEMENTS = LiveActivityWidget.entitlements; CODE_SIGN_ENTITLEMENTS = LiveActivityWidget.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1101; CURRENT_PROJECT_VERSION = 1102;
DEVELOPMENT_TEAM = UT7MSP4GWZ; DEVELOPMENT_TEAM = R9PZGUCNJ3;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17; GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -1153,7 +1193,7 @@
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)"; MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.firka.firkaa.LiveActivityWidget; PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka.LiveActivityWidget;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@@ -1173,9 +1213,9 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB41CF90195004384FC /* WidgetExtension.xcconfig */; baseConfigurationReference = 9740EEB41CF90195004384FC /* WidgetExtension.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AppIcon;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = AppIcon;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES; CLANG_ENABLE_OBJC_WEAK = YES;
@@ -1185,8 +1225,8 @@
CODE_SIGN_ENTITLEMENTS = LiveActivityWidget.entitlements; CODE_SIGN_ENTITLEMENTS = LiveActivityWidget.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1101; CURRENT_PROJECT_VERSION = 1102;
DEVELOPMENT_TEAM = UT7MSP4GWZ; DEVELOPMENT_TEAM = R9PZGUCNJ3;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17; GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -1203,7 +1243,7 @@
LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)"; MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.firka.firkaa.LiveActivityWidget; PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka.LiveActivityWidget;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@@ -1221,9 +1261,9 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB41CF90195004384FC /* WidgetExtension.xcconfig */; baseConfigurationReference = 9740EEB41CF90195004384FC /* WidgetExtension.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AppIcon;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = AppIcon;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES; CLANG_ENABLE_OBJC_WEAK = YES;
@@ -1233,8 +1273,8 @@
CODE_SIGN_ENTITLEMENTS = LiveActivityWidget.entitlements; CODE_SIGN_ENTITLEMENTS = LiveActivityWidget.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1101; CURRENT_PROJECT_VERSION = 1102;
DEVELOPMENT_TEAM = UT7MSP4GWZ; DEVELOPMENT_TEAM = R9PZGUCNJ3;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17; GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -1251,7 +1291,7 @@
LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)"; MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.firka.firkaa.LiveActivityWidget; PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka.LiveActivityWidget;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@@ -1269,9 +1309,9 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ARCHS = "$(ARCHS_STANDARD)"; ARCHS = "$(ARCHS_STANDARD)";
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AppIcon;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = AppIcon;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES; CLANG_ENABLE_OBJC_WEAK = YES;
@@ -1280,8 +1320,8 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = FirkaWatchComplicationsExtension.entitlements; CODE_SIGN_ENTITLEMENTS = FirkaWatchComplicationsExtension.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1101; CURRENT_PROJECT_VERSION = 1102;
DEVELOPMENT_TEAM = UT7MSP4GWZ; DEVELOPMENT_TEAM = R9PZGUCNJ3;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17; GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -1301,7 +1341,7 @@
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
OTHER_LDFLAGS = ""; OTHER_LDFLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = app.firka.firkaa.watchkitapp.complications; PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka.watchkitapp.complications;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos; SDKROOT = watchos;
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@@ -1322,9 +1362,9 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ARCHS = "$(ARCHS_STANDARD)"; ARCHS = "$(ARCHS_STANDARD)";
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AppIcon;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = AppIcon;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES; CLANG_ENABLE_OBJC_WEAK = YES;
@@ -1333,8 +1373,8 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = FirkaWatchComplicationsExtension.entitlements; CODE_SIGN_ENTITLEMENTS = FirkaWatchComplicationsExtension.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1101; CURRENT_PROJECT_VERSION = 1102;
DEVELOPMENT_TEAM = UT7MSP4GWZ; DEVELOPMENT_TEAM = R9PZGUCNJ3;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17; GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -1352,7 +1392,7 @@
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_LDFLAGS = ""; OTHER_LDFLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = app.firka.firkaa.watchkitapp.complications; PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka.watchkitapp.complications;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos; SDKROOT = watchos;
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@@ -1372,9 +1412,9 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ARCHS = "$(ARCHS_STANDARD)"; ARCHS = "$(ARCHS_STANDARD)";
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AppIcon;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = AppIcon;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES; CLANG_ENABLE_OBJC_WEAK = YES;
@@ -1383,8 +1423,8 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = FirkaWatchComplicationsExtension.entitlements; CODE_SIGN_ENTITLEMENTS = FirkaWatchComplicationsExtension.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1101; CURRENT_PROJECT_VERSION = 1102;
DEVELOPMENT_TEAM = UT7MSP4GWZ; DEVELOPMENT_TEAM = R9PZGUCNJ3;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17; GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -1402,7 +1442,7 @@
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_LDFLAGS = ""; OTHER_LDFLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = app.firka.firkaa.watchkitapp.complications; PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka.watchkitapp.complications;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos; SDKROOT = watchos;
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@@ -1422,9 +1462,9 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB41CF90195004384FC /* WidgetExtension.xcconfig */; baseConfigurationReference = 9740EEB41CF90195004384FC /* WidgetExtension.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AppIcon;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = AppIcon;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES; CLANG_ENABLE_OBJC_WEAK = YES;
@@ -1434,8 +1474,8 @@
CODE_SIGN_ENTITLEMENTS = HomeWidgetsExtension.entitlements; CODE_SIGN_ENTITLEMENTS = HomeWidgetsExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1101; CURRENT_PROJECT_VERSION = 1102;
DEVELOPMENT_TEAM = UT7MSP4GWZ; DEVELOPMENT_TEAM = R9PZGUCNJ3;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17; GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -1454,7 +1494,7 @@
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.firka.firkaa.HomeWidgetsExtension; PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka.HomeWidgetsExtension;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@@ -1473,9 +1513,9 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB41CF90195004384FC /* WidgetExtension.xcconfig */; baseConfigurationReference = 9740EEB41CF90195004384FC /* WidgetExtension.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AppIcon;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = AppIcon;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES; CLANG_ENABLE_OBJC_WEAK = YES;
@@ -1485,8 +1525,8 @@
CODE_SIGN_ENTITLEMENTS = HomeWidgetsExtension.entitlements; CODE_SIGN_ENTITLEMENTS = HomeWidgetsExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1101; CURRENT_PROJECT_VERSION = 1102;
DEVELOPMENT_TEAM = UT7MSP4GWZ; DEVELOPMENT_TEAM = R9PZGUCNJ3;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17; GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -1503,7 +1543,7 @@
LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)"; MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.firka.firkaa.HomeWidgetsExtension; PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka.HomeWidgetsExtension;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@@ -1520,9 +1560,9 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB41CF90195004384FC /* WidgetExtension.xcconfig */; baseConfigurationReference = 9740EEB41CF90195004384FC /* WidgetExtension.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AppIcon;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = AppIcon;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES; CLANG_ENABLE_OBJC_WEAK = YES;
@@ -1532,8 +1572,8 @@
CODE_SIGN_ENTITLEMENTS = HomeWidgetsExtension.entitlements; CODE_SIGN_ENTITLEMENTS = HomeWidgetsExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1101; CURRENT_PROJECT_VERSION = 1102;
DEVELOPMENT_TEAM = UT7MSP4GWZ; DEVELOPMENT_TEAM = R9PZGUCNJ3;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17; GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -1550,7 +1590,7 @@
LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)"; MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.firka.firkaa.HomeWidgetsExtension; PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka.HomeWidgetsExtension;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@@ -1563,13 +1603,13 @@
}; };
name = Profile; name = Profile;
}; };
4FF81B9C2F2EB4C300E95BA0 /* Debug */ = { 4FF81B9C2F2EB4C300E95BA0 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ARCHS = "$(ARCHS_STANDARD)"; ARCHS = "$(ARCHS_STANDARD)";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AppIcon;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES; CLANG_ENABLE_OBJC_WEAK = YES;
@@ -1579,14 +1619,15 @@
CODE_SIGN_ENTITLEMENTS = "FirkaWatch Watch App/FirkaWatch Watch App.entitlements"; CODE_SIGN_ENTITLEMENTS = "FirkaWatch Watch App/FirkaWatch Watch App.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = UT7MSP4GWZ; DEVELOPMENT_TEAM = R9PZGUCNJ3;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17; GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = FirkaWatch; INFOPLIST_KEY_CFBundleDisplayName = FirkaWatch;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_NSSupportsLiveActivities = YES;
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = app.firka.firkaa; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = app.firka.firka;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@@ -1596,7 +1637,7 @@
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.firka.firkaa.watchkitapp; PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka.watchkitapp;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = watchos; SDKROOT = watchos;
@@ -1615,13 +1656,13 @@
}; };
name = Debug; name = Debug;
}; };
4FF81B9D2F2EB4C300E95BA0 /* Release */ = { 4FF81B9D2F2EB4C300E95BA0 /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ARCHS = "$(ARCHS_STANDARD)"; ARCHS = "$(ARCHS_STANDARD)";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AppIcon;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES; CLANG_ENABLE_OBJC_WEAK = YES;
@@ -1631,14 +1672,15 @@
CODE_SIGN_ENTITLEMENTS = "FirkaWatch Watch App/FirkaWatch Watch App.entitlements"; CODE_SIGN_ENTITLEMENTS = "FirkaWatch Watch App/FirkaWatch Watch App.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = UT7MSP4GWZ; DEVELOPMENT_TEAM = R9PZGUCNJ3;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17; GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = FirkaWatch; INFOPLIST_KEY_CFBundleDisplayName = FirkaWatch;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_NSSupportsLiveActivities = YES;
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = app.firka.firkaa; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = app.firka.firka;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@@ -1646,7 +1688,7 @@
LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.firka.firkaa.watchkitapp; PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka.watchkitapp;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = watchos; SDKROOT = watchos;
@@ -1664,13 +1706,13 @@
}; };
name = Release; name = Release;
}; };
4FF81B9E2F2EB4C300E95BA0 /* Profile */ = { 4FF81B9E2F2EB4C300E95BA0 /* Profile */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ARCHS = "$(ARCHS_STANDARD)"; ARCHS = "$(ARCHS_STANDARD)";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AppIcon;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES; CLANG_ENABLE_OBJC_WEAK = YES;
@@ -1680,14 +1722,15 @@
CODE_SIGN_ENTITLEMENTS = "FirkaWatch Watch App/FirkaWatch Watch App.entitlements"; CODE_SIGN_ENTITLEMENTS = "FirkaWatch Watch App/FirkaWatch Watch App.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = UT7MSP4GWZ; DEVELOPMENT_TEAM = R9PZGUCNJ3;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17; GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = FirkaWatch; INFOPLIST_KEY_CFBundleDisplayName = FirkaWatch;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_NSSupportsLiveActivities = YES;
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = app.firka.firkaa; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = app.firka.firka;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@@ -1695,7 +1738,7 @@
LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.firka.firkaa.watchkitapp; PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka.watchkitapp;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = watchos; SDKROOT = watchos;
@@ -1828,27 +1871,27 @@
}; };
name = Release; name = Release;
}; };
97C147061CF9000F007C117D /* Debug */ = { 97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1101; CURRENT_PROJECT_VERSION = 1102;
DEVELOPMENT_TEAM = UT7MSP4GWZ; DEVELOPMENT_TEAM = R9PZGUCNJ3;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Firka Testing"; INFOPLIST_KEY_CFBundleDisplayName = Firka;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.9; MARKETING_VERSION = 1.0.9;
PRODUCT_BUNDLE_IDENTIFIER = app.firka.firkaa; PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@@ -1864,27 +1907,27 @@
}; };
name = Debug; name = Debug;
}; };
97C147071CF9000F007C117D /* Release */ = { 97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1101; CURRENT_PROJECT_VERSION = 1102;
DEVELOPMENT_TEAM = UT7MSP4GWZ; DEVELOPMENT_TEAM = R9PZGUCNJ3;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Firka Testing"; INFOPLIST_KEY_CFBundleDisplayName = Firka;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.9; MARKETING_VERSION = 1.0.9;
PRODUCT_BUNDLE_IDENTIFIER = app.firka.firkaa; PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";

View File

@@ -30,8 +30,8 @@ import BackgroundTasks
widgetDeepLinkChannel = FlutterMethodChannel(name: "firka.app/widget_deep_link", binaryMessenger: controller.binaryMessenger) widgetDeepLinkChannel = FlutterMethodChannel(name: "firka.app/widget_deep_link", binaryMessenger: controller.binaryMessenger)
widgetDeepLinkChannel?.setMethodCallHandler { [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) in widgetDeepLinkChannel?.setMethodCallHandler { [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) in
if call.method == "getPendingDeepLink" { if call.method == "getPendingDeepLink" {
if let controlNav = UserDefaults(suiteName: "group.app.firka.firkaa")?.string(forKey: "controlNavigation") { if let controlNav = UserDefaults(suiteName: "group.app.firka.firka")?.string(forKey: "controlNavigation") {
UserDefaults(suiteName: "group.app.firka.firkaa")?.removeObject(forKey: "controlNavigation") UserDefaults(suiteName: "group.app.firka.firka")?.removeObject(forKey: "controlNavigation")
result(controlNav) result(controlNav)
} else if let link = self?.pendingWidgetDeepLink { } else if let link = self?.pendingWidgetDeepLink {
self?.pendingWidgetDeepLink = nil self?.pendingWidgetDeepLink = nil

Binary file not shown.

Before

Width:  |  Height:  |  Size: 660 B

After

Width:  |  Height:  |  Size: 652 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -11,7 +11,7 @@ class HomeWidgetMethodChannel {
switch call.method { switch call.method {
case "getAppGroupDirectory": case "getAppGroupDirectory":
if let containerURL = FileManager.default.containerURL( if let containerURL = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: "group.app.firka.firkaa" forSecurityApplicationGroupIdentifier: "group.app.firka.firka"
) { ) {
result(containerURL.path) result(containerURL.path)
} else { } else {

View File

@@ -1,89 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>BGTaskSchedulerPermittedIdentifiers</key> <key>BGTaskSchedulerPermittedIdentifiers</key>
<array> <array>
<string>app.firka.timetable.refresh</string> <string>app.firka.timetable.refresh</string>
</array> </array>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
<key>CFBundleAllowMixedLocalizations</key> <key>CFBundleAllowMixedLocalizations</key>
<true/> <true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>Firka Testing</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleLocalizations</key>
<array>
<string>en</string> <string>en</string>
<key>CFBundleLocalizations</key> <string>hu</string>
<array> <string>de</string>
<string>en</string> </array>
<string>hu</string> <key>CFBundleName</key>
<string>de</string> <string>firka</string>
</array> <key>CFBundlePackageType</key>
<key>CFBundleDisplayName</key> <string>APPL</string>
<string>Firka Testing</string> <key>CFBundleShortVersionString</key>
<key>CFBundleExecutable</key> <string>$(FLUTTER_BUILD_NAME)</string>
<string>$(EXECUTABLE_NAME)</string> <key>CFBundleSignature</key>
<key>CFBundleIdentifier</key> <string>????</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <key>CFBundleURLTypes</key>
<key>CFBundleInfoDictionaryVersion</key> <array>
<string>6.0</string>
<key>CFBundleName</key>
<string>firka</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>app.firka.firkaa</string>
<key>CFBundleURLSchemes</key>
<array>
<string>firka</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>1101</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict> <dict>
<key>NSAllowsArbitraryLoads</key> <key>CFBundleURLName</key>
<true/> <string>app.firka.firka</string>
<key>CFBundleURLSchemes</key>
<array>
<string>firka</string>
</array>
</dict> </dict>
<key>NSSupportsLiveActivities</key> </array>
<key>CFBundleVersion</key>
<string>1102</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/> <true/>
<key>NSSupportsLiveActivitiesFrequentUpdates</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
<string>fetch</string>
<string>processing</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIStatusBarHidden</key>
<false/>
</dict> </dict>
<key>NSSupportsLiveActivities</key>
<true/>
<key>NSSupportsLiveActivitiesFrequentUpdates</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
<string>fetch</string>
<string>processing</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIStatusBarHidden</key>
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist> </plist>

View File

@@ -4,17 +4,17 @@
<dict> <dict>
<key>aps-environment</key> <key>aps-environment</key>
<string>development</string> <string>development</string>
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
<string>$(TeamIdentifierPrefix)app.firka.firka</string>
<key>com.apple.developer.usernotifications.time-sensitive</key> <key>com.apple.developer.usernotifications.time-sensitive</key>
<true/> <true/>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>group.app.firka.firkaa</string> <string>group.app.firka.firka</string>
</array> </array>
<key>keychain-access-groups</key> <key>keychain-access-groups</key>
<array> <array>
<string>$(AppIdentifierPrefix)app.firka.shared</string> <string>$(AppIdentifierPrefix)app.firka.shared</string>
</array> </array>
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
<string>$(TeamIdentifierPrefix)app.firka.firkaa</string>
</dict> </dict>
</plist> </plist>

View File

@@ -32,7 +32,7 @@ enum TokenError: Error {
class TokenManager { class TokenManager {
static let shared = TokenManager() static let shared = TokenManager()
private let appGroupID = "group.app.firka.firkaa" private let appGroupID = "group.app.firka.firka"
private let tokenFileName = "watch_token.json" private let tokenFileName = "watch_token.json"
private static let keychainService = "app.firka.watch.token" private static let keychainService = "app.firka.watch.token"

View File

@@ -12,7 +12,7 @@ struct WidgetData: Codable {
static func load() -> WidgetData? { static func load() -> WidgetData? {
guard let containerURL = FileManager.default.containerURL( guard let containerURL = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: "group.app.firka.firkaa" forSecurityApplicationGroupIdentifier: "group.app.firka.firka"
) else { ) else {
lastError = "No App Group container" lastError = "No App Group container"
return nil return nil

View File

@@ -3,17 +3,17 @@ 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';
import 'package:firka/data/models/token_model.dart'; import 'package:firka/data/models/token_model.dart';
import 'package:firka/data/util.dart';
import 'package:firka/core/debug_helper.dart'; import 'package:firka/core/debug_helper.dart';
import 'package:firka/data/util.dart';
import 'package:firka/services/active_account_helper.dart'; import 'package:firka/services/active_account_helper.dart';
import 'package:firka/services/watch_sync_helper.dart'; import 'package:firka/services/watch_sync_helper.dart';
import '../consts.dart'; import '../consts.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();

View File

@@ -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);
} }

View File

@@ -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,
); );

View File

@@ -16,6 +16,7 @@ import 'package:firka/core/bloc/theme_cubit.dart';
import 'package:firka/core/firka_bundle.dart'; import 'package:firka/core/firka_bundle.dart';
import 'package:firka/routing/app_router.dart'; import 'package:firka/routing/app_router.dart';
import 'package:firka/services/watch_sync_helper.dart'; import 'package:firka/services/watch_sync_helper.dart';
import 'package:firka/ui/theme/style.dart';
import 'package:firka/ui/phone/pages/extras/main_wear_pair.dart'; import 'package:firka/ui/phone/pages/extras/main_wear_pair.dart';
import 'package:firka/l10n/app_localizations.dart'; import 'package:firka/l10n/app_localizations.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
@@ -34,6 +35,23 @@ class _InitializationScreenState extends State<InitializationScreen> {
const Duration(seconds: 20), const Duration(seconds: 20),
); );
ThemeData _buildTheme(FirkaStyle style) {
return ThemeData(
scaffoldBackgroundColor: style.colors.background,
canvasColor: style.colors.background,
bottomSheetTheme: const BottomSheetThemeData(
backgroundColor: Colors.transparent,
),
colorScheme: ColorScheme.fromSeed(
seedColor: style.colors.accent,
brightness: style.isLight ? Brightness.light : Brightness.dark,
background: style.colors.background,
surface: style.colors.card,
),
useMaterial3: false,
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureBuilder<AppInitialization>( return FutureBuilder<AppInitialization>(
@@ -180,40 +198,47 @@ class _InitializationScreenState extends State<InitializationScreen> {
BlocProvider<ReauthCubit>.value(value: reauthCubit), BlocProvider<ReauthCubit>.value(value: reauthCubit),
BlocProvider<HomeRefreshCubit>.value(value: homeRefreshCubit), BlocProvider<HomeRefreshCubit>.value(value: homeRefreshCubit),
], ],
child: MaterialApp.router( child: BlocBuilder<ThemeCubit, ThemeState>(
title: 'Firka', builder: (context, themeState) {
key: const ValueKey('firkaApp'), final isLight = themeState.isLightMode;
routerConfig: _router!, final overlay = SystemUiOverlayStyle(
theme: ThemeData( statusBarColor: Colors.transparent,
primarySwatch: Colors.lightGreen, statusBarIconBrightness:
visualDensity: VisualDensity.adaptivePlatformDensity, isLight ? Brightness.dark : Brightness.light,
), statusBarBrightness:
localizationsDelegates: const [ isLight ? Brightness.light : Brightness.dark,
AppLocalizations.delegate, systemStatusBarContrastEnforced: false,
GlobalMaterialLocalizations.delegate, );
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
builder: (context, child) {
return BlocBuilder<ThemeCubit, ThemeState>(
builder: (context, themeState) {
final isLight = themeState.isLightMode;
final overlay = SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: isLight
? Brightness.dark
: Brightness.light,
statusBarBrightness: isLight
? Brightness.light
: Brightness.dark,
systemStatusBarContrastEnforced: false,
);
SystemChrome.setSystemUIOverlayStyle(overlay); final themeMode =
isLight ? ThemeMode.light : ThemeMode.dark;
final fallbackBg = isLight
? lightStyle.colors.background
: darkStyle.colors.background;
SystemChrome.setSystemUIOverlayStyle(overlay);
return MaterialApp.router(
title: 'Firka',
key: const ValueKey('firkaApp'),
routerConfig: _router!,
theme: _buildTheme(lightStyle),
darkTheme: _buildTheme(darkStyle),
themeMode: themeMode,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
builder: (context, child) {
return AnnotatedRegion<SystemUiOverlayStyle>( return AnnotatedRegion<SystemUiOverlayStyle>(
value: overlay, value: overlay,
child: child ?? const SizedBox.shrink(), child: ColoredBox(
color: fallbackBg,
child: child ?? const SizedBox.shrink(),
),
); );
}, },
); );

View File

@@ -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));
}

View File

@@ -1,17 +1 @@
DateTime? debugFakeTime; export 'package:firka_common/core/debug_helper.dart';
DateTime? debugSetAt;
var debugTimeAdvance = false;
DateTime timeNow() {
if (debugFakeTime != null) {
if (debugTimeAdvance && debugSetAt != null) {
var diff = DateTime.now().difference(debugSetAt!);
return debugFakeTime!.add(diff);
} else {
return debugFakeTime!;
}
} else {
return DateTime.now();
}
}

View File

@@ -1,8 +1,11 @@
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:kreta_api/kreta_api.dart';
import 'package:firka/core/debug_helper.dart';
import 'package:firka/l10n/app_localizations.dart'; import 'package:firka/l10n/app_localizations.dart';
import 'package:firka_common/core/debug_helper.dart';
import 'package:firka_common/core/extensions.dart';
import 'package:kreta_api/kreta_api.dart';
export 'package:firka_common/core/extensions.dart';
extension TimetableExtension on Iterable<Lesson> { extension TimetableExtension on Iterable<Lesson> {
List<Lesson> getAllSeqs(Lesson reference) { List<Lesson> getAllSeqs(Lesson reference) {
@@ -11,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,
@@ -59,26 +61,6 @@ extension TimetableExtension on Iterable<Lesson> {
} }
} }
extension IterableExtensionMap on Iterable<MapEntry<String, dynamic>> {
Map<String, dynamic> toMap() {
var map = <String, dynamic>{};
for (var item in this) {
map[item.key] = item.value;
}
return map;
}
}
extension IterableExtension<T> on Iterable<T> {
T? firstWhereOrNull(bool Function(T element) test) {
for (var element in this) {
if (test(element)) return element;
}
return null;
}
}
extension DurationExtension on Duration { extension DurationExtension on Duration {
String formatDuration() { String formatDuration() {
String hours = inHours.toString().padLeft(2, '0'); String hours = inHours.toString().padLeft(2, '0');
@@ -86,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,
@@ -104,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));
@@ -113,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:
@@ -172,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);
} }
} }
@@ -224,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);
@@ -297,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))),
); );
} }
@@ -320,4 +294,26 @@ extension StringExtension on String {
if (length == 1) this[0].toUpperCase(); if (length == 1) this[0].toUpperCase();
return this[0].toUpperCase() + substring(1, length); return this[0].toUpperCase() + substring(1, length);
} }
String shortenName([int start = 0]) {
if (length <= 16 || start >= length) {
return this;
}
int index = indexOf(" ", start);
if (index == -1) {
return this;
}
String string = substring(start, index);
if (string.endsWith(".")) {
return this;
}
return replaceRange(
start,
index,
"${string[0]}.",
).shortenName(index - string.length + 2 + 1);
}
} }

View File

@@ -1,62 +1,12 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:brotli/brotli.dart';
import 'package:firka/app/app_state.dart'; import 'package:firka/app/app_state.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
class FirkaBundle extends CachingAssetBundle { class FirkaBundle extends CachingAssetBundle {
// final bool _compressedBundle = !kDebugMode && Platform.isAndroid;
final bool _compressedBundle = false;
Map<String, dynamic>? index;
Future<Map<String, dynamic>> loadIndex() async {
var indexBrotli = await rootBundle.load("assets/firka.i");
var indexStr = brotli.decodeToString(indexBrotli.buffer.asInt8List());
return Future.value(jsonDecode(indexStr));
}
ByteData decode(Codec<List<int>, List<int>> codec, ByteData data) {
var dec = codec.decode(data.buffer.asInt8List());
var b = ByteData(dec.length);
var l = b.buffer.asInt8List();
for (var i = 0; i < dec.length; i++) {
l[i] = dec[i];
}
return b;
}
@override @override
Future<ByteData> load(String key) async { Future<ByteData> load(String key) async {
if (!_compressedBundle) { logger.finest(
logger.finest( "Loading asset from root bundle: assets/flutter_assets/$key",
"Loading asset from root bundle: assets/flutter_assets/$key", );
); return rootBundle.load(key);
return rootBundle.load(key);
} else {
index ??= await loadIndex();
final gzip = GZipCodec();
logger.finest(
"Loading asset from firka bundle: assets/flutter_assets/$key",
);
switch (index!["assets/flutter_assets/$key"]!) {
case "b": // brotli
return decode(brotli, await rootBundle.load(key));
case "g": // gzip
return decode(gzip, await rootBundle.load(key));
case "r": // raw
return rootBundle.load(key);
default:
logger.shout("Unknown file format: ${index![key]!}");
throw "Unknown file format: ${index![key]!}";
}
}
} }
} }

View File

@@ -1,152 +1 @@
import 'dart:typed_data'; export 'package:firka_common/core/icon_helper.dart';
import 'package:majesticons_flutter/majesticons_flutter.dart';
enum ClassIcon {
mathematics,
grammar,
literature,
history,
geography,
art,
physics,
music,
pe,
chemistry,
biology,
env,
religion,
economics,
it,
code,
networking,
theatre,
film,
electricalEngineering,
mechanicalEngineering,
technika,
dance,
philosophy,
ofo,
diligence,
attitude,
language,
linux,
database,
applications,
project,
}
Map<ClassIcon, RegExp> _descriptors = {
ClassIcon.mathematics: RegExp(r'mate(k|matika)'),
ClassIcon.grammar: RegExp(r'magyar nyelv|nyelvtan'),
ClassIcon.literature: RegExp(r'irodalom'),
ClassIcon.history: RegExp(r'tor(i|tenelem)'),
ClassIcon.geography: RegExp(r'foldrajz'),
ClassIcon.art: RegExp(r'rajz|muvtori|muveszet|vizualis'),
ClassIcon.physics: RegExp(r'fizika'),
ClassIcon.music: RegExp(r'^enek|zene|szolfezs|zongora|korus'),
ClassIcon.pe: RegExp(r'^tes(i|tneveles)|sport|edzeselmelet'),
ClassIcon.chemistry: RegExp(r'kemia'),
ClassIcon.biology: RegExp(r'biologia'),
ClassIcon.env: RegExp(
r'kornyezet|termeszet ?(tudomany|ismeret)|hon( es nep)?ismeret',
),
ClassIcon.religion: RegExp(r'(hit|erkolcs)tan|vallas|etika|bibliaismeret'),
ClassIcon.economics: RegExp(r'penzugy|gazdasag'),
ClassIcon.it: RegExp(r'informatika|szoftver|iroda|digitalis'),
ClassIcon.code: RegExp(r'prog|alkalmazas'),
ClassIcon.networking: RegExp(r'halozat'),
ClassIcon.theatre: RegExp(r'szinhaz'),
ClassIcon.film: RegExp(r'film|media'),
ClassIcon.electricalEngineering: RegExp(r'elektro(tech)?nika'),
ClassIcon.mechanicalEngineering: RegExp(r'gepesz|mernok|ipar'),
ClassIcon.technika: RegExp(r'technika'),
ClassIcon.dance: RegExp(r'tanc'),
ClassIcon.philosophy: RegExp(r'filozofia'),
ClassIcon.ofo: RegExp(r'osztaly(fonoki|kozosseg)|kozossegi|neveles'),
ClassIcon.diligence: RegExp(r'szorgalom'),
ClassIcon.attitude: RegExp(r'magatartas'),
ClassIcon.language: RegExp(
r'angol|nemet|francia|olasz|orosz|spanyol|latin|kinai|nyelv',
),
ClassIcon.linux: RegExp(r'linux'),
ClassIcon.database: RegExp(r'adatbazis.*'),
ClassIcon.applications: RegExp(r'asztali alkalmazasok'),
ClassIcon.project: RegExp(r'projekt'),
};
Map<ClassIcon, Uint8List> _iconMap = {
ClassIcon.mathematics: Majesticon.calculatorSolid,
ClassIcon.grammar: Majesticon.bookSolid,
ClassIcon.literature: Majesticon.bookOpenSolid,
ClassIcon.history: Majesticon.compass2Solid,
ClassIcon.geography: Majesticon.globeEarth2Solid,
ClassIcon.art: Majesticon.editPen2Solid,
// ClassIcon.physics: ,
ClassIcon.music: Majesticon.musicNoteSolid,
// ClassIcon.pe: ,
ClassIcon.chemistry: Majesticon.testTubeFilledSolid,
ClassIcon.biology: Majesticon.covidSolid,
// ClassIcon.env: ,
// ClassIcon.religion: ,
// ClassIcon.economics: ,
ClassIcon.it: Majesticon.laptopSolid,
ClassIcon.code: Majesticon.curlyBracesSolid,
ClassIcon.networking: Majesticon.cloudSolid,
// ClassIcon.theatre: ,
// ClassIcon.film: ,
// ClassIcon.electricalEngineering: ,
// ClassIcon.mechanicalEngineering: ,
ClassIcon.technika: Majesticon.ruler2Solid,
// ClassIcon.dance: ,
// ClassIcon.philosophy: ,
// ClassIcon.ofo: ,
// ClassIcon.diligence: ,
// ClassIcon.attitude: ,
ClassIcon.language: Majesticon.tooltipsSolid,
// ClassIcon.linux: ,
ClassIcon.database: Majesticon.dataSolid,
// ClassIcon.applications: ,
// ClassIcon.project: ,
};
ClassIcon? getIconType(String uid, String className, String category) {
ClassIcon? icon;
if (category.toLowerCase() == "matematika") {
icon = ClassIcon.mathematics;
}
if (icon == null) {
for (var desc in _descriptors.entries) {
if (desc.value.hasMatch(
className
.replaceAll("ö", "o")
.replaceAll("ü", "u")
.replaceAll("ó", "o")
.replaceAll("ő", "o")
.replaceAll("ú", "u")
.replaceAll("é", "e")
.replaceAll("á", "a")
.replaceAll("ű", "u")
.replaceAll("í", "i")
.toLowerCase(),
)) {
icon = desc.key;
break;
}
}
}
return icon;
}
Uint8List getIconData(ClassIcon? icon) {
if (icon == null) return Majesticon.alertCircleSolid;
var iconData = _iconMap[icon];
iconData ??= Majesticon.alertCircleSolid;
return iconData;
}

View File

@@ -1,9 +1 @@
List<T> listToTyped<T>(List<dynamic> dynamicList) { export 'package:firka_common/core/json_helper.dart';
var newList = List<T>.empty(growable: true);
for (var item in dynamicList) {
newList.add(item as T);
}
return newList;
}

View File

@@ -13,6 +13,7 @@ enum CacheId {
getSubjectAvg, getSubjectAvg,
getLessons, getLessons,
getHomework, getHomework,
getClassGroupAvg,
} }
@collection @collection

View File

@@ -3,8 +3,8 @@ import 'dart:convert';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; import 'package:dart_jsonwebtoken/dart_jsonwebtoken.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/extensions.dart'; import 'package:firka/core/extensions.dart';
import 'package:firka_common/core/debug_helper.dart';
import 'package:isar_community/isar.dart'; import 'package:isar_community/isar.dart';
part 'token_model.g.dart'; part 'token_model.g.dart';

View File

@@ -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);
} }

View File

@@ -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;
}
} }

View File

@@ -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),
);
},
),
], ],
), ),
], ],

View File

@@ -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,
), ),
], ],

View File

@@ -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

View File

@@ -1,134 +1 @@
import 'package:flutter/material.dart'; export 'package:firka_common/ui/components/firka_card.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:firka/core/bloc/theme_cubit.dart';
import 'package:firka/ui/components/firka_shadow.dart';
import 'package:firka/ui/theme/style.dart';
enum Attach { none, bottom, top }
class FirkaCard extends StatelessWidget {
final List<Widget> left;
final List<Widget>? center;
final double? height;
final List<Widget>? right;
final bool shadow;
final Widget? extra;
final Attach? attached;
final Color? color;
const FirkaCard({
required this.left,
this.shadow = true,
this.center,
this.right,
this.extra,
this.attached,
this.color,
this.height,
super.key,
});
@override
Widget build(BuildContext context) {
var right = this.right ?? [];
var attached = this.attached != null ? this.attached! : Attach.none;
final defaultRounding = 16.0;
final attachedRounding = 8.0;
final isLight = context.watch<ThemeCubit>().state.isLightMode;
if (extra != null) {
return SizedBox(
width: MediaQuery.of(context).size.width,
height: height,
child: FirkaShadow(
shadow: shadow,
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: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(children: left),
Row(children: center ?? []),
Row(children: right),
],
),
extra ?? SizedBox(),
],
),
),
),
),
);
} else {
return SizedBox(
width: MediaQuery.of(context).size.width,
height: height,
child: FirkaShadow(
shadow: shadow,
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),
],
),
),
),
),
);
}
}
}

View File

@@ -1,45 +1 @@
import 'package:flutter/material.dart'; export 'package:firka_common/ui/components/firka_shadow.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:firka/core/bloc/theme_cubit.dart';
import 'package:firka/ui/theme/style.dart';
class FirkaShadow extends StatelessWidget {
final Widget child;
final bool shadow;
const FirkaShadow({required this.shadow, required this.child, super.key});
@override
Widget build(BuildContext context) {
final borderRadius = BorderRadius.circular(8.0);
final shadowBox = BoxDecoration(
color: Colors.transparent,
shape: BoxShape.rectangle,
boxShadow: [
BoxShadow(
color: appStyle.colors.shadowColor,
spreadRadius: -4,
blurRadius: 0,
offset: Offset(0, 2),
),
],
borderRadius: BorderRadius.all(Radius.circular(16)),
);
if (!shadow) {
return ClipRRect(borderRadius: borderRadius, child: child);
}
final isLight = context.watch<ThemeCubit>().state.isLightMode;
if (isLight) {
return child;
} else {
return Container(
decoration: shadowBox,
child: ClipRRect(borderRadius: borderRadius, child: child),
);
}
}
}

View File

@@ -1,97 +1 @@
import 'package:kreta_api/kreta_api.dart'; export 'package:firka_common/ui/components/grade.dart';
import 'package:flutter/material.dart';
import 'package:firka/ui/theme/style.dart';
import 'package:firka/ui/components/grade_helpers.dart';
class GradeWidget extends StatelessWidget {
const GradeWidget(this.grade, {super.key})
: gradeValue = null,
_fromValue = false;
const GradeWidget.gradeValue(int value, {super.key})
: grade = null,
gradeValue = value,
_fromValue = true;
final Grade? grade;
final int? gradeValue;
final bool _fromValue;
@override
Widget build(BuildContext context) {
if (_fromValue && gradeValue != null) {
return _buildNumericCircle(
gradeValue!,
getGradeColor(gradeValue!.toDouble()),
);
}
final g = grade!;
Color gradeColor = appStyle.colors.grade1;
final gradeStr = g.numericValue?.toString() ?? '0';
if (g.valueType.name == 'Szazalekos') {
if (g.numericValue != null) {
gradeColor = getGradeColor(
percentageToGrade(g.numericValue!).toDouble(),
);
}
final str = g.strValue.replaceAll('%', '');
return Card(
shape: const CircleBorder(),
shadowColor: Colors.transparent,
color: gradeColor.withAlpha(38),
child: Padding(
padding: const EdgeInsets.all(8),
child: Row(
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) {
gradeColor = getGradeColor(g.numericValue!.toDouble());
}
if (gradeStr == '0') {
return Card(
shadowColor: Colors.transparent,
color: gradeColor.withAlpha(38),
child: Padding(
padding: const EdgeInsets.only(left: 8, right: 8, top: 2, bottom: 2),
child: Text(
g.strValue,
style: appStyle.fonts.H_H1.copyWith(
fontSize: 16,
color: gradeColor,
),
),
),
);
}
return _buildNumericCircle(g.numericValue!, gradeColor);
}
Widget _buildNumericCircle(int value, Color gradeColor) {
return Card(
shape: const CircleBorder(),
shadowColor: Colors.transparent,
color: gradeColor.withAlpha(38),
child: Padding(
padding: const EdgeInsets.only(left: 8, right: 8),
child: Text(
value.toString(),
style: appStyle.fonts.H_H1.copyWith(fontSize: 24, color: gradeColor),
),
),
);
}
}

View File

@@ -1,95 +1 @@
import 'dart:ui'; export 'package:firka_common/ui/components/grade_helpers.dart';
import 'package:firka/core/settings.dart';
import 'package:firka/app/app_state.dart';
import 'package:firka/ui/theme/style.dart';
import 'package:kreta_api/kreta_api.dart';
int roundGrade(double grade) {
final rounding = initData.settings
.group("settings")
.subGroup("application")
.subGroup("rounding");
if (grade < 1 + rounding.dbl("1")) {
return 1;
}
if (grade < 2 + rounding.dbl("2")) {
return 2;
}
if (grade < 3 + rounding.dbl("3")) {
return 3;
}
if (grade < 4 + rounding.dbl("4")) {
return 4;
}
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(double grade) {
switch (roundGrade(grade)) {
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;
}
}
(int total, List<int> countsByGrade) getGradeDistribution(List<Grade> grades) {
final filtered = grades
.where((g) => g.type.name != "felevi_jegy_ertekeles")
.toList();
final counts = [0, 0, 0, 0, 0];
for (final g in filtered) {
if (g.numericValue == null) continue;
final value = g.valueType.name == "Szazalekos"
? percentageToGrade(g.numericValue!.round())
: g.numericValue!.round().clamp(1, 5);
counts[value - 1]++;
}
return (filtered.length, counts);
}
extension GradeListExtension on List<Grade> {
double getAverageBySubject(Subject subject) {
var weightTotal = 0.00;
var sum = 0.00;
for (var grade in this) {
if (grade.subject.uid == subject.uid) {
if (grade.numericValue != null) {
var weight = (grade.weightPercentage ?? 100) / 100.0;
weightTotal += weight;
sum += grade.numericValue! * weight;
}
}
}
return sum / weightTotal;
}
}

View File

@@ -26,7 +26,7 @@ void showReauthBottomSheet(
elevation: 100, elevation: 100,
isScrollControlled: true, isScrollControlled: true,
isDismissible: true, isDismissible: true,
enableDrag: true, enableDrag: false,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
builder: (BuildContext context) { builder: (BuildContext context) {
return Container( return Container(

View File

@@ -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';
@@ -14,6 +16,7 @@ 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/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/core/settings.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';
@@ -26,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>();
@@ -53,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) {
@@ -76,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(() {});
@@ -95,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(
@@ -104,113 +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;
subjectAvgRounded += roundGrade(avg);
}
} }
subjectAvg /= subjectCount;
subjectAvgRounded /= subjectCount;
if (subjectCount == 0) {
subjectAvg = 0.00;
subjectAvgRounded = 0.00;
}
var subjectAvgColor = getGradeColor(subjectAvg);
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(
@@ -226,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: [
@@ -243,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,
@@ -262,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(
@@ -292,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(

View File

@@ -1,3 +1,6 @@
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';
@@ -20,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();
@@ -33,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(() {});
@@ -48,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(() {});
})(); })();
@@ -104,332 +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);
gradeWidgets.addAll(ghostGradeWidgets);
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,
), ),
), SizedBox(height: 16),
); Expanded(
gradeWidgets.add(SizedBox(height: 8)); child: Column(
for (var grade in group.value) { mainAxisAlignment: MainAxisAlignment.center,
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: 12), ),
Padding( Expanded(
padding: EdgeInsets.only(left: 4), child: Center(
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)),
],
),
),
],
),
],
),
),
],
),
);
} }
} }

View File

@@ -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 = true}) 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,95 @@ 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),
SizedBox(height: 24),
...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 {

View File

@@ -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()],
),
],
);
}
} }
} }

View File

@@ -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,
),
), ),
], ),
), ],
), ),
), ),
); );

View File

@@ -410,6 +410,11 @@ class _HomeScreenState extends FirkaState<HomeScreen>
await LiveActivityService.showConsentScreenIfNeeded(); await LiveActivityService.showConsentScreenIfNeeded();
}); });
} }
if (Platform.isIOS) {
Future.delayed(const Duration(seconds: 4), () {
if (!_disposed) _runLiveActivityLoginIfNeeded();
});
}
} }
Future<void> _preloadImages() async { Future<void> _preloadImages() async {
@@ -535,10 +540,35 @@ class _HomeScreenState extends FirkaState<HomeScreen>
if (Platform.isIOS) { if (Platform.isIOS) {
_refreshLiveActivityOnResume(); _refreshLiveActivityOnResume();
_runLiveActivityLoginIfNeeded();
} }
} }
} }
/// Fallback: if Live Activity login never ran (e.g. prefetch bailed on lifecycle
/// or fetchData didn't complete), run it once when app is resumed.
void _runLiveActivityLoginIfNeeded() {
if (_didRunLiveActivityLogin || _disposed) return;
Future.delayed(const Duration(milliseconds: 500), () async {
if (_disposed || _didRunLiveActivityLogin) return;
_didRunLiveActivityLogin = true;
final token = pickActiveToken(
tokens: initData.tokens,
settings: initData.settings,
preferredStudentIdNorm: initData.client.model.studentIdNorm,
);
final studentName = token?.studentId ?? 'Student';
LiveActivityService.onUserLogin(
client: initData.client,
studentName: studentName,
settingsStore: initData.settings,
).catchError((e, st) {
_didRunLiveActivityLogin = false;
logger.severe('LiveActivity registration failed: $e', e, st);
});
});
}
void _refreshLiveActivityOnResume() async { void _refreshLiveActivityOnResume() async {
if (!_hasCompletedFirstPrefetch) return; if (!_hasCompletedFirstPrefetch) return;
try { try {

View File

@@ -1,13 +1,19 @@
import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:carousel_slider/carousel_slider.dart'; 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: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_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:sensors_plus/sensors_plus.dart';
import 'package:vibration/vibration.dart';
import 'package:firka/core/bloc/theme_cubit.dart'; import 'package:firka/core/bloc/theme_cubit.dart';
import 'package:firka/core/state/firka_state.dart'; import 'package:firka/core/state/firka_state.dart';
@@ -15,16 +21,12 @@ 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';
// TODO: Replace these with actual privacy policy URLs const String _privacyUrlHungarian = 'https://firka.app/privacy-policy';
const String _privacyUrlHungarian = const String _privacyUrlOther = 'https://firka.app/privacy-policy';
'https://github.com/QwIT-Development/privacy-policy/blob/master/README.md';
const String _privacyUrlOther = 'https://firka.app/privacy';
class LoginScreen extends StatefulWidget { class LoginScreen extends StatefulWidget {
final AppInitialization data; final AppInitialization data;
const LoginScreen(this.data, {super.key}); const LoginScreen(this.data, {super.key});
@override @override
State<LoginScreen> createState() => _LoginScreenState(); State<LoginScreen> createState() => _LoginScreenState();
} }
@@ -36,29 +38,33 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loginWebView = LoginWebviewWidget(widget.data); _loginWebView = LoginWebviewWidget(widget.data);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
_preloadImages(); _preloadImages();
} }
// Method to get the appropriate privacy policy URL based on language
String _getPrivacyPolicyUrl() { String _getPrivacyPolicyUrl() {
// Check if current language is Hungarian by examining the locale return "https://firka.app/privacy";
final locale = Localizations.localeOf(context).languageCode;
return locale == 'hu' ? _privacyUrlHungarian : _privacyUrlOther;
} }
// Method to launch privacy policy URL Future<void> _showPrivacyPolicyWebview() async {
Future<void> _launchPrivacyPolicy() 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 {
@@ -72,11 +78,10 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
"assets/images/carousel_dark/slide3.webp", "assets/images/carousel_dark/slide3.webp",
"assets/images/carousel_dark/slide4.webp", "assets/images/carousel_dark/slide4.webp",
"assets/images/logos/colored_logo.webp", "assets/images/logos/colored_logo.webp",
"assets/images/logos/loading.gif",
]; ];
try { try {
await ImagePreloader.preloadMultipleAssets(FirkaBundle(), imagePaths); await ImagePreloader.preloadMultipleAssets(FirkaBundle(), imagePaths);
setState(() { setState(() {
_preloadDone = true; _preloadDone = true;
}); });
@@ -116,6 +121,7 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
final paddingWidthHorizontal = final paddingWidthHorizontal =
MediaQuery.of(context).size.width - MediaQuery.of(context).size.width -
MediaQuery.of(context).size.width * 0.95; MediaQuery.of(context).size.width * 0.95;
List<Map<String, Object>> slides = [ List<Map<String, Object>> slides = [
{ {
'title': widget.data.l10n.title1, 'title': widget.data.l10n.title1,
@@ -124,7 +130,6 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
'background': 'assets/images/carousel/slide1_background.webp', 'background': 'assets/images/carousel/slide1_background.webp',
'foreground': '', 'foreground': '',
'rotation': 180.00, 'rotation': 180.00,
// „Mi nekünk két szám típusunk van, int (egy 32 bites szám) meg a double (egy 64 bites tört szám), KURVA ANYÁDAT”
'scale': 1.5, 'scale': 1.5,
'x': 0.00, 'x': 0.00,
'y': 150.00, 'y': 150.00,
@@ -136,7 +141,6 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
'background': 'assets/images/carousel/slide2_background.webp', 'background': 'assets/images/carousel/slide2_background.webp',
'foreground': '', 'foreground': '',
'rotation': 180.00, 'rotation': 180.00,
//Mivel radiáns, és nullával nem lehet osztani (remélem tudtad), ezért ha eggyel osztunk akkor egy marad
'scale': 1.55, 'scale': 1.55,
'x': 10.00, 'x': 10.00,
'y': 160.00, 'y': 160.00,
@@ -155,15 +159,15 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
{ {
'title': widget.data.l10n.title4, 'title': widget.data.l10n.title4,
'subtitle': widget.data.l10n.subtitle4, 'subtitle': widget.data.l10n.subtitle4,
'picture': 'assets/images/$carousel/slide4.webp', 'picture': '',
'background': 'assets/images/carousel/slide4_background.webp', 'background': 'assets/images/carousel/slide4_background.webp',
'foreground': '', 'foreground': '',
'rotation': 180.00, 'rotation': 180.00,
'scale': 1.35, 'scale': 1.35,
'x': -5.00, 'x': -5.00,
'y': 80.00, 'y': 80.00,
'cards': true,
}, },
//TODO: implement simulated physics so that the little boxes can move like the phone moves
]; ];
return MaterialApp( return MaterialApp(
@@ -211,43 +215,106 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
Expanded( Expanded(
child: CarouselSlider.builder( child: CarouselSlider.builder(
itemCount: slides.length, itemCount: slides.length,
itemBuilder: (context, index, realIndex) => Column( itemBuilder: (context, index, realIndex) {
crossAxisAlignment: CrossAxisAlignment.start, final isCards = slides[index]['cards'] == true;
mainAxisAlignment: MainAxisAlignment.start,
children: [ return Column(
Padding( crossAxisAlignment: CrossAxisAlignment.start,
padding: EdgeInsetsGeometry.symmetric( mainAxisAlignment: MainAxisAlignment.start,
horizontal: paddingWidthHorizontal, children: [
), Padding(
child: Column( padding: EdgeInsetsGeometry.symmetric(
crossAxisAlignment: CrossAxisAlignment.start, horizontal: paddingWidthHorizontal,
mainAxisAlignment: MainAxisAlignment.start, ),
children: [ child: Column(
Text( crossAxisAlignment: CrossAxisAlignment.start,
slides[index]['title']! as String, mainAxisAlignment: MainAxisAlignment.start,
style: appStyle.fonts.H_18px.copyWith( children: [
color: appStyle.colors.textPrimary, Text(
slides[index]['title']! as String,
style: appStyle.fonts.H_18px.copyWith(
color: appStyle.colors.textPrimary,
),
softWrap: true,
overflow: TextOverflow.visible,
), ),
softWrap: true, const SizedBox(height: 8),
overflow: TextOverflow.visible, Text(
), slides[index]['subtitle']! as String,
const SizedBox(height: 8), style: appStyle.fonts.B_16R.copyWith(
Text( color: appStyle.colors.textPrimary,
slides[index]['subtitle']! as String, ),
style: appStyle.fonts.B_16R.copyWith( softWrap: true,
color: appStyle.colors.textPrimary, overflow: TextOverflow.visible,
), ),
softWrap: true, ],
overflow: TextOverflow.visible, ),
),
],
), ),
), if (isCards)
Stack( Expanded(
children: [ child: Stack(
slides[index]['background']! == '' children: [
? SizedBox() if ((slides[index]['background'] ?? '')
: ClipRect( .toString()
.isNotEmpty)
ClipRect(
clipper: ImageClipper(
MediaQuery.of(context),
),
child: Transform.rotate(
angle:
-math.pi /
(slides[index]['rotation']!
as double),
child: Transform.translate(
offset: Offset(
slides[index]['x'] as double,
slides[index]['y'] as double,
),
child: SizedBox(
width: MediaQuery.of(
context,
).size.width,
child: Transform.scale(
scale:
slides[index]['scale']
as double,
child: Image.asset(
slides[index]['background']!
as String,
bundle: DefaultAssetBundle.of(
context,
),
fit: BoxFit.contain,
width: double.infinity,
),
),
),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8),
child: LayoutBuilder(
builder: (ctx, constraints) =>
_FloatingCardsSlide(
width: constraints.maxWidth,
height: constraints.maxHeight,
topPadding: 30,
),
),
),
],
),
)
else
Stack(
children: [
if ((slides[index]['background'] ?? '')
.toString()
.isNotEmpty)
ClipRect(
clipper: ImageClipper( clipper: ImageClipper(
MediaQuery.of(context), MediaQuery.of(context),
), ),
@@ -283,31 +350,44 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
), ),
), ),
), ),
Column( Column(
children: [ children: [
SizedBox(height: 73), const SizedBox(height: 73),
Padding( Padding(
padding: EdgeInsetsGeometry.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 18, horizontal: 10,
), // 10 padding each side
child: SizedBox( // LayoutBuilder receives the inset width so the physics walls match the edges
width: MediaQuery.of(context).size.width, ),
child: Image( child: SizedBox(
image: PreloadedImageProvider( width: MediaQuery.of(
DefaultAssetBundle.of(context), context,
slides[index]['picture']! as String, ).size.width,
child:
(slides[index]['picture'] ?? '')
.toString()
.isNotEmpty
? Image(
image: PreloadedImageProvider(
DefaultAssetBundle.of(
context,
),
slides[index]['picture']!
as String,
),
fit: BoxFit.cover,
width: double.infinity,
alignment: Alignment.center,
)
: const SizedBox.shrink(),
), ),
fit: BoxFit.cover,
width: double.infinity,
alignment: Alignment.center,
), ),
), ],
), ),
], if ((slides[index]['foreground'] ?? '')
), .toString()
slides[index]['foreground']! == '' .isNotEmpty)
? SizedBox() SizedBox(
: SizedBox(
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
child: ClipRect( child: ClipRect(
clipBehavior: Clip.none, clipBehavior: Clip.none,
@@ -342,10 +422,11 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
), ),
), ),
), ),
], ],
), ),
], ],
), );
},
options: CarouselOptions( options: CarouselOptions(
height: double.infinity, height: double.infinity,
autoPlay: false, autoPlay: false,
@@ -368,8 +449,8 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
colors: [ colors: [
appStyle.colors.background.withAlpha(0), appStyle.colors.background.withAlpha(0),
appStyle.colors.background, appStyle.colors.background,
], // customize colors ],
stops: [0.0, 0.5], // percentages (0% → 50% → 100%) stops: const [0.0, 0.5],
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
), ),
@@ -418,7 +499,7 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
13, 13,
), ),
blurRadius: 2, blurRadius: 2,
offset: Offset(0, 1), offset: const Offset(0, 1),
spreadRadius: 0, spreadRadius: 0,
), ),
], ],
@@ -429,7 +510,9 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: appStyle.fonts.H_16px.copyWith( style: appStyle.fonts.H_16px.copyWith(
color: appStyle.colors.textPrimaryLight, color: appStyle.colors.textPrimaryLight,
fontVariations: [FontVariation("wght", 800)], fontVariations: const [
FontVariation("wght", 800),
],
), ),
), ),
), ),
@@ -439,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,
@@ -459,10 +542,453 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
} }
} }
//card config
class _CardConfig {
final String asset;
final Offset baseOffset;
final double size;
final double parallax;
final double glide;
final double aspect;
const _CardConfig({
required this.asset,
required this.baseOffset,
required this.size,
required this.parallax,
required this.glide,
this.aspect = 0.72,
});
}
//floating card part
class _FloatingCardsSlide extends StatefulWidget {
final double width;
final double height;
final double topPadding;
const _FloatingCardsSlide({
required this.width,
required this.height,
this.topPadding = 0,
});
@override
State<_FloatingCardsSlide> createState() => _FloatingCardsSlideState();
}
class _FloatingCardsSlideState extends State<_FloatingCardsSlide>
with SingleTickerProviderStateMixin {
static const double _friction = 0.878;
static const double _cardHeight =
45; //not in pixels, idk what unit but it works :p
// cap speeds so impacts stay tame
static const double _maxSpeed = _cardHeight * 1.2;
static const double _tiltForce = 0.05;
static const double _bounceDamping = 0.45;
static const double _collisionRestitution = 1.0;
//minimum speed it has to go to trigger a vibration, so the phone doesn't turn into a bomb if the cards are touching
static const double _vibrateSpeedThreshold = _maxSpeed * 0.09;
static const List<_CardConfig> _cards = [
_CardConfig(
asset: 'assets/images/carousel/card1.svg',
baseOffset: Offset(-100, -15),
size: _cardHeight,
parallax: 7.5,
glide: 1.05,
aspect: 4.48,
), // viewBox 215x48
_CardConfig(
asset: 'assets/images/carousel/card2.svg',
baseOffset: Offset(-8, -35),
size: _cardHeight,
parallax: 9.0,
glide: 1.12,
aspect: 2.25,
), // viewBox 108x48
_CardConfig(
asset: 'assets/images/carousel/card3.svg',
baseOffset: Offset(88, -5),
size: _cardHeight,
parallax: 7.0,
glide: 1.0,
aspect: 4.13,
), // viewBox 198x48
_CardConfig(
asset: 'assets/images/carousel/card4.svg',
baseOffset: Offset(-60, 55),
size: _cardHeight,
parallax: 9.5,
glide: 1.15,
aspect: 2.25,
), // viewBox 108x48
_CardConfig(
asset: 'assets/images/carousel/card5.svg',
baseOffset: Offset(52, 80),
size: _cardHeight,
parallax: 10.5,
glide: 1.18,
aspect: 3.02,
), // viewBox 145x48
_CardConfig(
asset: 'assets/images/carousel/card6.svg',
baseOffset: Offset(128, 18),
size: _cardHeight,
parallax: 7.5,
glide: 0.95,
aspect: 5.63,
), // viewBox 270x48
_CardConfig(
asset: 'assets/images/carousel/card7.svg',
baseOffset: Offset(-138, 22),
size: _cardHeight,
parallax: 8.5,
glide: 1.05,
aspect: 3.94,
), // viewBox 189x48
];
late Ticker _ticker;
List<Offset> _positions = [];
List<Offset> _velocities = [];
Offset _tilt = Offset.zero;
Offset? _baseline;
StreamSubscription<AccelerometerEvent>? _accelerometerSub;
Duration? _lastTick;
AccelerometerEvent? _lastAccelEvent;
double _sceneWidth = 0;
double _sceneHeight = 0;
//we needed a cooldown, so that again, the phone doesn't turn into a bomb | EDIT: actually this was fixed by the minimum speed, so we don't need it anymore
// DateTime? _lastVibration;
Offset _clampVel(Offset v) => Offset(
v.dx.clamp(-_maxSpeed, _maxSpeed),
v.dy.clamp(-_maxSpeed, _maxSpeed),
);
void _maybeVibrate() {
// final now = DateTime.now();
// if (_lastVibration != null &&
// now.difference(_lastVibration!).inMilliseconds < 2) //first used 50 but it wasn't good enough, so now it's 2
// return;
// _lastVibration = now;
Vibration.vibrate(duration: 20);
}
@override
void initState() {
super.initState();
_positions = _cards
.map((c) => c.baseOffset + const Offset(0, 300))
.toList();
final rng = math.Random();
_velocities = List.generate(_cards.length, (i) {
final jitter = (rng.nextDouble() - 0.5) * 3.0;
return Offset(jitter, -9.0 * _cards[i].glide);
});
_ticker = createTicker(_tick)..start();
_accelerometerSub = accelerometerEventStream(
samplingPeriod: SensorInterval.gameInterval,
).listen(_handleTilt);
}
void _handleTilt(AccelerometerEvent event) {
_lastAccelEvent = event;
final raw = Offset(event.x, event.y);
_baseline ??= raw;
final rel = raw - _baseline!;
_tilt = Offset(
(_tilt.dx * 0.88 + rel.dx * 0.12).clamp(-6.5, 6.5),
(_tilt.dy * 0.88 + rel.dy * 0.12).clamp(-6.5, 6.5),
);
}
void _tick(Duration elapsed) {
if (_positions.isEmpty || _velocities.isEmpty) return;
if (_lastTick == null) {
_lastTick = elapsed;
return;
}
final dt = ((elapsed - _lastTick!).inMicroseconds / 16667.0).clamp(
0.0,
4.0,
);
_lastTick = elapsed;
if (_sceneWidth == 0 || _sceneHeight == 0) return;
bool collidedThisTick = false;
bool wallHitThisTick = false;
setState(() {
final slope = Offset(-_tilt.dx, _tilt.dy);
final n = _cards.length;
//friction
for (int i = 0; i < n; i++) {
final card = _cards[i];
_velocities[i] += slope * card.parallax * _tiltForce * dt;
_velocities[i] *= math.pow(_friction, dt).toDouble();
_velocities[i] = _clampVel(_velocities[i]);
_positions[i] += _velocities[i] * dt;
}
// card to card collison and wall stuff
//
// running both together in a loop means that when card a is against a
// wall and card b pushes into it, the wall clamp on the a card.
// it goes back through the collision math wizard on the next loop,
// so car b receives the correct reaction instead of having a mating session with card a.
// five times is enough i think, more on slower end devices might cause issues idk tho
final double halfW = _sceneWidth / 2;
final double halfH = _sceneHeight / 2;
for (int iter = 0; iter < 5; iter++) {
//here's the card to card magic, meow i'm going crazy :3
for (int i = 0; i < n - 1; i++) {
for (int j = i + 1; j < n; j++) {
final wi = _cards[i].size * _cards[i].aspect;
final wj = _cards[j].size * _cards[j].aspect;
final hi = _cards[i].size;
final hj = _cards[j].size;
final pi = _positions[i];
final pj = _positions[j];
final overlapX = (wi + wj) / 2 - (pj.dx - pi.dx).abs();
final overlapY = (hi + hj) / 2 - (pj.dy - pi.dy).abs();
if (overlapX > 0 && overlapY > 0) {
if (overlapX < overlapY) {
final sign = pj.dx > pi.dx ? 1.0 : -1.0;
_positions[i] = Offset(pi.dx - sign * overlapX / 2, pi.dy);
_positions[j] = Offset(pj.dx + sign * overlapX / 2, pj.dy);
final viX = _velocities[i].dx;
final vjX = _velocities[j].dx;
if ((viX - vjX) * sign > 0) {
final impulse = (viX - vjX) * _collisionRestitution;
_velocities[i] = _clampVel(
Offset(_velocities[i].dx - impulse, _velocities[i].dy),
);
_velocities[j] = _clampVel(
Offset(_velocities[j].dx + impulse, _velocities[j].dy),
);
if ((viX - vjX).abs() > _vibrateSpeedThreshold) {
collidedThisTick = true;
}
}
} else {
final sign = pj.dy > pi.dy ? 1.0 : -1.0;
_positions[i] = Offset(pi.dx, pi.dy - sign * overlapY / 2);
_positions[j] = Offset(pj.dx, pj.dy + sign * overlapY / 2);
final viY = _velocities[i].dy;
final vjY = _velocities[j].dy;
if ((viY - vjY) * sign > 0) {
final impulse = (viY - vjY) * _collisionRestitution;
_velocities[i] = _clampVel(
Offset(_velocities[i].dx, _velocities[i].dy - impulse),
);
_velocities[j] = _clampVel(
Offset(_velocities[j].dx, _velocities[j].dy + impulse),
);
if ((viY - vjY).abs() > _vibrateSpeedThreshold) {
collidedThisTick = true;
}
}
}
}
}
}
// wall collision, runs every loop, explained before
// feeds back into the next collision loop.
for (int i = 0; i < n; i++) {
final card = _cards[i];
final double cardW = card.size * card.aspect;
final double cardH = card.size;
final double minX = -halfW + cardW / 2;
final double maxX = halfW - cardW / 2;
final double minY = -halfH + cardH / 2;
final double maxY = halfH - cardH / 2;
Offset pos = _positions[i];
double vx = _velocities[i].dx;
double vy = _velocities[i].dy;
// capture incoming velocity components before any bounce damping
final double preVx = vx;
final double preVy = vy;
double impactSpeed = 0.0; // normal to the wall
bool hit = false;
if (pos.dx < minX) {
pos = Offset(minX, pos.dy);
vx = vx.abs() * _bounceDamping;
impactSpeed = math.max(impactSpeed, preVx.abs());
hit = true;
} else if (pos.dx > maxX) {
pos = Offset(maxX, pos.dy);
vx = -vx.abs() * _bounceDamping;
impactSpeed = math.max(impactSpeed, preVx.abs());
hit = true;
}
if (pos.dy < minY) {
pos = Offset(pos.dx, minY);
vy = vy.abs() * _bounceDamping;
impactSpeed = math.max(impactSpeed, preVy.abs());
hit = true;
} else if (pos.dy > maxY) {
pos = Offset(pos.dx, maxY);
vy = -vy.abs() * _bounceDamping;
impactSpeed = math.max(impactSpeed, preVy.abs());
hit = true;
}
_velocities[i] = _clampVel(Offset(vx, vy));
_positions[i] = pos;
// vibrate only when the component toward the wall exceeded threshold
if (hit && impactSpeed > _vibrateSpeedThreshold) {
wallHitThisTick = true;
}
}
}
});
// vibration on collisions (card-card or walls)
if (collidedThisTick || wallHitThisTick) _maybeVibrate();
}
@override
void dispose() {
_ticker.dispose();
_accelerometerSub?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final double totalHeight = widget.height;
_sceneWidth = widget.width;
_sceneHeight = math.max(0, totalHeight - widget.topPadding);
final Offset center = Offset(
_sceneWidth / 2,
widget.topPadding + _sceneHeight / 2,
);
return SizedBox(
width: _sceneWidth,
height: totalHeight,
child: Stack(
clipBehavior: Clip.hardEdge,
children: [
//background rectangle
Positioned(
left: 0,
right: 0,
top: widget.topPadding,
bottom: 0,
child: Container(
decoration: BoxDecoration(
color: appStyle.colors.buttonSecondaryFill, //button color xdddd
borderRadius: BorderRadius.only(
topLeft: Radius.circular(32),
topRight: Radius.circular(32),
),
),
),
),
...List.generate(_cards.length, (i) {
final card = _cards[i];
final double cardWidth = card.size * card.aspect;
final double cardHeight = card.size;
final Offset pos =
center + _positions[i] - Offset(cardWidth / 2, cardHeight / 2);
return Positioned(
left: pos.dx,
top: pos.dy,
width: cardWidth,
height: cardHeight,
child: SvgPicture.asset(
card.asset,
width: cardWidth,
height: cardHeight,
fit: BoxFit.contain,
),
);
}),
if (kDebugMode) _buildDebugOverlay(center),
],
),
);
}
Widget _buildDebugOverlay(Offset center) {
final firstPos = _positions.isNotEmpty ? _positions.first : Offset.zero;
final firstVel = _velocities.isNotEmpty ? _velocities.first : Offset.zero;
final accel = _lastAccelEvent;
final speed = firstVel.distance;
String formatOffset(Offset o) =>
'(${o.dx.toStringAsFixed(2)}, ${o.dy.toStringAsFixed(2)})';
return Positioned(
left: 12,
top: 12,
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.55),
borderRadius: BorderRadius.circular(10),
),
width: math.min(260, _sceneWidth - 24),
child: DefaultTextStyle(
style: appStyle.fonts.B_12R.copyWith(
color: Colors.white,
height: 1.25,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('Sim Debug'),
const SizedBox(height: 4),
Text('tilt: ${formatOffset(_tilt)}'),
if (accel != null)
Text(
'accel: (${accel.x.toStringAsFixed(2)}, ${accel.y.toStringAsFixed(2)}, ${accel.z.toStringAsFixed(2)})',
),
Text('baseline set: ${_baseline != null}'),
Text('scene: ${_sceneWidth.toStringAsFixed(0)} x ${_sceneHeight.toStringAsFixed(0)}'),
Text('center: ${formatOffset(center)}'),
Text('card[0] pos: ${formatOffset(firstPos)}'),
Text('card[0] vel: ${formatOffset(firstVel)} | |v|=${speed.toStringAsFixed(2)}'),
Text('cards: ${_cards.length}'),
],
),
),
),
);
}
}
// this sucks :3 // this sucks :3
class ImageClipper extends CustomClipper<Rect> { class ImageClipper extends CustomClipper<Rect> {
final MediaQueryData _mediaQuery; final MediaQueryData _mediaQuery;
ImageClipper(this._mediaQuery); ImageClipper(this._mediaQuery);
@override @override
@@ -476,7 +1002,5 @@ class ImageClipper extends CustomClipper<Rect> {
} }
@override @override
bool shouldReclip(covariant CustomClipper<Rect> oldClipper) { bool shouldReclip(covariant CustomClipper<Rect> oldClipper) => false;
return false;
}
} }

View File

@@ -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,
), ),
), ),
), ),

View File

@@ -157,6 +157,11 @@ class _SettingsScreenState extends FirkaState<SettingsScreen> {
item.iconType!, item.iconType!,
item.iconData!, item.iconData!,
color: appStyle.colors.accent, color: appStyle.colors.accent,
package:
item.iconType == FirkaIconType.icons ||
item.iconType == FirkaIconType.majesticonsLocal
? 'firka'
: null,
), ),
); );
cardWidgets.add(SizedBox(width: 8)); cardWidgets.add(SizedBox(width: 8));
@@ -224,6 +229,12 @@ class _SettingsScreenState extends FirkaState<SettingsScreen> {
item.iconType!, item.iconType!,
item.iconData!, item.iconData!,
color: appStyle.colors.accent, color: appStyle.colors.accent,
package:
item.iconType == FirkaIconType.icons ||
item.iconType ==
FirkaIconType.majesticonsLocal
? 'firka'
: null,
), ),
SizedBox(width: 4), SizedBox(width: 4),
], ],
@@ -265,6 +276,12 @@ class _SettingsScreenState extends FirkaState<SettingsScreen> {
item.iconType!, item.iconType!,
item.iconData!, item.iconData!,
color: appStyle.colors.accent, color: appStyle.colors.accent,
package:
item.iconType == FirkaIconType.icons ||
item.iconType ==
FirkaIconType.majesticonsLocal
? 'firka'
: null,
), ),
SizedBox(width: 4), SizedBox(width: 4),
], ],
@@ -912,6 +929,7 @@ class _SettingsScreenState extends FirkaState<SettingsScreen> {
showModalBottomSheet<void>( showModalBottomSheet<void>(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
enableDrag: false,
builder: (BuildContext context) { builder: (BuildContext context) {
return LoginWebviewWidget(widget.data); return LoginWebviewWidget(widget.data);
}, },
@@ -1067,6 +1085,12 @@ class _SettingsScreenState extends FirkaState<SettingsScreen> {
item.iconType!, item.iconType!,
item.iconData!, item.iconData!,
color: appStyle.colors.accent, color: appStyle.colors.accent,
package:
item.iconType == FirkaIconType.icons ||
item.iconType ==
FirkaIconType.majesticonsLocal
? 'firka'
: null,
), ),
SizedBox(width: 8), SizedBox(width: 8),
], ],

View File

@@ -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,
),
),
),
],
), ),
), ),
); );

View File

@@ -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,
),
),
]
: [],
), ),
), ),
), ),

View File

@@ -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,
), ),
), ),
], ],

View 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),
],
),
),
);
}
}

View File

@@ -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,26 +47,64 @@ 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)];
}
}
List<FlSpot> _smoothSpots(List<FlSpot> input) {
if (input.length < 3) return input;
final smoothed = <FlSpot>[];
for (var i = 0; i < input.length; i++) {
if (i == 0 || i == input.length - 1) {
smoothed.add(input[i]);
continue;
}
final prev = input[i - 1].y;
final curr = input[i].y;
final next = input[i + 1].y;
final blended = (0.25 * prev) + (0.5 * curr) + (0.25 * next);
smoothed.add(FlSpot(input[i].x, blended));
}
return smoothed;
} }
@override @override
@@ -103,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,
); );
} }
@@ -153,88 +176,73 @@ 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);
} }
LineChartData avgData() { LineChartData avgData() {
var firstX = spots.first.x; final smoothedSpots = _smoothSpots(spots);
var lastX = spots.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(
@@ -243,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),
@@ -293,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,
@@ -332,39 +323,44 @@ 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(
spots: spots, spots: smoothedSpots,
isCurved: true, isCurved: true,
curveSmoothness: 0.35, curveSmoothness: 0.5,
showingIndicators: _touchedIndex != null ? [_touchedIndex!] : [], showingIndicators: _touchedIndex != null ? [_touchedIndex!] : [],
gradient: LinearGradient( gradient: LinearGradient(
colors: [for (final s in spots) colorForY(s.y)], colors: [for (final s in smoothedSpots) colorForY(s.y)],
), ),
barWidth: 5, barWidth: 5,
isStrokeCapRound: true, isStrokeCapRound: true,
@@ -373,7 +369,8 @@ class _GradeChartState extends State<GradeChart> {
show: true, show: true,
gradient: LinearGradient( gradient: LinearGradient(
colors: [ colors: [
for (final s in spots) colorForY(s.y).withValues(alpha: 0.1), for (final s in smoothedSpots)
colorForY(s.y).withValues(alpha: 0.1),
], ],
), ),
), ),
@@ -387,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) {
@@ -397,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),
); );
} }
} }

View File

@@ -1,3 +1,4 @@
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';
@@ -11,8 +12,14 @@ import 'package:firka/ui/theme/style.dart';
class GradeSummaryBar extends StatefulWidget { class GradeSummaryBar extends StatefulWidget {
final List<Grade> grades; final List<Grade> grades;
final AppLocalizations l10n; final AppLocalizations l10n;
final bool showAverage;
const GradeSummaryBar({super.key, required this.grades, required this.l10n}); const GradeSummaryBar({
super.key,
required this.grades,
required this.l10n,
this.showAverage = false,
});
@override @override
State<GradeSummaryBar> createState() => _GradeSummaryBarState(); State<GradeSummaryBar> createState() => _GradeSummaryBarState();
@@ -23,93 +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 = [ final averageText = widget.showAverage
appStyle.colors.grade1, ? (widget.grades.getAverage() ?? 0).toStringAsFixed(2)
appStyle.colors.grade2, : '';
appStyle.colors.grade3, final bar = ClipRRect(
appStyle.colors.grade4, borderRadius: BorderRadius.circular(8),
appStyle.colors.grade5, child: Row(
]; children: List.generate(5, (i) {
final totalCounted = countsByGrade.reduce((a, b) => a + b); 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.l10n.gradesCount(total), ? '${widget.l10n.gradesCount(total)} ($averageText)'
style: appStyle.fonts.B_16SB.apply( : widget.l10n.gradesCount(total),
color: appStyle.colors.textPrimary, style: appStyle.fonts.B_16SB.apply(
),
),
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,
),
),
],
);
}),
),
], ],
), ],
), ),
), ),
); );

View File

@@ -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,
),
),
],
),
],
),
); );
} }
} }

View File

@@ -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);
},
);
}
}

View File

@@ -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,
),
),
],
),
],
),
],
);
}
}

View 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);
},
);
}
}

View File

@@ -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!.shortenName(),
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,

View File

@@ -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!.shortenName(),
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,
], ],
); ),
} ),
);
} }
} }

View 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();
}

View File

@@ -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(

View File

@@ -5,8 +5,12 @@ 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:isar_community/isar.dart'; import 'package:isar_community/isar.dart';
import 'package:majesticons_flutter/majesticons_flutter.dart';
import 'package:webview_flutter/webview_flutter.dart'; import 'package:webview_flutter/webview_flutter.dart';
import 'package:firka/services/watch_sync_helper.dart'; import 'package:firka/services/watch_sync_helper.dart';
@@ -39,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,
); );
@@ -54,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()
@@ -71,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;
@@ -194,30 +205,77 @@ class _LoginWebviewWidgetState extends FirkaState<LoginWebviewWidget>
final safePadding = mediaQuery.padding; final safePadding = mediaQuery.padding;
return Material( return Material(
color: appStyle.colors.card, color: appStyle.colors.background, //why was this card? :sob:
child: Padding( child: Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
top: 61 + safePadding.top, top: 61 + safePadding.top,
left: 12, left: 12,
right: 12, right: 12,
bottom: 0 + safePadding.bottom, bottom: safePadding.bottom,
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Row(
widget.data.l10n.runningInDomainBrowser, children: [
style: appStyle.fonts.B_16R.copyWith( Padding(
color: appStyle.colors.textPrimary, 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,
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: 8), const SizedBox(height: 22),
Expanded( Expanded(
child: ClipRRect( child: ClipRRect(
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(
@@ -228,21 +286,16 @@ 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(
child: SizedBox( child: Image.asset(
width: 32, "assets/images/logos/loading.gif",
height: 32, width: 50,
child: CircularProgressIndicator( height: 50,
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(
appStyle.colors.accent,
),
),
), ),
), ),
), ),
@@ -253,6 +306,117 @@ class _LoginWebviewWidgetState extends FirkaState<LoginWebviewWidget>
), ),
), ),
), ),
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: _displayHost,
style: appStyle.fonts.B_14R.copyWith(
fontSize: 16,
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,
),
),
),
),
),
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),
], ],
), ),
), ),

View File

@@ -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)]);
}
}

View File

@@ -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,8 @@ class TimeTableDayWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget noLessonsWidget = SizedBox();
List<Widget> ttBody = List.empty(growable: true);
if (lessons.isEmpty) { if (lessons.isEmpty) {
noLessonsWidget = Column( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
@@ -60,7 +59,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,
), ),
@@ -69,59 +90,93 @@ class TimeTableDayWidget extends StatelessWidget {
), ),
], ],
); );
} else {
for (var i = 0; i < events.length; i++) {
var event = events[i];
ttBody.add(
FirkaCard(
left: [
Text(
event.name,
style: appStyle.fonts.B_16R.apply(
color: appStyle.colors.textPrimary,
),
),
],
),
);
}
for (var i = 0; i < lessons.length; i++) {
var lesson = lessons[i];
Lesson? nextLesson = lessons.length > i + 1 ? lessons[i + 1] : null;
ttBody.add(
LessonWidget(
data,
week,
day,
lessons.getLessonNo(lesson),
lesson,
tests.firstWhereOrNull(
(test) => test.lessonNumber == lesson.lessonNumber,
),
nextLesson,
),
);
}
} }
return SizedBox( List<Widget> ttLessons = List.empty(growable: true);
width: MediaQuery.of(context).size.width / 1.1, for (final event in events) {
child: ttBody.isEmpty ttLessons.add(
? noLessonsWidget FirkaCard.single(
: Padding( margin: EdgeInsets.zero,
padding: const EdgeInsets.only( padding: EdgeInsets.all(16),
top: 70 + 16 + 20, child: Text(
left: 4, event.name,
right: 4, style: appStyle.fonts.B_16R.apply(
), color: appStyle.colors.textPrimary,
child: SingleChildScrollView( ),
child: Column( ),
mainAxisAlignment: MainAxisAlignment.start, ),
crossAxisAlignment: CrossAxisAlignment.start, );
children: [...ttBody, SizedBox(height: 24)], }
),
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(
color: appStyle.colors.cardTranslucent,
margin: EdgeInsets.all(0),
padding: EdgeInsets.symmetric(vertical: 11, horizontal: 16),
shadow: false,
left: [
Text(
initData.l10n.breakTxt,
style: appStyle.fonts.B_14SB.copyWith(
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,
),
),
],
),
);
}
return Padding(
padding: const EdgeInsets.only(top: 70 + 16 + 20, left: 20, right: 20),
child: ListView.separated(
separatorBuilder: (context, index) => SizedBox(height: 16),
itemBuilder: (context, index) {
if (ttLessons.length == index) {
return SizedBox(height: 55);
}
return ttLessons[index];
},
itemCount: ttLessons.length + 1,
),
); );
} }
} }

Some files were not shown because too many files have changed in this diff Show More