1
0

Compare commits

...

132 Commits
1.7.1 ... 1.9.0

Author SHA1 Message Date
9a8fb593c0 Merge branch 'release/1.9.0' 2023-01-01 21:57:47 +01:00
f4c6e0ad1b Version 1.9.0 2023-01-01 21:57:39 +01:00
b30b7c3318 New Crowdin updates (#2068) 2023-01-01 21:52:46 +01:00
897eac050a Refactor student selection screen (#2087) 2023-01-01 20:26:32 +01:00
83974b6550 Fix NPE when trying to remove a message from mailbox that doesn't match any student (#2090) 2023-01-01 20:21:28 +01:00
7efd106658 Update date in LICENSE file (#2089) 2023-01-01 12:16:09 +01:00
9cedab979c Bump robolectric from 4.9 to 4.9.1 (#2088) 2022-12-26 20:07:25 +00:00
510e2d5b88 Fix html entities parsing in school announcements (#2086) 2022-12-25 04:40:58 +01:00
63d6a0b325 Merge branch 'hotfix/1.8.3' into develop 2022-12-21 13:30:26 +01:00
da0943b319 Merge branch 'hotfix/1.8.3' 2022-12-21 13:29:38 +01:00
67875b1a9a Version 1.8.3 2022-12-21 13:29:32 +01:00
aa3d7e37fc Automatically show current student mailbox only when there is only one mailbox available (#2085)
* Automatically show current student mailbox only when there is only one mailbox for this student available

* Fallback to 'unknown' mailbox key if there is no matching mailbox to message
2022-12-21 13:15:28 +01:00
ede5914d70 Automatically show current student mailbox only when there is only one mailbox available (#2085)
* Automatically show current student mailbox only when there is only one mailbox for this student available

* Fallback to 'unknown' mailbox key if there is no matching mailbox to message
2022-12-21 00:31:29 +01:00
09c968f273 Merge branch 'hotfix/1.8.2' into develop 2022-12-21 00:15:02 +01:00
345b580601 Merge branch 'hotfix/1.8.2' 2022-12-21 00:07:57 +01:00
61240777cf Version 1.8.2 2022-12-21 00:00:03 +01:00
7588345b6d Bump sdk 2022-12-20 23:35:47 +01:00
2fa26c37a9 Revert "Fix app name in french (#2072)"
This reverts commit 277ffd22be.
2022-12-20 21:55:46 +01:00
c34c63c128 Add support for new ADFS light instances (#2084)
* Update known symbols

* Update resman host details

* Add adfslight from tomaszowmazowiecki

* Bump sdk to 1.8.2-SNAPSHOT

* Add migration 54 with tests

* Close db in migration tests

* Run tests workflow on every pull request
2022-12-20 16:06:55 +01:00
a26dadb224 Fix a typo in excuse message subject (#2071) 2022-12-20 15:59:36 +01:00
277ffd22be Fix app name in french (#2072) 2022-12-20 15:59:36 +01:00
f1479d489b Bump play-services-ads from 21.3.0 to 21.4.0 (#2083) 2022-12-20 12:28:26 +00:00
fba4e85311 Bump firebase-bom from 31.1.0 to 31.1.1 (#2079) 2022-12-14 21:52:41 +00:00
4a5991ade4 Bump hianalytics from 6.9.0.300 to 6.9.0.301 (#2080) 2022-12-14 21:43:04 +00:00
a735c378f1 Bump fragment-ktx from 1.5.4 to 1.5.5 (#2077) 2022-12-14 21:42:21 +00:00
217ebfc549 Fix app name in french (#2072) 2022-12-14 22:41:57 +01:00
df5155f1c7 Fix a typo in excuse message subject (#2071) 2022-12-05 15:45:07 +01:00
b93c0222a2 Fix conference details strings (#2070) 2022-12-05 15:44:30 +01:00
083ca34f1b Bump hianalytics from 6.8.0.300 to 6.9.0.300 (#2069) 2022-12-05 13:32:46 +00:00
5d5dfd4eb4 Bump mockk from 1.13.2 to 1.13.3 (#2067) 2022-12-01 18:38:59 +00:00
429fdfa4a0 Update project to Android SDK 33 (#2011) 2022-12-01 19:02:25 +01:00
302d723cfb Suppress menu deprecations (#2031) 2022-12-01 18:14:28 +01:00
8f50ee82b3 Change fakelog to https (#2063) 2022-11-28 19:50:14 +01:00
9dc1220496 Bump about_libraries from 10.5.1 to 10.5.2 (#2066) 2022-11-28 18:36:14 +00:00
ae39bd94e5 Bump hilt_version from 2.44.1 to 2.44.2 (#2058) 2022-11-22 20:42:38 +00:00
85ce23845f Bump firebase-bom from 31.0.3 to 31.1.0 (#2057) 2022-11-21 20:51:23 +00:00
890d60811b Bump agcp from 1.7.3.301 to 1.7.3.302 (#2059) 2022-11-21 20:51:07 +00:00
49e68f5c8b Bump agconnect-crash from 1.7.3.300 to 1.7.3.302 (#2060) 2022-11-21 20:50:47 +00:00
1df4679db8 Merge branch 'release/1.8.1' into develop 2022-11-20 00:05:47 +01:00
e03aae2d56 Merge branch 'release/1.8.1' 2022-11-20 00:05:38 +01:00
9c60ce688b Version 1.8.1 2022-11-20 00:05:34 +01:00
fdce2cf477 Improve error handling in horizontal tile on dashboard (#2053) 2022-11-19 22:50:51 +01:00
650cbd5a10 Add info about optional ads in README (#2054) 2022-11-19 14:11:26 +01:00
b160367744 Add support for reversed student name mailbox matching (#2051)
* Add support for reversed student name mailbox matching

* Add additional log stmt to mailbox matching in all mailbox load

* Revert changes in mapper
2022-11-19 08:52:35 +01:00
6c115fb915 Fallback to subject name from timetable when attendance item doesn't have it (#2052)
* Fallback to subject name from timetable when attendance item doesn't have it

* Fix tests

* Add some unit tests
2022-11-18 16:32:04 +01:00
7a408899df Merge branch 'release/1.8.0' into develop 2022-11-16 20:31:18 +01:00
4bce35f810 Merge branch 'release/1.8.0' 2022-11-16 20:31:09 +01:00
2d83218f61 Version 1.8.0 2022-11-16 20:31:02 +01:00
d3e276d6fc New Crowdin updates (#2049)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
2022-11-16 20:13:48 +01:00
51a1097bb4 Add mailbox chooser to messages (#2002) 2022-11-16 13:46:47 +01:00
db4f172fb8 Fix unread status in sent messages (#2048) 2022-11-16 12:54:55 +01:00
4d49e956b8 Bump junit from 1.1.3 to 1.1.4 (#2043) 2022-11-14 20:48:12 +00:00
b8296ac02f Bump kotlin_version from 1.7.20 to 1.7.21 (#2042) 2022-11-14 20:31:26 +00:00
d6385e8cdd Bump core from 1.4.0 to 1.5.0 (#2045) 2022-11-14 20:31:02 +00:00
885319a885 Bump hilt_version from 2.44 to 2.44.1 (#2044) 2022-11-14 18:36:20 +00:00
fded5007c1 Bump firebase-bom from 31.0.2 to 31.0.3 (#2041) 2022-11-14 18:22:25 +00:00
66ff14f719 Bump agcp from 1.7.3.300 to 1.7.3.301 (#2046) 2022-11-14 18:21:35 +00:00
1257dc63d3 Bump runner from 1.4.0 to 1.5.1 (#2047) 2022-11-14 18:21:17 +00:00
50b6d380b6 Fix unexpected error in support ad when no internet (#2030) 2022-11-02 16:44:05 +01:00
62b7d42a73 New Crowdin updates (#1966) 2022-11-01 20:57:05 +01:00
21fe209246 Bump firebase-bom from 31.0.1 to 31.0.2 (#2032) 2022-11-01 19:51:28 +00:00
02cd4e4e06 Bump sonarqube-gradle-plugin from 3.4.0.2513 to 3.5.0.2730 (#2033) 2022-11-01 19:50:59 +00:00
86fe2b61cb Bump agcp from 1.7.2.300 to 1.7.3.300 (#2034) 2022-11-01 19:50:37 +00:00
4113bd9b53 Bump agconnect-crash from 1.7.2.300 to 1.7.3.300 (#2035) 2022-11-01 19:50:17 +00:00
d924902dac Bump CircularImageView from 4.2.0 to 4.3.0 (#2036) 2022-11-01 19:49:56 +00:00
b269360ecb Langs placement in README adjustments (#2029) 2022-10-30 03:00:39 +01:00
ffd5addadb Add Crowdin badges to README (#2025) 2022-10-28 11:10:35 +02:00
c5e2b18695 Fix SSL certificate out-of-date detection (#2028) 2022-10-28 11:10:05 +02:00
515a3973b7 Use text color, font face and red dot to differentiate unread messages (#2027) 2022-10-28 11:09:38 +02:00
7bee10d5ce Hide room view in timetable item if there is no room in API (#2026) 2022-10-28 11:08:40 +02:00
22a4f509dc Add installation id to crashlytics and bug report emails (#2024) 2022-10-27 12:41:33 +02:00
3925a6261b Add missing CS and SK links in German README (#2018) 2022-10-26 22:27:37 +02:00
49b383fbe5 Bump firebase-bom from 31.0.0 to 31.0.1 (#2019) 2022-10-26 18:22:53 +00:00
4a484dc2ce Bump fragment-ktx from 1.5.3 to 1.5.4 (#2020) 2022-10-26 18:22:34 +00:00
a14c4b489b Bump material from 1.6.1 to 1.7.0 (#2022) 2022-10-26 18:22:16 +00:00
e91cd18804 Bump firebase-bom from 30.5.0 to 31.0.0 (#2013) 2022-10-18 19:41:15 +00:00
4c24363599 Bump about_libraries from 10.5.0 to 10.5.1 (#2012) 2022-10-18 19:40:28 +00:00
e20c232f8f Bump kotlinx-serialization-json from 1.4.0 to 1.4.1 (#2014) 2022-10-18 19:39:54 +00:00
1f11eea9b5 Bump gradle from 7.3.0 to 7.3.1 (#2015) 2022-10-18 19:39:36 +00:00
42f9a00e8c Bump play-services-ads from 21.2.0 to 21.3.0 (#2016) 2022-10-18 19:39:16 +00:00
ad487e680c Fix grade weight text truncation in grade dialog with large font set (#2009) 2022-10-05 22:25:09 +02:00
3f431022a5 Bump about_libraries from 10.4.0 to 10.5.0 (#2005) 2022-10-04 08:08:21 +00:00
cd037f0ce0 Bump kotlin_version from 1.7.10 to 1.7.20 (#2003) 2022-10-04 07:58:58 +00:00
37f7f21a03 Reorder action buttons on the message preview screen to hide the forward button in overflow menu (#2000) 2022-10-04 09:51:30 +02:00
c653039590 Bump coil from 2.2.1 to 2.2.2 (#2004) 2022-10-04 07:50:05 +00:00
95a90a7a79 Bump mockk from 1.13.1 to 1.13.2 (#2006) 2022-10-04 07:49:41 +00:00
4dc80595ac Bump robolectric from 4.8.2 to 4.9 (#2007) 2022-10-04 07:49:22 +00:00
8114a2376e Bump gradle from 7.2.2 to 7.3.0 (#1985) 2022-09-28 21:43:45 +00:00
a523850216 Bump annotation from 1.4.0 to 1.5.0 (#1998) 2022-09-28 23:34:06 +02:00
354f51dd70 Fix student average calculation error in grade statistics (#1981)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
2022-09-28 23:33:05 +02:00
b271c12ebc Bump hilt_version from 2.43.2 to 2.44 (#1994) 2022-09-28 20:28:31 +00:00
8ca41b5ba3 Bump hianalytics from 6.7.0.300 to 6.8.0.300 (#1995) 2022-09-28 20:28:01 +00:00
edbe45332a Bump mockk from 1.12.8 to 1.13.1 (#1996) 2022-09-28 20:27:43 +00:00
1bbd249275 Bump fragment-ktx from 1.5.2 to 1.5.3 (#1997) 2022-09-28 20:25:58 +00:00
5148ff291b Bump google-services from 4.3.13 to 4.3.14 (#1992) 2022-09-21 20:59:41 +00:00
a1dc00af42 Bump agcp from 1.7.1.300 to 1.7.2.300 (#1989) 2022-09-21 20:49:36 +00:00
f1db993fee Bump agconnect-crash from 1.7.1.300 to 1.7.2.300 (#1990) 2022-09-21 20:40:36 +00:00
4f0519552e Bump firebase-crashlytics-gradle from 2.9.1 to 2.9.2 (#1988) 2022-09-21 20:39:29 +00:00
3625c5c518 Bump firebase-bom from 30.4.1 to 30.5.0 (#1991) 2022-09-21 20:39:08 +00:00
afbfb9761f Bump mockk from 1.12.7 to 1.12.8 (#1986) 2022-09-21 20:38:55 +00:00
a5c636853a Bump coil from 2.2.0 to 2.2.1 (#1973) 2022-09-14 11:19:05 +00:00
d5d45ed1ba Bump play-services-ads from 21.1.0 to 21.2.0 (#1972) 2022-09-14 11:18:29 +00:00
d3f869c6c2 Bump firebase-bom from 30.4.0 to 30.4.1 (#1971) 2022-09-14 11:18:09 +00:00
46c29c438e Bump appcompat from 1.5.0 to 1.5.1 (#1975) 2022-09-14 11:17:19 +00:00
73a7255d3a Bump firebase-bom from 30.3.2 to 30.4.0 (#1968) 2022-09-05 19:49:30 +00:00
c7af85e0e1 Merge branch 'release/1.7.5' into develop 2022-09-02 21:31:39 +02:00
afc16e3d17 Merge branch 'release/1.7.5' 2022-09-02 21:31:31 +02:00
59f6f5c212 Version 1.7.5 2022-09-02 21:31:27 +02:00
86f8763e69 Display lesson number in attendance notification if subject is blank (#1965) 2022-09-02 21:30:30 +02:00
157becb017 Fix matching mailboxes when there is more than one space between words (#1964) 2022-09-02 20:19:19 +02:00
83ca9a7060 Merge branch 'release/1.7.4' into develop 2022-09-01 17:57:30 +02:00
1033be4503 Merge branch 'release/1.7.4' 2022-09-01 17:57:24 +02:00
e67066f3ae Version 1.7.4 2022-09-01 17:57:20 +02:00
bc22808b0e Add support for matching last kingergarten semester if there is no any current (#1962) 2022-09-01 17:48:46 +02:00
e05abb3539 Fix showing empty view in grade details when there is no grades (#1963) 2022-09-01 17:48:03 +02:00
6153c7b97d Add support for match mailboxes with more different names (#1961) 2022-09-01 14:55:00 +02:00
e574e5e2ec Merge branch 'release/1.7.3' into develop 2022-08-31 19:31:39 +02:00
e6571a1dfc Merge branch 'release/1.7.3' 2022-08-31 19:31:33 +02:00
d566de0282 Version 1.7.3 2022-08-31 19:31:28 +02:00
558db061f5 Fix marking message as read (#1960)
* Fix marking message as read

* Update sdk

* Update sdk

* Fix tests

* Use many recipients strings instead of first recipient
2022-08-31 18:31:12 +02:00
190f40ede8 Merge branch 'release/1.7.2' into develop 2022-08-30 13:34:28 +02:00
4b795d6ef5 Merge branch 'release/1.7.2' 2022-08-30 13:34:20 +02:00
d139bd5b14 Version 1.7.2 2022-08-30 13:34:10 +02:00
bf34cb0c1e New Crowdin updates (#1953)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
2022-08-30 12:11:32 +02:00
eed091aad2 Update row in mailboxes table on primary key conflict (#1954) 2022-08-30 09:07:24 +02:00
535206056d Fix selecting student mailbox based on studentName field (#1958) 2022-08-30 09:07:07 +02:00
f2cb3b4f9e Change message draft key in shared preferences (#1959) 2022-08-30 09:06:29 +02:00
54372e0a55 Bump kotlinx-serialization-json from 1.3.3 to 1.4.0 (#1950) 2022-08-29 13:40:14 +00:00
5c17c38d1d Bump mockk from 1.12.5 to 1.12.7 (#1955) 2022-08-29 13:30:56 +00:00
f68a8e4215 Bump robolectric from 4.8.1 to 4.8.2 (#1956) 2022-08-29 13:30:28 +00:00
70c2cb7dbf Bump desugar_jdk_libs from 1.1.6 to 1.1.8 (#1957) 2022-08-29 13:30:00 +00:00
7f6fd60821 Merge branch 'release/1.7.1' into develop 2022-08-23 00:56:29 +02:00
192 changed files with 13098 additions and 1340 deletions

View File

@ -2,10 +2,12 @@ name: Tests
on:
push:
branches: [ master, develop ]
branches:
- master
- develop
- 'hotfix/**'
tags: [ '*' ]
pull_request:
branches: [ master, develop ]
jobs:

View File

@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2022 Wulkanowy
Copyright 2023 Wulkanowy
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -1,18 +1,13 @@
[English version of README](README.en.md)
[Deutsche Version von README](README.de.md)
[Polska wersja README](README.md)
[Slovenská verzia README](README.sk.md)
Česká verze / [Deutsche Version](README.de.md) / [English version](README.en.md) / [Polska wersja](README.md) / [Slovenská verzia](README.sk.md)
# Wulkanowy
[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/wulkanowy/wulkanowy/Tests/develop?style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions)
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/wulkanowy/wulkanowy/test.yml?branch=develop&style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions)
[![Codecov](https://img.shields.io/codecov/c/github/wulkanowy/wulkanowy/master.svg?style=flat-square)](https://codecov.io/gh/wulkanowy/wulkanowy)
[![Discord](https://img.shields.io/discord/390889354199040011.svg?style=flat-square)](https://discord.gg/vccAQBr)
[![F-Droid](https://img.shields.io/f-droid/v/io.github.wulkanowy.svg?style=flat-square)](https://f-droid.org/packages/io.github.wulkanowy/)
[![Last release](https://img.shields.io/github/release/wulkanowy/wulkanowy.svg?logo=github&style=flat-square)](https://github.com/wulkanowy/wulkanowy/releases)
[![Crowdin](https://badges.crowdin.net/wulkanowy2/localized.svg)](https://translate.wulkanowy.net.pl)
Neoficiální klient deníku VULCAN UONET+ pro žáka a rodiče
@ -39,7 +34,7 @@ Neoficiální klient deníku VULCAN UONET+ pro žáka a rodiče
* podpora více účtů s možností přejmenování žáků
* tmavý a černý (AMOLED) motiv
* offline režim
* žádné reklamy
* volitelné reklamy na podporu projektu
## Stáhnout
@ -57,7 +52,7 @@ Aktuální verzi si můžete stáhnout z Google Play, F-Droid nebo Huawei AppGal
Můžete si také stáhnout [vývojovou verzi](https://wulkanowy.github.io/#download), která zahrnuje nové funkce připravované pro příští vydání
## Postaveno s
## Postaveno s pomocí
* [Wulkanowy SDK](https://github.com/wulkanowy/sdk)

View File

@ -1,14 +1,13 @@
[Polska wersja README](README.md)
[English version of README](README.en.md)
[Česká verze](README.cs.md) / Deutsche Version / [English version](README.en.md) / [Polska wersja](README.md) / [Slovenská verzia](README.sk.md)
# Wulkanowy
[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/wulkanowy/wulkanowy/Tests/develop?style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions)
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/wulkanowy/wulkanowy/test.yml?branch=develop&style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions)
[![Codecov](https://img.shields.io/codecov/c/github/wulkanowy/wulkanowy/master.svg?style=flat-square)](https://codecov.io/gh/wulkanowy/wulkanowy)
[![Discord](https://img.shields.io/discord/390889354199040011.svg?style=flat-square)](https://discord.gg/vccAQBr)
[![F-Droid](https://img.shields.io/f-droid/v/io.github.wulkanowy.svg?style=flat-square)](https://f-droid.org/packages/io.github.wulkanowy/)
[![Last release](https://img.shields.io/github/release/wulkanowy/wulkanowy.svg?logo=github&style=flat-square)](https://github.com/wulkanowy/wulkanowy/releases)
[![Crowdin](https://badges.crowdin.net/wulkanowy2/localized.svg)](https://translate.wulkanowy.net.pl)
Inoffizieller Android VULCAN UONET+ Registrierungsclient für Schüler und ihre Eltern
@ -22,7 +21,7 @@ Inoffizieller Android VULCAN UONET+ Registrierungsclient für Schüler und ihre
* Prozentsatz der Anwesenheit
* Prüfungen
* Stundenplan
* Unterricht abgeschlossen
* abgeschlossene Unterrichtsstunden
* Nachrichten
* Hausaufgaben
* Anmerkungen
@ -35,7 +34,7 @@ Inoffizieller Android VULCAN UONET+ Registrierungsclient für Schüler und ihre
* Unterstützung für mehrere Konten mit der Möglichkeit, den Namen des Schülers zu ändern
* dunkles und schwarzes (AMOLED) Thema
* Offline-Modus
* keine Werbung
* optionale Werbungen, die es uns ermöglichen das Projekt zu unterstützen
## Herunterladen

View File

@ -1,18 +1,13 @@
[Polska wersja README](README.md)
[Deutsche Version von README](README.de.md)
[Česká verze README](README.cs.md)
[Slovenská verzia README](README.sk.md)
[Česká verze](README.cs.md) / [Deutsche Version](README.de.md) / English version / [Polska wersja](README.md) / [Slovenská verzia](README.sk.md)
# Wulkanowy
[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/wulkanowy/wulkanowy/Tests/develop?style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions)
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/wulkanowy/wulkanowy/test.yml?branch=develop&style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions)
[![Codecov](https://img.shields.io/codecov/c/github/wulkanowy/wulkanowy/master.svg?style=flat-square)](https://codecov.io/gh/wulkanowy/wulkanowy)
[![Discord](https://img.shields.io/discord/390889354199040011.svg?style=flat-square)](https://discord.gg/vccAQBr)
[![F-Droid](https://img.shields.io/f-droid/v/io.github.wulkanowy.svg?style=flat-square)](https://f-droid.org/packages/io.github.wulkanowy/)
[![Last release](https://img.shields.io/github/release/wulkanowy/wulkanowy.svg?logo=github&style=flat-square)](https://github.com/wulkanowy/wulkanowy/releases)
[![Crowdin](https://badges.crowdin.net/wulkanowy2/localized.svg)](https://translate.wulkanowy.net.pl)
Unofficial android VULCAN UONET+ register client for both students and their parents
@ -39,7 +34,7 @@ Unofficial android VULCAN UONET+ register client for both students and their par
* support for multiple accounts with the ability to rename students
* dark and black (AMOLED) theme
* offline mode
* no ads
* optional ads which allow to support the project
## Download

View File

@ -1,18 +1,13 @@
[English version of README](README.en.md)
[Deutsche Version von README](README.de.md)
[Česká verze README](README.cs.md)
[Slovenská verzia README](README.sk.md)
[Česká verze](README.cs.md) / [Deutsche Version](README.de.md) / [English version](README.en.md) / Polska wersja / [Slovenská verzia](README.sk.md)
# Wulkanowy
[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/wulkanowy/wulkanowy/Tests/develop?style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions)
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/wulkanowy/wulkanowy/test.yml?branch=develop&style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions)
[![Codecov](https://img.shields.io/codecov/c/github/wulkanowy/wulkanowy/master.svg?style=flat-square)](https://codecov.io/gh/wulkanowy/wulkanowy)
[![Discord](https://img.shields.io/discord/390889354199040011.svg?style=flat-square)](https://discord.gg/vccAQBr)
[![F-Droid](https://img.shields.io/f-droid/v/io.github.wulkanowy.svg?style=flat-square)](https://f-droid.org/packages/io.github.wulkanowy/)
[![Last release](https://img.shields.io/github/release/wulkanowy/wulkanowy.svg?logo=github&style=flat-square)](https://github.com/wulkanowy/wulkanowy/releases)
[![Crowdin](https://badges.crowdin.net/wulkanowy2/localized.svg)](https://translate.wulkanowy.net.pl)
Nieoficjalny klient dziennika VULCAN UONET+ dla ucznia i rodzica
@ -39,7 +34,7 @@ Nieoficjalny klient dziennika VULCAN UONET+ dla ucznia i rodzica
* obsługa wielu kont wraz z możliwością zmiany nazwy ucznia
* ciemny i czarny (AMOLED) motyw
* tryb offline
* brak reklam
* opcjonalne reklamy umożliwiające wsparcie projektu
## Pobierz

View File

@ -1,18 +1,13 @@
[English version of README](README.en.md)
[Deutsche Version von README](README.de.md)
[Polska wersja README](README.md)
[Česká verze README](README.cs.md)
[Česká verze](README.cs.md) / [Deutsche Version](README.de.md) / [English version](README.en.md) / [Polska wersja](README.md) / Slovenská verzia
# Wulkanowy
[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/wulkanowy/wulkanowy/Tests/develop?style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions)
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/wulkanowy/wulkanowy/test.yml?branch=develop&style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions)
[![Codecov](https://img.shields.io/codecov/c/github/wulkanowy/wulkanowy/master.svg?style=flat-square)](https://codecov.io/gh/wulkanowy/wulkanowy)
[![Discord](https://img.shields.io/discord/390889354199040011.svg?style=flat-square)](https://discord.gg/vccAQBr)
[![F-Droid](https://img.shields.io/f-droid/v/io.github.wulkanowy.svg?style=flat-square)](https://f-droid.org/packages/io.github.wulkanowy/)
[![Last release](https://img.shields.io/github/release/wulkanowy/wulkanowy.svg?logo=github&style=flat-square)](https://github.com/wulkanowy/wulkanowy/releases)
[![Crowdin](https://badges.crowdin.net/wulkanowy2/localized.svg)](https://translate.wulkanowy.net.pl)
Neoficiálny klient denníka VULCAN UONET+ pre žiaka a rodičov
@ -39,7 +34,7 @@ Neoficiálny klient denníka VULCAN UONET+ pre žiaka a rodičov
* podpora viacerých účtov s možnosťou premenovania žiakov
* tmavý a čierny (AMOLED) motív
* offline režim
* žiadne reklamy
* voliteľné reklamy na podporu projektu
## Stiahnuť
@ -57,7 +52,7 @@ Aktuálnu verziu si môžete stiahnuť z Google Play, F-Droid alebo Huawei AppGa
Môžete si tiež stiahnuť [vývojovú verziu](https://wulkanowy.github.io/#download), ktorá zahrňuje nové funkcie pripravované pre budúce vydanie
## Postavené s
## Postavené s pomocou
* [Wulkanowy SDK](https://github.com/wulkanowy/sdk)

View File

@ -16,15 +16,15 @@ apply from: 'hooks.gradle'
android {
namespace 'io.github.wulkanowy'
compileSdkVersion 32
compileSdkVersion 33
defaultConfig {
applicationId "io.github.wulkanowy"
testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21
targetSdkVersion 32
versionCode 110
versionName "1.7.1"
targetSdkVersion 33
versionCode 119
versionName "1.9.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "app_name", "Wulkanowy"
@ -161,8 +161,8 @@ play {
defaultToAppBundles = false
track = 'production'
releaseStatus = com.github.triplet.gradle.androidpublisher.ReleaseStatus.IN_PROGRESS
userFraction = 0.95d
updatePriority = 5
userFraction = 0.10d
updatePriority = 1
enabled.set(false)
}
@ -181,24 +181,24 @@ ext {
android_hilt = "1.0.0"
room = "2.4.3"
chucker = "3.5.2"
mockk = "1.12.5"
mockk = "1.13.3"
coroutines = "1.6.4"
}
dependencies {
implementation "io.github.wulkanowy:sdk:1.7.0"
implementation "io.github.wulkanowy:sdk:1.9.0"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.6'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.8'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
implementation "androidx.core:core-ktx:1.8.0"
implementation "androidx.core:core-ktx:1.9.0"
implementation 'androidx.core:core-splashscreen:1.0.0'
implementation "androidx.activity:activity-ktx:1.5.1"
implementation "androidx.appcompat:appcompat:1.5.0"
implementation "androidx.fragment:fragment-ktx:1.5.2"
implementation "androidx.annotation:annotation:1.4.0"
implementation "androidx.activity:activity-ktx:1.6.1"
implementation "androidx.appcompat:appcompat:1.5.1"
implementation "androidx.fragment:fragment-ktx:1.5.5"
implementation "androidx.annotation:annotation:1.5.0"
implementation "androidx.preference:preference-ktx:1.2.0"
implementation "androidx.recyclerview:recyclerview:1.2.1"
@ -206,10 +206,10 @@ dependencies {
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
implementation "com.google.android.material:material:1.6.1"
implementation "com.google.android.material:material:1.7.0"
implementation "com.github.wulkanowy:material-chips-input:2.3.1"
implementation "com.github.PhilJay:MPAndroidChart:v3.1.0"
implementation 'com.github.lopspower:CircularImageView:4.2.0'
implementation 'com.github.lopspower:CircularImageView:4.3.0'
implementation "androidx.work:work-runtime-ktx:$work_manager"
playImplementation "androidx.work:work-gcm:$work_manager"
@ -236,21 +236,22 @@ dependencies {
implementation "at.favre.lib:slf4j-timber:1.0.1"
implementation 'com.github.bastienpaulfr:Treessence:1.0.5'
implementation "com.mikepenz:aboutlibraries-core:$about_libraries"
implementation "io.coil-kt:coil:2.2.0"
implementation "io.github.wulkanowy:AppKillerManager:3.0.0"
implementation "io.coil-kt:coil:2.2.2"
implementation "io.github.wulkanowy:AppKillerManager:3.0.1"
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
implementation 'com.fredporciuncula:flow-preferences:1.8.0'
implementation 'org.apache.commons:commons-text:1.10.0'
playImplementation platform('com.google.firebase:firebase-bom:30.3.2')
playImplementation platform('com.google.firebase:firebase-bom:31.1.1')
playImplementation 'com.google.firebase:firebase-analytics-ktx'
playImplementation 'com.google.firebase:firebase-messaging:'
playImplementation 'com.google.firebase:firebase-crashlytics:'
playImplementation 'com.google.android.play:core:1.10.3'
playImplementation 'com.google.android.play:core-ktx:1.8.1'
playImplementation 'com.google.android.gms:play-services-ads:21.1.0'
playImplementation 'com.google.android.gms:play-services-ads:21.4.0'
hmsImplementation 'com.huawei.hms:hianalytics:6.7.0.300'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.7.1.300'
hmsImplementation 'com.huawei.hms:hianalytics:6.9.0.301'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.7.3.302'
releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker"
@ -263,17 +264,17 @@ dependencies {
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines"
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
testImplementation 'org.robolectric:robolectric:4.8.1'
testImplementation "androidx.test:runner:1.4.0"
testImplementation "androidx.test.ext:junit:1.1.3"
testImplementation "androidx.test:core:1.4.0"
testImplementation 'org.robolectric:robolectric:4.9.1'
testImplementation "androidx.test:runner:1.5.1"
testImplementation "androidx.test.ext:junit:1.1.4"
testImplementation "androidx.test:core:1.5.0"
testImplementation "androidx.room:room-testing:$room"
testImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
kaptTest "com.google.dagger:hilt-android-compiler:$hilt_version"
androidTestImplementation "androidx.test:core:1.4.0"
androidTestImplementation "androidx.test:runner:1.4.0"
androidTestImplementation "androidx.test.ext:junit:1.1.3"
androidTestImplementation "androidx.test:core:1.5.0"
androidTestImplementation "androidx.test:runner:1.5.1"
androidTestImplementation "androidx.test.ext:junit:1.1.4"
androidTestImplementation "io.mockk:mockk-android:$mockk"
androidTestImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/colorPrimary" />
<foreground android:drawable="@drawable/ic_launcher_foreground_dev" />
</adaptive-icon>
<monochrome android:drawable="@drawable/ic_launcher_foreground_dev_mono" />
</adaptive-icon>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/colorPrimary" />
<foreground android:drawable="@drawable/ic_launcher_foreground_dev" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -8,15 +8,7 @@ import javax.inject.Singleton
@Suppress("UNUSED_PARAMETER")
class AnalyticsHelper @Inject constructor() {
fun logEvent(name: String, vararg params: Pair<String, Any?>) {
// do nothing
}
fun setCurrentScreen(activity: Activity, name: String?) {
// do nothing
}
fun popCurrentScreen(name: String?) {
// do nothing
}
fun logEvent(name: String, vararg params: Pair<String, Any?>) = Unit
fun setCurrentScreen(activity: Activity, name: String?) = Unit
fun popCurrentScreen(name: String?) = Unit
}

View File

@ -3,26 +3,38 @@ package io.github.wulkanowy.utils
import android.app.Activity
import android.content.Context
import android.os.Bundle
import com.huawei.agconnect.crash.AGConnectCrash
import com.huawei.hms.analytics.HiAnalytics
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.repositories.PreferencesRepository
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AnalyticsHelper @Inject constructor(
@ApplicationContext private val context: Context
@ApplicationContext private val context: Context,
preferencesRepository: PreferencesRepository,
appInfo: AppInfo,
) {
private val analytics by lazy { HiAnalytics.getInstance(context) }
private val connectCrash by lazy { AGConnectCrash.getInstance() }
init {
if (!appInfo.isDebug) {
connectCrash.setUserId(preferencesRepository.installationId)
}
}
fun logEvent(name: String, vararg params: Pair<String, Any?>) {
Bundle().apply {
params.forEach {
if (it.second == null) return@forEach
when (it.second) {
is String, is String? -> putString(it.first, it.second as String)
is Int, is Int? -> putInt(it.first, it.second as Int)
is Boolean, is Boolean? -> putBoolean(it.first, it.second as Boolean)
params.forEach { (key, value) ->
if (value == null) return@forEach
when (value) {
is String -> putString(key, value)
is Int -> putInt(key, value)
is Boolean -> putBoolean(key, value)
}
}
analytics.onEvent(name, this)

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.utils
import android.util.Log
import com.huawei.agconnect.crash.AGConnectCrash
import fr.bipi.tressence.base.FormatterPriorityTree
import fr.bipi.tressence.common.StackTraceRecorder
class CrashLogTree : FormatterPriorityTree(Log.VERBOSE) {
@ -22,16 +23,10 @@ class CrashLogExceptionTree : FormatterPriorityTree(Log.ERROR, ExceptionFilter)
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
if (skipLog(priority, tag, message, t)) return
// Disabled due to a bug in the Huawei library
/*connectCrash.setCustomKey("priority", priority)
connectCrash.setCustomKey("tag", tag.orEmpty())
connectCrash.setCustomKey("message", message)
if (t != null) {
connectCrash.recordException(t)
} else {
connectCrash.recordException(StackTraceRecorder(format(priority, tag, message)))
}*/
}
}
}

View File

@ -8,7 +8,8 @@
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<queries>
<intent>
@ -36,13 +37,14 @@
<application
android:name=".WulkanowyApp"
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="false"
android:theme="@style/WulkanowyTheme"
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
tools:ignore="DataExtractionRules,UnusedAttribute">
<activity
android:name=".ui.modules.splash.SplashActivity"
android:exported="true"

View File

@ -49,8 +49,8 @@ fun <T, U> Resource<T>.mapData(block: (T) -> U) = when (this) {
fun <T> Flow<Resource<T>>.logResourceStatus(name: String, showData: Boolean = false) = onEach {
val description = when (it) {
is Resource.Loading -> "started"
is Resource.Intermediate -> "intermediate data received" + if (showData) " (data: `${it.data}`)" else ""
is Resource.Loading -> "started"
is Resource.Success -> "success" + if (showData) " (data: `${it.data}`)" else ""
is Resource.Error -> "exception occurred: ${it.error}"
}

View File

@ -47,6 +47,7 @@ import javax.inject.Singleton
AutoMigration(from = 44, to = 45),
AutoMigration(from = 46, to = 47),
AutoMigration(from = 47, to = 48),
AutoMigration(from = 51, to = 52),
],
version = AppDatabase.VERSION_SCHEMA,
exportSchema = true
@ -55,7 +56,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() {
companion object {
const val VERSION_SCHEMA = 51
const val VERSION_SCHEMA = 54
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(),
@ -105,6 +106,8 @@ abstract class AppDatabase : RoomDatabase() {
Migration49(),
Migration50(),
Migration51(),
Migration53(),
Migration54(),
)
fun newInstance(

View File

@ -2,11 +2,12 @@ package io.github.wulkanowy.data.db.dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Update
interface BaseDao<T> {
@Insert
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(items: List<T>): List<Long>
@Update

View File

@ -3,15 +3,16 @@ package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Query
import io.github.wulkanowy.data.db.entities.Mailbox
import kotlinx.coroutines.flow.Flow
import javax.inject.Singleton
@Singleton
@Dao
interface MailboxDao : BaseDao<Mailbox> {
@Query("SELECT * FROM Mailboxes WHERE userLoginId = :userLoginId ")
suspend fun loadAll(userLoginId: Int): List<Mailbox>
@Query("SELECT * FROM Mailboxes WHERE email = :email")
suspend fun loadAll(email: String): List<Mailbox>
@Query("SELECT * FROM Mailboxes WHERE userLoginId = :userLoginId AND studentName = :studentName ")
suspend fun load(userLoginId: Int, studentName: String): Mailbox?
@Query("SELECT * FROM Mailboxes WHERE email = :email AND symbol = :symbol AND schoolId = :schoolId")
fun loadAll(email: String, symbol: String, schoolId: String): Flow<List<Mailbox>>
}

View File

@ -16,4 +16,7 @@ interface MessagesDao : BaseDao<Message> {
@Query("SELECT * FROM Messages WHERE mailbox_key = :mailboxKey AND folder_id = :folder ORDER BY date DESC")
fun loadAll(mailboxKey: String, folder: Int): Flow<List<Message>>
@Query("SELECT * FROM Messages WHERE email = :email AND folder_id = :folder ORDER BY date DESC")
fun loadAll(folder: Int, email: String): Flow<List<Message>>
}

View File

@ -13,4 +13,7 @@ interface TimetableDao : BaseDao<Timetable> {
@Query("SELECT * FROM Timetable WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :from AND date <= :end")
fun loadAll(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): Flow<List<Timetable>>
@Query("SELECT * FROM Timetable WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :from AND date <= :end")
fun load(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): List<Timetable>
}

View File

@ -1,20 +1,27 @@
package io.github.wulkanowy.data.db.entities
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
@Parcelize
@Entity(tableName = "Mailboxes")
data class Mailbox(
@PrimaryKey
val globalKey: String,
val email: String,
val symbol: String,
val schoolId: String,
val fullName: String,
val userName: String,
val userLoginId: Int,
val studentName: String,
val schoolNameShort: String,
val type: MailboxType,
)
) : java.io.Serializable, Parcelable
enum class MailboxType {
STUDENT,

View File

@ -9,6 +9,9 @@ import java.time.Instant
@Entity(tableName = "Messages")
data class Message(
@ColumnInfo(name = "email")
val email: String,
@ColumnInfo(name = "message_global_key")
val messageGlobalKey: String,
@ -29,6 +32,12 @@ data class Message(
var unread: Boolean,
@ColumnInfo(name = "read_by")
val readBy: Int?,
@ColumnInfo(name = "unread_by")
val unreadBy: Int?,
@ColumnInfo(name = "has_attachments")
val hasAttachments: Boolean
) : Serializable {

View File

@ -0,0 +1,57 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration53 : Migration(52, 53) {
override fun migrate(database: SupportSQLiteDatabase) {
createMailboxTable(database)
recreateMessagesTable(database)
}
private fun createMailboxTable(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE IF EXISTS Mailboxes")
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `Mailboxes` (
`globalKey` TEXT NOT NULL,
`email` TEXT NOT NULL,
`symbol` TEXT NOT NULL,
`schoolId` TEXT NOT NULL,
`fullName` TEXT NOT NULL,
`userName` TEXT NOT NULL,
`studentName` TEXT NOT NULL,
`schoolNameShort` TEXT NOT NULL,
`type` TEXT NOT NULL,
PRIMARY KEY(`globalKey`)
)""".trimIndent()
)
}
private fun recreateMessagesTable(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE IF EXISTS Messages")
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `Messages` (
`email` TEXT NOT NULL,
`message_global_key` TEXT NOT NULL,
`mailbox_key` TEXT NOT NULL,
`message_id` INTEGER NOT NULL,
`correspondents` TEXT NOT NULL,
`subject` TEXT NOT NULL,
`date` INTEGER NOT NULL,
`folder_id` INTEGER NOT NULL,
`unread` INTEGER NOT NULL,
`read_by` INTEGER,
`unread_by` INTEGER,
`has_attachments` INTEGER NOT NULL,
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`is_notified` INTEGER NOT NULL,
`content` TEXT NOT NULL,
`sender` TEXT,
`recipients` TEXT
)""".trimIndent()
)
}
}

View File

@ -0,0 +1,26 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration54 : Migration(53, 54) {
override fun migrate(database: SupportSQLiteDatabase) {
migrateResman(database)
removeTomaszowMazowieckiStudents(database)
}
private fun migrateResman(database: SupportSQLiteDatabase) {
database.execSQL("""
UPDATE Students SET
scrapper_base_url = 'https://vulcan.net.pl',
login_type = 'ADFSLightScoped',
symbol = 'rzeszowprojekt'
WHERE scrapper_base_url = 'https://resman.pl'
""".trimIndent())
}
private fun removeTomaszowMazowieckiStudents(database: SupportSQLiteDatabase) {
database.execSQL("DELETE FROM Students WHERE symbol = 'tomaszowmazowiecki'")
}
}

View File

@ -3,17 +3,22 @@ package io.github.wulkanowy.data.mappers
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.db.entities.AttendanceSummary
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.sdk.pojo.Attendance as SdkAttendance
import io.github.wulkanowy.sdk.pojo.AttendanceSummary as SdkAttendanceSummary
fun List<SdkAttendance>.mapToEntities(semester: Semester) = map {
fun List<SdkAttendance>.mapToEntities(semester: Semester, lessons: List<Timetable>) = map {
Attendance(
studentId = semester.studentId,
diaryId = semester.diaryId,
date = it.date,
timeId = it.timeId,
number = it.number,
subject = it.subject,
subject = it.subject.ifBlank {
lessons.find { lesson ->
lesson.date == it.date && lesson.number == it.number
}?.subject.orEmpty()
},
name = it.name,
presence = it.presence,
absence = it.absence,

View File

@ -10,9 +10,11 @@ fun List<SdkMailbox>.mapToEntities(student: Student) = map {
globalKey = it.globalKey,
fullName = it.fullName,
userName = it.userName,
userLoginId = student.userLoginId,
studentName = it.studentName,
schoolNameShort = it.schoolNameShort,
type = MailboxType.valueOf(it.type.name),
email = student.email,
symbol = student.symbol,
schoolId = student.schoolSymbol,
)
}

View File

@ -2,21 +2,36 @@ package io.github.wulkanowy.data.mappers
import io.github.wulkanowy.data.db.entities.*
import io.github.wulkanowy.sdk.pojo.MailboxType
import timber.log.Timber
import io.github.wulkanowy.sdk.pojo.Message as SdkMessage
import io.github.wulkanowy.sdk.pojo.MessageAttachment as SdkMessageAttachment
import io.github.wulkanowy.sdk.pojo.Recipient as SdkRecipient
fun List<SdkMessage>.mapToEntities(mailbox: Mailbox) = map {
fun List<SdkMessage>.mapToEntities(
student: Student,
mailbox: Mailbox?,
allMailboxes: List<Mailbox>
): List<Message> = map {
Message(
messageGlobalKey = it.globalKey,
mailboxKey = mailbox.globalKey,
mailboxKey = mailbox?.globalKey ?: allMailboxes.find { box ->
box.fullName == it.mailbox
}?.globalKey.let { mailboxKey ->
if (mailboxKey == null) {
Timber.e("Can't find ${it.mailbox} in $allMailboxes")
"unknown"
} else mailboxKey
},
email = student.email,
messageId = it.id,
correspondents = it.correspondents,
subject = it.subject.trim(),
date = it.dateZoned.toInstant(),
folderId = it.folderId,
unread = it.unread,
hasAttachments = it.hasAttachments
unreadBy = it.unreadBy,
readBy = it.readBy,
hasAttachments = it.hasAttachments,
).apply {
content = it.content.orEmpty()
}

View File

@ -0,0 +1,87 @@
package io.github.wulkanowy.data.mappers
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.pojos.*
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.mapper.mapSemesters
import java.time.Instant
import io.github.wulkanowy.sdk.scrapper.register.RegisterStudent as SdkRegisterStudent
import io.github.wulkanowy.sdk.scrapper.register.RegisterUser as SdkRegisterUser
fun SdkRegisterUser.mapToPojo(password: String) = RegisterUser(
email = email,
login = login,
password = password,
baseUrl = baseUrl,
loginType = loginType,
symbols = symbols.map { registerSymbol ->
RegisterSymbol(
symbol = registerSymbol.symbol,
error = registerSymbol.error,
userName = registerSymbol.userName,
schools = registerSymbol.schools.map {
RegisterUnit(
userLoginId = it.userLoginId,
schoolId = it.schoolId,
schoolName = it.schoolName,
schoolShortName = it.schoolShortName,
parentIds = it.parentIds,
studentIds = it.studentIds,
employeeIds = it.employeeIds,
error = it.error,
students = it.subjects
.filterIsInstance<SdkRegisterStudent>()
.map { registerSubject ->
RegisterStudent(
studentId = registerSubject.studentId,
studentName = registerSubject.studentName,
studentSecondName = registerSubject.studentSecondName,
studentSurname = registerSubject.studentSurname,
className = registerSubject.className,
classId = registerSubject.classId,
isParent = registerSubject.isParent,
semesters = registerSubject.semesters
.mapSemesters()
.mapToEntities(registerSubject.studentId),
)
},
)
}
)
}
)
fun RegisterStudent.mapToStudentWithSemesters(
user: RegisterUser,
symbol: RegisterSymbol,
unit: RegisterUnit,
colors: List<Long>,
): StudentWithSemesters = StudentWithSemesters(
semesters = semesters,
student = Student(
email = user.login, // for compatibility
userName = symbol.userName,
userLoginId = unit.userLoginId,
isParent = isParent,
className = className,
classId = classId,
studentId = studentId,
symbol = symbol.symbol,
loginType = user.loginType.name,
schoolName = unit.schoolName,
schoolShortName = unit.schoolShortName,
schoolSymbol = unit.schoolId,
studentName = "$studentName $studentSurname",
loginMode = Sdk.Mode.SCRAPPER.name,
scrapperBaseUrl = user.baseUrl,
mobileBaseUrl = "",
certificateKey = "",
privateKey = "",
password = user.password,
isCurrent = false,
registrationDate = Instant.now(),
).apply {
avatarColor = colors.random()
},
)

View File

@ -0,0 +1,43 @@
package io.github.wulkanowy.data.pojos
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.sdk.scrapper.Scrapper
data class RegisterUser(
val email: String,
val password: String,
val login: String, // may be the same as email
val baseUrl: String,
val loginType: Scrapper.LoginType,
val symbols: List<RegisterSymbol>,
) : java.io.Serializable
data class RegisterSymbol(
val symbol: String,
val error: Throwable?,
val userName: String,
val schools: List<RegisterUnit>,
) : java.io.Serializable
data class RegisterUnit(
val userLoginId: Int,
val schoolId: String,
val schoolName: String,
val schoolShortName: String,
val parentIds: List<Int>,
val studentIds: List<Int>,
val employeeIds: List<Int>,
val error: Throwable?,
val students: List<RegisterStudent>,
) : java.io.Serializable
data class RegisterStudent(
val studentId: Int,
val studentName: String,
val studentSecondName: String,
val studentSurname: String,
val className: String,
val classId: Int,
val isParent: Boolean,
val semesters: List<Semester>,
) : java.io.Serializable

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.db.dao.AttendanceDao
import io.github.wulkanowy.data.db.dao.TimetableDao
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
@ -9,8 +10,10 @@ import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.pojo.Absent
import io.github.wulkanowy.utils.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.withContext
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
@ -20,6 +23,7 @@ import javax.inject.Singleton
@Singleton
class AttendanceRepository @Inject constructor(
private val attendanceDb: AttendanceDao,
private val timetableDb: TimetableDao,
private val sdk: Sdk,
private val refreshHelper: AutoRefreshHelper,
) {
@ -48,10 +52,15 @@ class AttendanceRepository @Inject constructor(
attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday)
},
fetch = {
val lessons = withContext(Dispatchers.IO) {
timetableDb.load(
semester.diaryId, semester.studentId, start.monday, end.sunday
)
}
sdk.init(student)
.switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear)
.getAttendance(start.monday, end.sunday, semester.semesterId)
.mapToEntities(semester)
.mapToEntities(semester, lessons)
},
saveFetchResult = { old, new ->
attendanceDb.deleteAll(old uniqueSubtract new)

View File

@ -1,48 +0,0 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.db.dao.MailboxDao
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.uniqueSubtract
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class MailboxRepository @Inject constructor(
private val mailboxDao: MailboxDao,
private val sdk: Sdk,
private val refreshHelper: AutoRefreshHelper,
) {
private val cacheKey = "mailboxes"
suspend fun refreshMailboxes(student: Student) {
val new = sdk.init(student).getMailboxes().mapToEntities(student)
val old = mailboxDao.loadAll(student.userLoginId)
mailboxDao.deleteAll(old uniqueSubtract new)
mailboxDao.insertAll(new uniqueSubtract old)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
}
suspend fun getMailbox(student: Student): Mailbox {
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student))
val mailbox = mailboxDao.load(student.userLoginId, student.studentName)
return if (isExpired || mailbox == null) {
refreshMailboxes(student)
val newMailbox = mailboxDao.load(student.userLoginId, student.studentName)
requireNotNull(newMailbox) {
"Mailbox for ${student.userName} - ${student.studentName} not found!"
}
newMailbox
} else mailbox
}
}

View File

@ -3,8 +3,9 @@ package io.github.wulkanowy.data.repositories
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.db.dao.MailboxDao
import io.github.wulkanowy.data.db.dao.MessageAttachmentDao
import io.github.wulkanowy.data.db.dao.MessagesDao
import io.github.wulkanowy.data.db.entities.*
@ -13,8 +14,8 @@ import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED
import io.github.wulkanowy.data.enums.MessageFolder.TRASHED
import io.github.wulkanowy.data.mappers.mapFromEntities
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.data.pojos.MessageDraft
import io.github.wulkanowy.domain.messages.GetMailboxByStudentUseCase
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.pojo.Folder
import io.github.wulkanowy.utils.AutoRefreshHelper
@ -40,16 +41,18 @@ class MessageRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
private val sharedPrefProvider: SharedPrefProvider,
private val json: Json,
private val mailboxDao: MailboxDao,
private val getMailboxByStudentUseCase: GetMailboxByStudentUseCase,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "message"
private val messagesCacheKey = "message"
private val mailboxCacheKey = "mailboxes"
@Suppress("UNUSED_PARAMETER")
fun getMessages(
student: Student,
mailbox: Mailbox,
mailbox: Mailbox?,
folder: MessageFolder,
forceRefresh: Boolean,
notify: Boolean = false,
@ -58,13 +61,20 @@ class MessageRepository @Inject constructor(
isResultEmpty = { it.isEmpty() },
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(
key = getRefreshKey(cacheKey, student, folder)
key = getRefreshKey(messagesCacheKey, mailbox, folder)
)
it.isEmpty() || forceRefresh || isExpired
},
query = { messagesDb.loadAll(mailbox.globalKey, folder.id) },
query = {
if (mailbox == null) {
messagesDb.loadAll(folder.id, student.email)
} else messagesDb.loadAll(mailbox.globalKey, folder.id)
},
fetch = {
sdk.init(student).getMessages(Folder.valueOf(folder.name)).mapToEntities(mailbox)
sdk.init(student).getMessages(
folder = Folder.valueOf(folder.name),
mailboxKey = mailbox?.globalKey,
).mapToEntities(student, mailbox, mailboxDao.loadAll(student.email))
},
saveFetchResult = { old, new ->
messagesDb.deleteAll(old uniqueSubtract new)
@ -72,7 +82,9 @@ class MessageRepository @Inject constructor(
it.isNotified = !notify
})
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student, folder))
refreshHelper.updateLastRefreshTimestamp(
getRefreshKey(messagesCacheKey, mailbox, folder)
)
}
)
@ -87,9 +99,11 @@ class MessageRepository @Inject constructor(
Timber.d("Message content in db empty: ${it.message.content.isBlank()}")
it.message.unread || it.message.content.isBlank()
},
query = { messagesDb.loadMessageWithAttachment(message.messageGlobalKey) },
query = {
messagesDb.loadMessageWithAttachment(message.messageGlobalKey)
},
fetch = {
sdk.init(student).getMessageDetails(it!!.message.messageGlobalKey)
sdk.init(student).getMessageDetails(it!!.message.messageGlobalKey, markAsRead)
},
saveFetchResult = { old, new ->
checkNotNull(old) { "Fetched message no longer exist!" }
@ -98,7 +112,7 @@ class MessageRepository @Inject constructor(
id = message.id
unread = !markAsRead
sender = new.sender
recipients = new.recipients.firstOrNull() ?: "Wielu adresoatów"
recipients = new.recipients.singleOrNull() ?: "Wielu adresatów"
content = content.ifBlank { new.content }
})
)
@ -110,8 +124,10 @@ class MessageRepository @Inject constructor(
}
)
fun getMessagesFromDatabase(mailbox: Mailbox): Flow<List<Message>> {
return messagesDb.loadAll(mailbox.globalKey, RECEIVED.id)
fun getMessagesFromDatabase(student: Student, mailbox: Mailbox?): Flow<List<Message>> {
return if (mailbox == null) {
messagesDb.loadAll(RECEIVED.id, student.email)
} else messagesDb.loadAll(mailbox.globalKey, RECEIVED.id)
}
suspend fun updateMessages(messages: List<Message>) {
@ -133,7 +149,7 @@ class MessageRepository @Inject constructor(
)
}
suspend fun deleteMessages(student: Student, mailbox: Mailbox, messages: List<Message>) {
suspend fun deleteMessages(student: Student, mailbox: Mailbox?, messages: List<Message>) {
val firstMessage = messages.first()
sdk.init(student).deleteMessages(
messages = messages.map { it.messageGlobalKey },
@ -162,15 +178,49 @@ class MessageRepository @Inject constructor(
).first()
}
suspend fun deleteMessage(student: Student, mailbox: Mailbox, message: Message) {
suspend fun deleteMessage(student: Student, mailbox: Mailbox?, message: Message) {
deleteMessages(student, mailbox, listOf(message))
}
suspend fun getMailboxes(student: Student, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
isResultEmpty = { it.isEmpty() },
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(
key = getRefreshKey(mailboxCacheKey, student),
)
it.isEmpty() || isExpired || forceRefresh
},
query = { mailboxDao.loadAll(student.email, student.symbol, student.schoolSymbol) },
fetch = {
sdk.init(student).getMailboxes().mapToEntities(student)
},
saveFetchResult = { old, new ->
mailboxDao.deleteAll(old uniqueSubtract new)
mailboxDao.insertAll(new uniqueSubtract old)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(mailboxCacheKey, student))
}
)
suspend fun getMailboxByStudent(student: Student): Mailbox? {
val mailbox = getMailboxByStudentUseCase(student)
return if (mailbox == null) {
getMailboxes(student, forceRefresh = true)
.onResourceError { throw it }
.onResourceSuccess { Timber.i("Found ${it.size} new mailboxes") }
.waitForResult()
getMailboxByStudentUseCase(student)
} else mailbox
}
var draftMessage: MessageDraft?
get() = sharedPrefProvider.getString(context.getString(R.string.pref_key_message_send_draft))
get() = sharedPrefProvider.getString(context.getString(R.string.pref_key_message_draft))
?.let { json.decodeFromString(it) }
set(value) = sharedPrefProvider.putString(
context.getString(R.string.pref_key_message_send_draft),
context.getString(R.string.pref_key_message_draft),
value?.let { json.encodeToString(it) }
)
}

View File

@ -10,17 +10,16 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.enums.*
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import io.github.wulkanowy.ui.modules.grade.GradeAverageMode
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.time.Instant
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
@OptIn(ExperimentalCoroutinesApi::class)
@Singleton
class PreferencesRepository @Inject constructor(
@ApplicationContext val context: Context,
@ -316,6 +315,16 @@ class PreferencesRepository @Inject constructor(
putBoolean(context.getString(R.string.pref_key_ads_enabled), value)
}
var installationId: String
get() = sharedPref.getString(PREF_KEY_INSTALLATION_ID, null).orEmpty()
private set(value) = sharedPref.edit { putString(PREF_KEY_INSTALLATION_ID, value) }
init {
if (installationId.isEmpty()) {
installationId = UUID.randomUUID().toString()
}
}
private fun getLong(id: Int, default: Int) = getLong(context.getString(id), default)
private fun getLong(id: String, default: Int) =
@ -331,23 +340,14 @@ class PreferencesRepository @Inject constructor(
private fun getBoolean(id: String, default: Int) =
sharedPref.getBoolean(id, context.resources.getBoolean(default))
private fun getBoolean(id: Int, default: Boolean) =
sharedPref.getBoolean(context.getString(id), default)
private companion object {
private const val PREF_KEY_INSTALLATION_ID = "installation_id"
private const val PREF_KEY_DASHBOARD_ITEMS_POSITION = "dashboard_items_position"
private const val PREF_KEY_IN_APP_REVIEW_COUNT = "in_app_review_count"
private const val PREF_KEY_IN_APP_REVIEW_DATE = "in_app_review_date"
private const val PREF_KEY_IN_APP_REVIEW_DONE = "in_app_review_done"
private const val PREF_KEY_APP_SUPPORT_SHOWN = "app_support_shown"
private const val PREF_KEY_PERSONALIZED_ADS_ENABLED = "personalized_ads_enabled"
private const val PREF_KEY_ADMIN_DISMISSED_MESSAGE_IDS = "admin_message_dismissed_ids"
}
}

View File

@ -33,9 +33,11 @@ class RecipientRepository @Inject constructor(
suspend fun getRecipients(
student: Student,
mailbox: Mailbox,
type: MailboxType
mailbox: Mailbox?,
type: MailboxType,
): List<Recipient> {
mailbox ?: return emptyList()
val cached = recipientDb.loadAll(type, mailbox.globalKey)
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student))
@ -47,11 +49,15 @@ class RecipientRepository @Inject constructor(
suspend fun getMessageSender(
student: Student,
mailbox: Mailbox,
message: Message
): List<Recipient> = sdk.init(student)
.getMessageReplayDetails(message.messageGlobalKey)
.sender
.let(::listOf)
.mapToEntities(mailbox.globalKey)
mailbox: Mailbox?,
message: Message,
): List<Recipient> {
mailbox ?: return emptyList()
return sdk.init(student)
.getMessageReplayDetails(message.messageGlobalKey)
.sender
.let(::listOf)
.mapToEntities(mailbox.globalKey)
}
}

View File

@ -11,6 +11,8 @@ import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.mappers.mapToPojo
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.DispatchersProvider
@ -52,6 +54,14 @@ class StudentRepository @Inject constructor(
sdk.getStudentsFromScrapper(email, password, scrapperBaseUrl, symbol)
.mapToEntities(password, appInfo.defaultColorsForAvatar)
suspend fun getUserSubjectsFromScrapper(
email: String,
password: String,
scrapperBaseUrl: String,
symbol: String
): RegisterUser = sdk.getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, symbol)
.mapToPojo(password)
suspend fun getStudentsHybrid(
email: String,
password: String,

View File

@ -0,0 +1,65 @@
package io.github.wulkanowy.domain.messages
import io.github.wulkanowy.data.db.dao.MailboxDao
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Student
import javax.inject.Inject
class GetMailboxByStudentUseCase @Inject constructor(
private val mailboxDao: MailboxDao,
) {
suspend operator fun invoke(student: Student): Mailbox? {
return mailboxDao.loadAll(student.email)
.filterByStudent(student)
}
private fun List<Mailbox>.filterByStudent(student: Student): Mailbox? {
val normalizedStudentName = student.studentName.normalizeStudentName()
return singleOrNull {
it.studentName.normalizeStudentName() == normalizedStudentName
} ?: singleOrNull {
it.studentName.normalizeStudentName() == normalizedStudentName
&& it.schoolNameShort == student.schoolShortName
} ?: singleOrNull {
it.studentName.getFirstAndLastPart() == normalizedStudentName.getFirstAndLastPart()
} ?: singleOrNull {
it.studentName.getReversedName() == normalizedStudentName
} ?: singleOrNull {
it.studentName.getUnauthorizedVersion() == normalizedStudentName
}
}
private fun String.normalizeStudentName(): String {
return trim().split(" ")
.filter { it.isNotBlank() }
.joinToString(" ") { part ->
part.lowercase().replaceFirstChar { it.uppercase() }
}
}
private fun String.getFirstAndLastPart(): String {
val parts = normalizeStudentName().split(" ")
val endParts = parts.filterIndexed { i, _ ->
i == 0 || parts.size - 1 == i
}
return endParts.joinToString(" ")
}
private fun String.getReversedName(): String {
val parts = normalizeStudentName().split(" ")
return parts
.asReversed()
.joinToString(" ")
}
private fun String.getUnauthorizedVersion(): String {
return normalizeStudentName().split(" ")
.joinToString(" ") {
it.first() + "*".repeat(it.length - 1)
}
}
}

View File

@ -8,7 +8,6 @@ import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.GroupNotificationData
import io.github.wulkanowy.data.pojos.NotificationData
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.splash.SplashActivity
import io.github.wulkanowy.utils.descriptionRes
import io.github.wulkanowy.utils.getPlural
import io.github.wulkanowy.utils.toFormattedString
@ -22,8 +21,9 @@ class NewAttendanceNotification @Inject constructor(
suspend fun notify(items: List<Attendance>, student: Student) {
val lines = items.filterNot { it.presence || it.name == "UNKNOWN" }
.map {
val lesson = it.subject.ifBlank { "Lekcja ${it.number}" }
val description = context.getString(it.descriptionRes)
"${it.date.toFormattedString("dd.MM")} - ${it.subject}: $description"
"${it.date.toFormattedString("dd.MM")} - $lesson: $description"
}
.ifEmpty { return }

View File

@ -8,7 +8,6 @@ import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.GroupNotificationData
import io.github.wulkanowy.data.pojos.NotificationData
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.splash.SplashActivity
import io.github.wulkanowy.utils.getPlural
import javax.inject.Inject

View File

@ -3,7 +3,6 @@ package io.github.wulkanowy.services.sync.works
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED
import io.github.wulkanowy.data.repositories.MailboxRepository
import io.github.wulkanowy.data.repositories.MessageRepository
import io.github.wulkanowy.data.waitForResult
import io.github.wulkanowy.services.sync.notifications.NewMessageNotification
@ -12,12 +11,11 @@ import javax.inject.Inject
class MessageWork @Inject constructor(
private val messageRepository: MessageRepository,
private val mailboxRepository: MailboxRepository,
private val newMessageNotification: NewMessageNotification,
) : Work {
override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) {
val mailbox = mailboxRepository.getMailbox(student)
val mailbox = messageRepository.getMailboxByStudent(student)
messageRepository.getMessages(
student = student,
mailbox = mailbox,
@ -26,7 +24,7 @@ class MessageWork @Inject constructor(
notify = notify
).waitForResult()
messageRepository.getMessagesFromDatabase(mailbox).first()
messageRepository.getMessagesFromDatabase(student, mailbox).first()
.filter { !it.isNotified && it.unread }.let {
if (it.isNotEmpty()) newMessageNotification.notify(it, student)
messageRepository.updateMessages(it.onEach { message -> message.isNotified = true })

View File

@ -1,22 +1,23 @@
package io.github.wulkanowy.services.sync.works
import io.github.wulkanowy.data.dataOrNull
import io.github.wulkanowy.data.db.entities.MailboxType
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.MailboxRepository
import io.github.wulkanowy.data.repositories.MessageRepository
import io.github.wulkanowy.data.repositories.RecipientRepository
import io.github.wulkanowy.data.toFirstResult
import javax.inject.Inject
class RecipientWork @Inject constructor(
private val mailboxRepository: MailboxRepository,
private val messageRepository: MessageRepository,
private val recipientRepository: RecipientRepository
) : Work {
override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) {
mailboxRepository.refreshMailboxes(student)
val mailbox = mailboxRepository.getMailbox(student)
recipientRepository.refreshRecipients(student, mailbox, MailboxType.EMPLOYEE)
val mailboxes = messageRepository.getMailboxes(student, forceRefresh = true).toFirstResult()
mailboxes.dataOrNull?.forEach {
recipientRepository.refreshRecipients(student, it, MailboxType.EMPLOYEE)
}
}
}

View File

@ -4,7 +4,6 @@ import android.app.Dialog
import android.content.ClipData
import android.content.ClipboardManager
import android.os.Bundle
import android.view.LayoutInflater
import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
import androidx.appcompat.app.AlertDialog
@ -15,6 +14,7 @@ import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.DialogErrorBinding
import io.github.wulkanowy.utils.*
import javax.inject.Inject
@ -25,6 +25,9 @@ class ErrorDialog : DialogFragment() {
@Inject
lateinit var appInfo: AppInfo
@Inject
lateinit var preferencesRepository: PreferencesRepository
companion object {
private const val ARGUMENT_KEY = "error"
@ -34,9 +37,9 @@ class ErrorDialog : DialogFragment() {
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val error = requireArguments().getSerializable(ARGUMENT_KEY) as Throwable
val error = requireArguments().serializable<Throwable>(ARGUMENT_KEY)
val binding = DialogErrorBinding.inflate(LayoutInflater.from(context))
val binding = DialogErrorBinding.inflate(layoutInflater)
binding.bindErrorDetails(error)
return getAlertDialog(binding, error).apply {
@ -99,7 +102,8 @@ class ErrorDialog : DialogFragment() {
R.string.about_feedback_template,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
"${appInfo.versionName}-${appInfo.buildFlavor}"
"${appInfo.versionName}-${appInfo.buildFlavor}",
preferencesRepository.installationId,
) + "\n" + content,
onActivityNotFound = {
requireContext().openInternetBrowser(

View File

@ -1,6 +1,9 @@
package io.github.wulkanowy.ui.base
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.PackageManager.GET_ACTIVITIES
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
@ -41,9 +44,8 @@ class ThemeManager @Inject constructor(private val preferencesRepository: Prefer
)
}
private fun isThemeApplicable(activity: AppCompatActivity) =
activity.packageManager
.getPackageInfo(activity.packageName, GET_ACTIVITIES)
private fun isThemeApplicable(activity: AppCompatActivity): Boolean =
getPackageInfo(activity)
.activities
.singleOrNull { it.name == activity::class.java.canonicalName }
?.theme
@ -52,4 +54,14 @@ class ThemeManager @Inject constructor(private val preferencesRepository: Prefer
|| it == R.style.WulkanowyTheme_Login || it == R.style.WulkanowyTheme_Login_Black
|| it == R.style.WulkanowyTheme_MessageSend || it == R.style.WulkanowyTheme_MessageSend_Black
}
@Suppress("DEPRECATION")
private fun getPackageInfo(activity: AppCompatActivity): PackageInfo {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
activity.packageManager.getPackageInfo(
activity.packageName,
PackageManager.PackageInfoFlags.of(GET_ACTIVITIES.toLong())
)
} else activity.packageManager.getPackageInfo(activity.packageName, GET_ACTIVITIES)
}
}

View File

@ -6,6 +6,7 @@ import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.FragmentAboutBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.about.contributor.ContributorFragment
@ -30,6 +31,9 @@ class AboutFragment : BaseFragment<FragmentAboutBinding>(R.layout.fragment_about
@Inject
lateinit var appInfo: AppInfo
@Inject
lateinit var preferencesRepository: PreferencesRepository
override val versionRes: Triple<String, String, Drawable?>?
get() = context?.run {
val buildTimestamp =
@ -185,7 +189,8 @@ class AboutFragment : BaseFragment<FragmentAboutBinding>(R.layout.fragment_about
R.string.about_feedback_template,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
"${appInfo.versionName}-${appInfo.buildFlavor}"
"${appInfo.versionName}-${appInfo.buildFlavor}",
preferencesRepository.installationId,
),
onActivityNotFound = {
requireContext().openInternetBrowser(

View File

@ -34,6 +34,7 @@ class AccountFragment : BaseFragment<FragmentAccountBinding>(R.layout.fragment_a
override val titleStringId = R.string.account_title
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)

View File

@ -6,6 +6,7 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import androidx.core.view.get
import androidx.core.view.isVisible
import dagger.hilt.android.AndroidEntryPoint
@ -21,6 +22,7 @@ import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoFragment
import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView
import io.github.wulkanowy.utils.createNameInitialsDrawable
import io.github.wulkanowy.utils.nickOrName
import io.github.wulkanowy.utils.serializable
import javax.inject.Inject
@AndroidEntryPoint
@ -37,12 +39,12 @@ class AccountDetailsFragment :
private const val ARGUMENT_KEY = "Data"
fun newInstance(student: Student) =
AccountDetailsFragment().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, student) }
}
fun newInstance(student: Student) = AccountDetailsFragment().apply {
arguments = bundleOf(ARGUMENT_KEY to student)
}
}
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
@ -51,7 +53,7 @@ class AccountDetailsFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentAccountDetailsBinding.bind(view)
presenter.onAttachView(this, requireArguments()[ARGUMENT_KEY] as Student)
presenter.onAttachView(this, requireArguments().serializable(ARGUMENT_KEY))
}
override fun initView() {

View File

@ -4,11 +4,13 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.GridLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.databinding.DialogAccountEditBinding
import io.github.wulkanowy.ui.base.BaseDialogFragment
import io.github.wulkanowy.utils.serializable
import javax.inject.Inject
@AndroidEntryPoint
@ -24,12 +26,9 @@ class AccountEditDialog : BaseDialogFragment<DialogAccountEditBinding>(), Accoun
private const val ARGUMENT_KEY = "student_with_semesters"
fun newInstance(student: Student) =
AccountEditDialog().apply {
arguments = Bundle().apply {
putSerializable(ARGUMENT_KEY, student)
}
}
fun newInstance(student: Student) = AccountEditDialog().apply {
arguments = bundleOf(ARGUMENT_KEY to student)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
@ -45,7 +44,7 @@ class AccountEditDialog : BaseDialogFragment<DialogAccountEditBinding>(), Accoun
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
presenter.onAttachView(this, requireArguments()[ARGUMENT_KEY] as Student)
presenter.onAttachView(this, requireArguments().serializable(ARGUMENT_KEY))
}
override fun initView() {

View File

@ -4,6 +4,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
@ -13,6 +14,7 @@ import io.github.wulkanowy.ui.modules.account.AccountAdapter
import io.github.wulkanowy.ui.modules.account.AccountFragment
import io.github.wulkanowy.ui.modules.account.AccountItem
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.utils.serializable
import javax.inject.Inject
@AndroidEntryPoint
@ -30,9 +32,7 @@ class AccountQuickDialog : BaseDialogFragment<DialogAccountQuickBinding>(), Acco
fun newInstance(studentsWithSemesters: List<StudentWithSemesters>) =
AccountQuickDialog().apply {
arguments = Bundle().apply {
putSerializable(STUDENTS_ARGUMENT_KEY, studentsWithSemesters.toTypedArray())
}
arguments = bundleOf(STUDENTS_ARGUMENT_KEY to studentsWithSemesters.toTypedArray())
}
}
@ -49,8 +49,8 @@ class AccountQuickDialog : BaseDialogFragment<DialogAccountQuickBinding>(), Acco
@Suppress("UNCHECKED_CAST")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val studentsWithSemesters =
(requireArguments()[STUDENTS_ARGUMENT_KEY] as Array<StudentWithSemesters>).toList()
val studentsWithSemesters = requireArguments()
.serializable<Array<StudentWithSemesters>>(STUDENTS_ARGUMENT_KEY).toList()
presenter.onAttachView(this, studentsWithSemesters)
}

View File

@ -4,11 +4,13 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.databinding.DialogAttendanceBinding
import io.github.wulkanowy.utils.descriptionRes
import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.serializable
import io.github.wulkanowy.utils.toFormattedString
class AttendanceDialog : DialogFragment() {
@ -22,16 +24,14 @@ class AttendanceDialog : DialogFragment() {
private const val ARGUMENT_KEY = "Item"
fun newInstance(exam: Attendance) = AttendanceDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) }
arguments = bundleOf(ARGUMENT_KEY to exam)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
arguments?.run {
attendance = getSerializable(ARGUMENT_KEY) as Attendance
}
attendance = requireArguments().serializable(ARGUMENT_KEY)
}
override fun onCreateView(

View File

@ -84,6 +84,7 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
}
}
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)

View File

@ -4,11 +4,13 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.data.db.entities.Conference
import io.github.wulkanowy.databinding.DialogConferenceBinding
import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.serializable
import io.github.wulkanowy.utils.toFormattedString
class ConferenceDialog : DialogFragment() {
@ -22,16 +24,14 @@ class ConferenceDialog : DialogFragment() {
private const val ARGUMENT_KEY = "item"
fun newInstance(conference: Conference) = ConferenceDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, conference) }
arguments = bundleOf(ARGUMENT_KEY to conference)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
arguments?.let {
conference = it.getSerializable(ARGUMENT_KEY) as Conference
}
conference = requireArguments().serializable(ARGUMENT_KEY)
}
override fun onCreateView(
@ -57,4 +57,4 @@ class ConferenceDialog : DialogFragment() {
conferenceDialogAgendaTitle.isVisible = conference.agenda.isNotBlank()
}
}
}
}

View File

@ -61,6 +61,7 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
fun newInstance() = DashboardFragment()
}
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)

View File

@ -33,18 +33,27 @@ sealed class DashboardItem(val type: Type) {
}
data class HorizontalGroup(
val unreadMessagesCount: Int? = null,
val attendancePercentage: Double? = null,
val luckyNumber: Int? = null,
val unreadMessagesCount: Cell<Int?>? = null,
val attendancePercentage: Cell<Double>? = null,
val luckyNumber: Cell<Int>? = null,
override val error: Throwable? = null,
override val isLoading: Boolean = false
) : DashboardItem(Type.HORIZONTAL_GROUP) {
data class Cell<T>(
val data: T?,
val error: Boolean,
val isLoading: Boolean,
) {
val isHidden: Boolean
get() = data == null && !error && !isLoading
}
override val isDataLoaded
get() = unreadMessagesCount != null || attendancePercentage != null || luckyNumber != null
get() = unreadMessagesCount?.isLoading == false || attendancePercentage?.isLoading == false || luckyNumber?.isLoading == false
val isFullDataLoaded
get() = luckyNumber != -1 && attendancePercentage != -1.0 && unreadMessagesCount != -1
get() = luckyNumber?.isLoading != true && attendancePercentage?.isLoading != true && unreadMessagesCount?.isLoading != true
}
data class Grades(

View File

@ -25,7 +25,6 @@ class DashboardPresenter @Inject constructor(
private val gradeRepository: GradeRepository,
private val semesterRepository: SemesterRepository,
private val messageRepository: MessageRepository,
private val mailboxRepository: MailboxRepository,
private val attendanceSummaryRepository: AttendanceSummaryRepository,
private val timetableRepository: TimetableRepository,
private val homeworkRepository: HomeworkRepository,
@ -227,50 +226,71 @@ class DashboardPresenter @Inject constructor(
private fun loadHorizontalGroup(student: Student, forceRefresh: Boolean) {
flow {
val semester = semesterRepository.getCurrentSemester(student)
val mailbox = mailboxRepository.getMailbox(student)
val selectedTiles = preferencesRepository.selectedDashboardTiles
val flowSuccess = flowOf(Resource.Success(null))
val luckyNumberFlow = luckyNumberRepository.getLuckyNumber(student, forceRefresh)
.mapResourceData {
it ?: LuckyNumber(0, LocalDate.now(), 0)
}
.onResourceError { errorHandler.dispatch(it) }
.takeIf { DashboardItem.Tile.LUCKY_NUMBER in selectedTiles } ?: flowSuccess
val messageFLow = messageRepository.getMessages(
student = student,
mailbox = mailbox,
folder = MessageFolder.RECEIVED,
forceRefresh = forceRefresh
).takeIf { DashboardItem.Tile.MESSAGES in selectedTiles } ?: flowSuccess
val messageFLow = flatResourceFlow {
val mailbox = messageRepository.getMailboxByStudent(student)
val attendanceFlow = attendanceSummaryRepository.getAttendanceSummary(
student = student,
semester = semester,
subjectId = -1,
forceRefresh = forceRefresh
).takeIf { DashboardItem.Tile.ATTENDANCE in selectedTiles } ?: flowSuccess
messageRepository.getMessages(
student = student,
mailbox = mailbox,
folder = MessageFolder.RECEIVED,
forceRefresh = forceRefresh
)
}
.onResourceError { errorHandler.dispatch(it) }
.takeIf { DashboardItem.Tile.MESSAGES in selectedTiles } ?: flowSuccess
val attendanceFlow = flatResourceFlow {
val semester = semesterRepository.getCurrentSemester(student)
attendanceSummaryRepository.getAttendanceSummary(
student = student,
semester = semester,
subjectId = -1,
forceRefresh = forceRefresh
)
}
.onResourceError { errorHandler.dispatch(it) }
.takeIf { DashboardItem.Tile.ATTENDANCE in selectedTiles } ?: flowSuccess
emitAll(
combine(
luckyNumberFlow,
messageFLow,
attendanceFlow
flow = luckyNumberFlow,
flow2 = messageFLow,
flow3 = attendanceFlow,
) { luckyNumberResource, messageResource, attendanceResource ->
val resList = listOf(luckyNumberResource, messageResource, attendanceResource)
resList.firstNotNullOfOrNull { it.errorOrNull }?.let { throw it }
val isLoading = resList.any { it is Resource.Loading }
val luckyNumber = luckyNumberResource.dataOrNull?.luckyNumber
val messageCount = messageResource.dataOrNull?.count { it.unread }
val attendancePercentage = attendanceResource.dataOrNull?.calculatePercentage()
DashboardItem.HorizontalGroup(
isLoading = isLoading,
attendancePercentage = if (attendancePercentage == 0.0 && isLoading) -1.0 else attendancePercentage,
unreadMessagesCount = if (messageCount == 0 && isLoading) -1 else messageCount,
luckyNumber = if (luckyNumber == 0 && isLoading) -1 else luckyNumber
isLoading = resList.any { it is Resource.Loading },
error = resList.map { it.errorOrNull }.let { errors ->
if (errors.all { it != null }) {
errors.firstOrNull()
} else null
},
attendancePercentage = DashboardItem.HorizontalGroup.Cell(
data = attendanceResource.dataOrNull?.calculatePercentage(),
error = attendanceResource.errorOrNull != null,
isLoading = attendanceResource is Resource.Loading,
),
unreadMessagesCount = DashboardItem.HorizontalGroup.Cell(
data = messageResource.dataOrNull?.count { it.unread },
error = messageResource.errorOrNull != null,
isLoading = messageResource is Resource.Loading,
),
luckyNumber = DashboardItem.HorizontalGroup.Cell(
data = luckyNumberResource.dataOrNull?.luckyNumber,
error = luckyNumberResource.errorOrNull != null,
isLoading = luckyNumberResource is Resource.Loading,
)
)
})
}
@ -281,11 +301,8 @@ class DashboardPresenter @Inject constructor(
if (it.isLoading) {
Timber.i("Loading horizontal group data started")
if (it.isFullDataLoaded) {
firstLoadedItemList += DashboardItem.Type.HORIZONTAL_GROUP
}
} else {
firstLoadedItemList += DashboardItem.Type.HORIZONTAL_GROUP
Timber.i("Loading horizontal group result: Success")
}
}

View File

@ -171,81 +171,105 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
position: Int
) {
val item = items[position] as DashboardItem.HorizontalGroup
val unreadMessagesCount = item.unreadMessagesCount
val attendancePercentage = item.attendancePercentage
val luckyNumber = item.luckyNumber
val error = item.error
val isLoading = item.isLoading
val binding = horizontalGroupViewHolder.binding
val context = binding.root.context
val isLoadingVisible =
(isLoading && !item.isDataLoaded) || (isLoading && !item.isFullDataLoaded)
(item.isLoading && !item.isDataLoaded) || (item.isLoading && !item.isFullDataLoaded)
val isWideErrorShow = isLoadingVisible || item.error != null
with(horizontalGroupViewHolder.binding) {
dashboardHorizontalGroupItemInfoContainer.isVisible = isWideErrorShow
dashboardHorizontalGroupItemInfoProgress.isVisible = isLoadingVisible
dashboardHorizontalGroupItemInfoErrorText.isVisible = item.error != null
bindLuckyNumber(item, isWideErrorShow)
bindMessages(item, isWideErrorShow)
bindAttendance(item, isWideErrorShow)
}
}
private fun ItemDashboardHorizontalGroupBinding.bindLuckyNumber(
item: DashboardItem.HorizontalGroup,
isWideErrorShow: Boolean
) {
with(dashboardHorizontalGroupItemLuckyValue) {
isVisible = item.luckyNumber?.error != true
text = if (item.luckyNumber?.data == 0) {
context.getString(R.string.dashboard_horizontal_group_no_data)
} else item.luckyNumber?.data?.toString()
}
dashboardHorizontalGroupItemLuckyError.isVisible = item.luckyNumber?.error == true
with(dashboardHorizontalGroupItemLuckyContainer) {
isVisible = item.luckyNumber?.isHidden == false && !isWideErrorShow
setOnClickListener { onLuckyNumberTileClickListener() }
val isAttendanceHidden = item.attendancePercentage?.isHidden == true
val isMessagesHidden = item.unreadMessagesCount?.isHidden == true
val isLuckyNumberHidden = item.luckyNumber?.isHidden == true
updateLayoutParams<ViewGroup.MarginLayoutParams> {
updateMarginsRelative(
end = if (isAttendanceHidden && isMessagesHidden && !isLuckyNumberHidden) {
0
} else context.dpToPx(8f).toInt()
)
}
}
}
private fun ItemDashboardHorizontalGroupBinding.bindMessages(
item: DashboardItem.HorizontalGroup,
isWideErrorShow: Boolean
) {
dashboardHorizontalGroupItemMessageError.isVisible = item.unreadMessagesCount?.error == true
with(dashboardHorizontalGroupItemMessageValue) {
isVisible = item.unreadMessagesCount?.error != true
text = item.unreadMessagesCount?.data.toString()
}
with(dashboardHorizontalGroupItemMessageContainer) {
isVisible = item.unreadMessagesCount?.isHidden == false && !isWideErrorShow
setOnClickListener { onMessageTileClickListener() }
}
}
private fun ItemDashboardHorizontalGroupBinding.bindAttendance(
item: DashboardItem.HorizontalGroup,
isWideErrorShow: Boolean
) {
val attendancePercentage = item.attendancePercentage?.data
val attendanceColor = when {
attendancePercentage == null || attendancePercentage == .0 -> {
context.getThemeAttrColor(R.attr.colorOnSurface)
root.context.getThemeAttrColor(R.attr.colorOnSurface)
}
attendancePercentage <= ATTENDANCE_SECOND_WARNING_THRESHOLD -> {
context.getThemeAttrColor(R.attr.colorPrimary)
root.context.getThemeAttrColor(R.attr.colorPrimary)
}
attendancePercentage <= ATTENDANCE_FIRST_WARNING_THRESHOLD -> {
context.getThemeAttrColor(R.attr.colorTimetableChange)
root.context.getThemeAttrColor(R.attr.colorTimetableChange)
}
else -> context.getThemeAttrColor(R.attr.colorOnSurface)
else -> root.context.getThemeAttrColor(R.attr.colorOnSurface)
}
val attendanceString = if (attendancePercentage == null || attendancePercentage == .0) {
context.getString(R.string.dashboard_horizontal_group_no_data)
root.context.getString(R.string.dashboard_horizontal_group_no_data)
} else {
"%.2f%%".format(attendancePercentage)
}
with(binding.dashboardHorizontalGroupItemAttendanceValue) {
dashboardHorizontalGroupItemAttendanceError.isVisible =
item.attendancePercentage?.error == true
with(dashboardHorizontalGroupItemAttendanceValue) {
isVisible = item.attendancePercentage?.error != true
text = attendanceString
setTextColor(attendanceColor)
}
with(binding) {
dashboardHorizontalGroupItemMessageValue.text = unreadMessagesCount.toString()
dashboardHorizontalGroupItemLuckyValue.text = if (luckyNumber == 0) {
context.getString(R.string.dashboard_horizontal_group_no_data)
} else luckyNumber?.toString()
dashboardHorizontalGroupItemInfoContainer.isVisible = error != null || isLoadingVisible
dashboardHorizontalGroupItemInfoProgress.isVisible = isLoadingVisible
dashboardHorizontalGroupItemInfoErrorText.isVisible = error != null
with(dashboardHorizontalGroupItemLuckyContainer) {
isVisible = luckyNumber != null && luckyNumber != -1 && !isLoadingVisible
setOnClickListener { onLuckyNumberTileClickListener() }
updateLayoutParams<ViewGroup.MarginLayoutParams> {
updateMarginsRelative(
end = if (attendancePercentage == null && unreadMessagesCount == null && luckyNumber != null) {
0
} else {
context.dpToPx(8f).toInt()
}
)
with(dashboardHorizontalGroupItemAttendanceContainer) {
isVisible = item.attendancePercentage?.isHidden == false && !isWideErrorShow
setOnClickListener { onAttendanceTileClickListener() }
updateLayoutParams<ConstraintLayout.LayoutParams> {
matchConstraintPercentWidth = when {
item.luckyNumber?.isHidden == true && item.unreadMessagesCount?.isHidden == true -> 1.0f
item.luckyNumber?.isHidden == true || item.unreadMessagesCount?.isHidden == true -> 0.5f
else -> 0.4f
}
}
with(dashboardHorizontalGroupItemAttendanceContainer) {
isVisible =
attendancePercentage != null && attendancePercentage != -1.0 && !isLoadingVisible
updateLayoutParams<ConstraintLayout.LayoutParams> {
matchConstraintPercentWidth = when {
luckyNumber == null && unreadMessagesCount == null -> 1.0f
luckyNumber == null || unreadMessagesCount == null -> 0.5f
else -> 0.4f
}
}
setOnClickListener { onAttendanceTileClickListener() }
}
with(dashboardHorizontalGroupItemMessageContainer) {
isVisible =
unreadMessagesCount != null && unreadMessagesCount != -1 && !isLoadingVisible
setOnClickListener { onMessageTileClickListener() }
}
}
}

View File

@ -1,9 +1,7 @@
package io.github.wulkanowy.ui.modules.debug.logviewer
import android.content.Intent
import android.content.Intent.EXTRA_EMAIL
import android.content.Intent.EXTRA_STREAM
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
import android.content.Intent.*
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
@ -36,6 +34,7 @@ class LogViewerFragment : BaseFragment<FragmentLogviewerBinding>(R.layout.fragme
fun newInstance() = LogViewerFragment()
}
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)

View File

@ -19,9 +19,12 @@ val debugMessageItems = listOf(
private fun generateMessage(sender: String, subject: String) = Message(
subject = subject,
messageId = 123,
email = "",
date = Instant.now(),
folderId = 0,
unread = true,
readBy = 2,
unreadBy = 2,
hasAttachments = false,
messageGlobalKey = "",
correspondents = sender,

View File

@ -4,12 +4,14 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.databinding.DialogExamBinding
import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.openCalendarEventAdd
import io.github.wulkanowy.utils.serializable
import io.github.wulkanowy.utils.toFormattedString
import java.time.LocalTime
@ -24,16 +26,14 @@ class ExamDialog : DialogFragment() {
private const val ARGUMENT_KEY = "Item"
fun newInstance(exam: Exam) = ExamDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) }
arguments = bundleOf(ARGUMENT_KEY to exam)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
arguments?.run {
exam = getSerializable(ARGUMENT_KEY) as Exam
}
exam = requireArguments().serializable(ARGUMENT_KEY)
}
override fun onCreateView(

View File

@ -51,6 +51,7 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
override val currentPageIndex get() = binding.gradeViewPager.currentItem
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)

View File

@ -5,6 +5,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Grade
@ -27,22 +28,19 @@ class GradeDetailsDialog : DialogFragment() {
private const val COLOR_THEME_KEY = "Theme"
fun newInstance(grade: Grade, colorTheme: GradeColorTheme) =
GradeDetailsDialog().apply {
arguments = Bundle().apply {
putSerializable(ARGUMENT_KEY, grade)
putSerializable(COLOR_THEME_KEY, colorTheme)
}
}
fun newInstance(grade: Grade, colorTheme: GradeColorTheme) = GradeDetailsDialog().apply {
arguments = bundleOf(
ARGUMENT_KEY to grade,
COLOR_THEME_KEY to colorTheme
)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
arguments?.run {
grade = getSerializable(ARGUMENT_KEY) as Grade
gradeColorTheme = getSerializable(COLOR_THEME_KEY) as GradeColorTheme
}
grade = requireArguments().serializable(ARGUMENT_KEY)
gradeColorTheme = requireArguments().serializable(COLOR_THEME_KEY)
}
override fun onCreateView(

View File

@ -5,9 +5,7 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.View.GONE
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.view.View.*
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
@ -42,6 +40,7 @@ class GradeDetailsFragment :
override val isViewEmpty
get() = gradeDetailsAdapter.itemCount == 0
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)

View File

@ -3,7 +3,6 @@ package io.github.wulkanowy.ui.modules.grade.details
import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.enums.GradeExpandMode
import io.github.wulkanowy.data.enums.GradeSortingMode
import io.github.wulkanowy.data.enums.GradeSortingMode.*
import io.github.wulkanowy.data.repositories.GradeRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
@ -132,16 +131,17 @@ class GradeDetailsPresenter @Inject constructor(
}
.logResourceStatus("load grade details")
.onResourceData {
val gradeItems = createGradeItems(it)
view?.run {
enableSwipe(true)
showProgress(false)
showErrorView(false)
showContent(it.isNotEmpty())
showEmpty(it.isEmpty())
showContent(gradeItems.isNotEmpty())
showEmpty(gradeItems.isEmpty())
updateNewGradesAmount(it)
updateMarkAsDoneButton()
updateData(
data = createGradeItems(it),
data = gradeItems,
expandMode = preferencesRepository.gradeExpandMode,
preferencesRepository.gradeColorTheme
)

View File

@ -15,6 +15,7 @@ import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.grade.GradeView
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.serializable
import io.github.wulkanowy.utils.setOnItemSelectedListener
import javax.inject.Inject
@ -48,8 +49,8 @@ class GradeStatisticsFragment :
messageContainer = binding.gradeStatisticsRecycler
presenter.onAttachView(
view = this,
type = savedInstanceState?.getSerializable(SAVED_CHART_TYPE) as? GradeStatisticsItem.DataType,
subjectName = savedInstanceState?.getSerializable(SAVED_SUBJECT_NAME) as? String,
type = savedInstanceState?.serializable(SAVED_CHART_TYPE),
subjectName = savedInstanceState?.serializable(SAVED_SUBJECT_NAME),
)
}

View File

@ -10,6 +10,7 @@ import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.ItemGradeSummaryBinding
import io.github.wulkanowy.databinding.ScrollableHeaderGradeSummaryBinding
import io.github.wulkanowy.sdk.scrapper.grades.isGradeValid
import io.github.wulkanowy.utils.calcFinalAverage
import java.util.Locale
import javax.inject.Inject
@ -61,7 +62,7 @@ class GradeSummaryAdapter @Inject constructor(
if (items.isEmpty()) return
val context = binding.root.context
val finalItemsCount = items.count { it.finalGrade.matches("[0-6][+-]?".toRegex()) }
val finalItemsCount = items.count { isGradeValid(it.finalGrade) }
val calculatedItemsCount = items.count { value -> value.average != 0.0 }
val allItemsCount = items.count { !it.subject.equals("zachowanie", true) }
val finalAverage = items.calcFinalAverage(

View File

@ -7,6 +7,7 @@ import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
@ -14,6 +15,7 @@ import io.github.wulkanowy.data.db.entities.Homework
import io.github.wulkanowy.databinding.DialogHomeworkBinding
import io.github.wulkanowy.ui.base.BaseDialogFragment
import io.github.wulkanowy.utils.openInternetBrowser
import io.github.wulkanowy.utils.serializable
import javax.inject.Inject
@AndroidEntryPoint
@ -35,16 +37,14 @@ class HomeworkDetailsDialog : BaseDialogFragment<DialogHomeworkBinding>(), Homew
private const val ARGUMENT_KEY = "Item"
fun newInstance(homework: Homework) = HomeworkDetailsDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, homework) }
arguments = bundleOf(ARGUMENT_KEY to homework)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
arguments?.run {
homework = getSerializable(ARGUMENT_KEY) as Homework
}
homework = requireArguments().serializable(ARGUMENT_KEY)
}
override fun onCreateView(

View File

@ -2,13 +2,17 @@ package io.github.wulkanowy.ui.modules.login
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build.VERSION_CODES.TIRAMISU
import android.os.Bundle
import android.view.MenuItem
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE
import androidx.fragment.app.commit
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.databinding.ActivityLoginBinding
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.modules.login.advanced.LoginAdvancedFragment
@ -16,6 +20,9 @@ import io.github.wulkanowy.ui.modules.login.form.LoginFormFragment
import io.github.wulkanowy.ui.modules.login.recover.LoginRecoverFragment
import io.github.wulkanowy.ui.modules.login.studentselect.LoginStudentSelectFragment
import io.github.wulkanowy.ui.modules.login.symbol.LoginSymbolFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.notifications.NotificationsFragment
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.UpdateHelper
import javax.inject.Inject
@ -28,6 +35,9 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
@Inject
lateinit var updateHelper: UpdateHelper
@Inject
lateinit var appInfo: AppInfo
companion object {
fun getStartIntent(context: Context) = Intent(context, LoginActivity::class.java)
}
@ -55,7 +65,7 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) onBackPressed()
if (item.itemId == android.R.id.home) onBackPressedDispatcher.onBackPressed()
return true
}
@ -67,8 +77,24 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
openFragment(LoginSymbolFragment.newInstance(loginData))
}
fun navigateToStudentSelect(studentsWithSemesters: List<StudentWithSemesters>) {
openFragment(LoginStudentSelectFragment.newInstance(studentsWithSemesters))
fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser) {
openFragment(LoginStudentSelectFragment.newInstance(loginData, registerUser))
}
fun navigateToNotifications() {
val isNotificationsPermissionRequired = appInfo.systemVersion >= TIRAMISU
val isPermissionGranted = ContextCompat.checkSelfPermission(
this, "android.permission.POST_NOTIFICATIONS"
) == PackageManager.PERMISSION_GRANTED
if (isNotificationsPermissionRequired && !isPermissionGranted) {
openFragment(NotificationsFragment.newInstance(), clearBackStack = true)
} else navigateToFinish()
}
fun navigateToFinish() {
startActivity(MainActivity.getStartIntent(this))
finish()
}
fun onAdvancedLoginClick() {
@ -80,6 +106,8 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
}
private fun openFragment(fragment: Fragment, clearBackStack: Boolean = false) {
supportFragmentManager.popBackStack(fragment::class.java.name, POP_BACK_STACK_INCLUSIVE)
supportFragmentManager.commit {
replace(R.id.loginContainer, fragment)
setReorderingAllowed(true)

View File

@ -6,4 +6,5 @@ data class LoginData(
val login: String,
val password: String,
val baseUrl: String,
val symbol: String?,
) : Serializable

View File

@ -8,7 +8,7 @@ import android.widget.ArrayAdapter
import androidx.core.widget.doOnTextChanged
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.databinding.FragmentLoginAdvancedBinding
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.ui.base.BaseFragment
@ -327,8 +327,8 @@ class LoginAdvancedFragment :
(activity as? LoginActivity)?.navigateToSymbolFragment(loginData)
}
override fun navigateToStudentSelect(studentsWithSemesters: List<StudentWithSemesters>) {
(activity as? LoginActivity)?.navigateToStudentSelect(studentsWithSemesters)
override fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser) {
(activity as? LoginActivity)?.navigateToStudentSelect(loginData, registerUser)
}
override fun onResume() {

View File

@ -4,9 +4,15 @@ import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.pojos.RegisterStudent
import io.github.wulkanowy.data.pojos.RegisterSymbol
import io.github.wulkanowy.data.pojos.RegisterUnit
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.scrapper.Scrapper
import io.github.wulkanowy.sdk.scrapper.getNormalizedSymbol
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
@ -142,19 +148,23 @@ class LoginAdvancedPresenter @Inject constructor(
is Resource.Success -> {
analytics.logEvent(
"registration_form",
"success" to true,
"students" to it.data.size,
"error" to "No error"
)
val loginData = LoginData(
login = view?.formUsernameValue.orEmpty().trim(),
password = view?.formPassValue.orEmpty().trim(),
baseUrl = view?.formHostValue.orEmpty().trim()
)
when (it.data.size) {
0 -> view?.navigateToSymbol(loginData)
else -> view?.navigateToStudentSelect(it.data)
}
"success" to true,
"students" to it.data.size,
"error" to "No error"
)
val loginData = LoginData(
login = view?.formUsernameValue.orEmpty().trim(),
password = view?.formPassValue.orEmpty().trim(),
baseUrl = view?.formHostValue.orEmpty().trim(),
symbol = view?.formSymbolValue.orEmpty().trim().getNormalizedSymbol(),
)
when (it.data.size) {
0 -> view?.navigateToSymbol(loginData)
else -> view?.navigateToStudentSelect(
loginData = loginData,
registerUser = it.data.toRegisterUser(loginData),
)
}
}
is Resource.Error -> {
analytics.logEvent(
@ -173,6 +183,58 @@ class LoginAdvancedPresenter @Inject constructor(
}.launch("login")
}
private fun List<StudentWithSemesters>.toRegisterUser(loginData: LoginData) = RegisterUser(
email = loginData.login,
password = loginData.password,
login = loginData.login,
baseUrl = loginData.baseUrl,
loginType = firstOrNull()?.student?.loginType?.let(
Scrapper.LoginType::valueOf
) ?: Scrapper.LoginType.AUTO,
symbols = this
.groupBy { students -> students.student.symbol }
.map { (symbol, students) ->
RegisterSymbol(
symbol = symbol,
error = null,
userName = "",
schools = students
.groupBy { student ->
Triple(
first = student.student.schoolSymbol,
second = student.student.userLoginId,
third = student.student.schoolShortName
)
}
.map { (groupKey, students) ->
val (schoolId, loginId, schoolName) = groupKey
RegisterUnit(
students = students.map {
RegisterStudent(
studentId = it.student.studentId,
studentName = it.student.studentName,
studentSecondName = it.student.studentName,
studentSurname = it.student.studentName,
className = it.student.className,
classId = it.student.classId,
isParent = it.student.isParent,
semesters = it.semesters,
)
},
userLoginId = loginId,
schoolId = schoolId,
schoolName = schoolName,
schoolShortName = schoolName,
parentIds = listOf(),
studentIds = listOf(),
employeeIds = listOf(),
error = null
)
}
)
},
)
private suspend fun getStudentsAppropriatesToLoginType(): List<StudentWithSemesters> {
val email = view?.formUsernameValue.orEmpty()
val password = view?.formPassValue.orEmpty()

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.ui.modules.login.advanced
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.login.LoginData
@ -72,7 +73,7 @@ interface LoginAdvancedView : BaseView {
fun navigateToSymbol(loginData: LoginData)
fun navigateToStudentSelect(studentsWithSemesters: List<StudentWithSemesters>)
fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser)
fun setErrorPinRequired()

View File

@ -9,7 +9,8 @@ import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.FragmentLoginFormBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.login.LoginActivity
@ -32,6 +33,9 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
@Inject
lateinit var appInfo: AppInfo
@Inject
lateinit var preferencesRepository: PreferencesRepository
companion object {
fun newInstance() = LoginFormFragment()
}
@ -222,8 +226,8 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
(activity as? LoginActivity)?.navigateToSymbolFragment(loginData)
}
override fun navigateToStudentSelect(studentsWithSemesters: List<StudentWithSemesters>) {
(activity as? LoginActivity)?.navigateToStudentSelect(studentsWithSemesters)
override fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser) {
(activity as? LoginActivity)?.navigateToStudentSelect(loginData, registerUser)
}
override fun openAdvancedLogin() {
@ -260,8 +264,9 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
R.string.login_email_text,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
appInfo.versionName,
"${appInfo.versionName}-${appInfo.buildFlavor}",
"$formHostValue/$formHostSymbol",
preferencesRepository.installationId,
lastError
)
)

View File

@ -93,7 +93,7 @@ class LoginFormPresenter @Inject constructor(
if (!validateCredentials(email, password, host)) return
resourceFlow {
studentRepository.getStudentsScrapper(
studentRepository.getUserSubjectsFromScrapper(
email = email,
password = password,
scrapperBaseUrl = host,
@ -109,14 +109,14 @@ class LoginFormPresenter @Inject constructor(
}
}
.onResourceSuccess {
when (it.size) {
0 -> view?.navigateToSymbol(LoginData(email, password, host))
else -> view?.navigateToStudentSelect(it)
val loginData = LoginData(email, password, host, symbol)
when (it.symbols.size) {
0 -> view?.navigateToSymbol(loginData)
else -> view?.navigateToStudentSelect(loginData, it)
}
analytics.logEvent(
"registration_form",
"success" to true,
"students" to it.size,
"scrapperBaseUrl" to host,
"error" to "No error"
)
@ -134,7 +134,6 @@ class LoginFormPresenter @Inject constructor(
analytics.logEvent(
"registration_form",
"success" to false,
"students" to -1,
"scrapperBaseUrl" to host,
"error" to it.message.ifNullOrBlank { "No message" }
)

View File

@ -1,6 +1,6 @@
package io.github.wulkanowy.ui.modules.login.form
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.login.LoginData
@ -60,7 +60,7 @@ interface LoginFormView : BaseView {
fun navigateToSymbol(loginData: LoginData)
fun navigateToStudentSelect(studentsWithSemesters: List<StudentWithSemesters>)
fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser)
fun openPrivacyPolicyPage()

View File

@ -98,7 +98,7 @@ class LoginRecoverFragment :
loginRecoverButton.setOnClickListener { presenter.onRecoverClick() }
loginRecoverErrorRetry.setOnClickListener { presenter.onRecoverClick() }
loginRecoverErrorDetails.setOnClickListener { presenter.onDetailsClick() }
loginRecoverLogin.setOnClickListener { (activity as LoginActivity).onBackPressed() }
loginRecoverLogin.setOnClickListener { (activity as LoginActivity).onBackPressedDispatcher.onBackPressed() }
}
with(bindingLocal.loginRecoverHost) {

View File

@ -2,65 +2,182 @@ package io.github.wulkanowy.ui.modules.login.studentselect
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil.ItemCallback
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.databinding.ItemLoginStudentSelectBinding
import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.*
import javax.inject.Inject
@SuppressLint("SetTextI18n")
class LoginStudentSelectAdapter @Inject constructor() :
RecyclerView.Adapter<LoginStudentSelectAdapter.ItemViewHolder>() {
ListAdapter<LoginStudentSelectItem, RecyclerView.ViewHolder>(Differ) {
private val checkedList = mutableMapOf<Int, Boolean>()
override fun getItemViewType(position: Int): Int = getItem(position).type.ordinal
var items = emptyList<Pair<StudentWithSemesters, Boolean>>()
set(value) {
field = value
checkedList.clear()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (LoginStudentSelectItemType.values()[viewType]) {
LoginStudentSelectItemType.EMPTY_SYMBOLS_HEADER -> EmptySymbolsHeaderViewHolder(
ItemLoginStudentSelectEmptySymbolHeaderBinding.inflate(inflater, parent, false),
)
LoginStudentSelectItemType.SYMBOL_HEADER -> SymbolsHeaderViewHolder(
ItemLoginStudentSelectHeaderSymbolBinding.inflate(inflater, parent, false)
)
LoginStudentSelectItemType.SCHOOL_HEADER -> SchoolHeaderViewHolder(
ItemLoginStudentSelectHeaderSchoolBinding.inflate(inflater, parent, false)
)
LoginStudentSelectItemType.STUDENT -> StudentViewHolder(
ItemLoginStudentSelectStudentBinding.inflate(inflater, parent, false)
)
LoginStudentSelectItemType.HELP -> HelpViewHolder(
ItemLoginStudentSelectHelpBinding.inflate(inflater, parent, false)
)
}
}
var onClickListener: (StudentWithSemesters, alreadySaved: Boolean) -> Unit = { _, _ -> }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is EmptySymbolsHeaderViewHolder -> holder.bind(getItem(position) as LoginStudentSelectItem.EmptySymbolsHeader)
is SymbolsHeaderViewHolder -> holder.bind(getItem(position) as LoginStudentSelectItem.SymbolHeader)
is SchoolHeaderViewHolder -> holder.bind(getItem(position) as LoginStudentSelectItem.SchoolHeader)
is StudentViewHolder -> holder.bind(getItem(position) as LoginStudentSelectItem.Student)
is HelpViewHolder -> holder.bind(getItem(position) as LoginStudentSelectItem.Help)
}
}
override fun getItemCount() = items.size
private class EmptySymbolsHeaderViewHolder(
private val binding: ItemLoginStudentSelectEmptySymbolHeaderBinding,
) : RecyclerView.ViewHolder(binding.root) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder(
ItemLoginStudentSelectBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val (studentAndSemesters, alreadySaved) = items[position]
val student = studentAndSemesters.student
val semesters = studentAndSemesters.semesters
val diary = semesters.maxByOrNull { it.semesterId }
with(holder.binding) {
loginItemName.text = "${student.studentName} ${diary?.diaryName.orEmpty()}"
loginItemSchool.text = student.schoolName
loginItemName.isEnabled = !alreadySaved
loginItemSchool.isEnabled = !alreadySaved
loginItemSignedIn.visibility = if (alreadySaved) View.VISIBLE else View.GONE
with(loginItemCheck) {
isEnabled = !alreadySaved
keyListener = null
isChecked = checkedList[position] ?: false
fun bind(item: LoginStudentSelectItem.EmptySymbolsHeader) {
with(binding) {
loginStudentSelectEmptySymbolChevron.rotation = if (item.isExpanded) 270f else 90f
root.setOnClickListener { item.onClick() }
}
}
}
root.setOnClickListener {
onClickListener(studentAndSemesters, alreadySaved)
private class SymbolsHeaderViewHolder(
private val binding: ItemLoginStudentSelectHeaderSymbolBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: LoginStudentSelectItem.SymbolHeader) {
with(binding) {
loginStudentSelectHeaderSymbolValue.text = buildString {
append(root.context.getString(R.string.mobile_device_symbol))
append(": ")
append(item.humanReadableName ?: item.symbol.symbol)
if (!item.humanReadableName.isNullOrBlank()) {
append(" (${item.symbol.symbol})")
}
}
loginStudentSelectHeaderSymbolUsername.text = item.symbol.userName
loginStudentSelectHeaderSymbolUsername.isVisible = item.symbol.userName.isNotBlank()
loginStudentSelectHeaderSymbolError.text = item.symbol.error?.message
loginStudentSelectHeaderSymbolError.isVisible = item.symbol.error != null
loginStudentSelectHeaderSymbolError.maxLines = when {
item.isErrorExpanded -> Int.MAX_VALUE
else -> 2
}
if (item.symbol.error != null) {
root.setOnClickListener { item.onClick(item.symbol) }
} else root.setOnClickListener(null)
}
}
}
private class SchoolHeaderViewHolder(
private val binding: ItemLoginStudentSelectHeaderSchoolBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: LoginStudentSelectItem.SchoolHeader) {
with(binding) {
loginStudentSelectHeaderSchoolName.text = buildString {
append(item.unit.schoolName.trim())
append(" (")
append(item.unit.schoolShortName)
append(")")
}
loginStudentSelectHeaderSchoolDetails.isVisible = item.unit.students.isEmpty()
loginStudentSelectHeaderSchoolError.text = item.unit.error?.message
loginStudentSelectHeaderSchoolError.isVisible = item.unit.error != null
loginStudentSelectHeaderSchoolError.maxLines = when {
item.isErrorExpanded -> Int.MAX_VALUE
else -> 2
}
if (item.unit.error != null) {
root.setOnClickListener { item.onClick(item.unit) }
} else root.setOnClickListener(null)
}
}
}
private class StudentViewHolder(
private val binding: ItemLoginStudentSelectStudentBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: LoginStudentSelectItem.Student) {
val student = item.student
val semesters = student.semesters
val diary = semesters.maxByOrNull { it.semesterId }
with(binding) {
loginItemName.text = "${student.studentName} ${student.studentSurname}"
loginItemName.isEnabled = item.isEnabled
loginItemSignedIn.text = if (!item.isEnabled) {
root.context.getString(R.string.login_signed_in)
} else diary?.diaryName
with(loginItemCheck) {
if (isEnabled) {
isChecked = !isChecked
checkedList[position] = isChecked
}
keyListener = null
isEnabled = item.isEnabled
isChecked = item.isSelected || !item.isEnabled
}
root.isEnabled = item.isEnabled
root.setOnClickListener {
item.onClick(item)
}
}
}
}
class ItemViewHolder(val binding: ItemLoginStudentSelectBinding) :
RecyclerView.ViewHolder(binding.root)
private class HelpViewHolder(
private val binding: ItemLoginStudentSelectHelpBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: LoginStudentSelectItem.Help) {
with(binding) {
loginStudentSelectHelpSymbol.isVisible = item.isSymbolButtonVisible
loginStudentSelectHelpSymbol.setOnClickListener { item.onEnterSymbolClick() }
loginStudentSelectHelpMail.setOnClickListener { item.onContactUsClick() }
loginStudentSelectHelpDiscord.setOnClickListener { item.onDiscordClick() }
}
}
}
private object Differ : ItemCallback<LoginStudentSelectItem>() {
override fun areItemsTheSame(
oldItem: LoginStudentSelectItem, newItem: LoginStudentSelectItem
): Boolean = when {
oldItem is LoginStudentSelectItem.EmptySymbolsHeader && newItem is LoginStudentSelectItem.EmptySymbolsHeader -> true
oldItem is LoginStudentSelectItem.SymbolHeader && newItem is LoginStudentSelectItem.SymbolHeader -> {
oldItem.symbol == newItem.symbol
}
oldItem is LoginStudentSelectItem.Student && newItem is LoginStudentSelectItem.Student -> {
oldItem.student == newItem.student
}
else -> oldItem == newItem
}
override fun areContentsTheSame(
oldItem: LoginStudentSelectItem, newItem: LoginStudentSelectItem
): Boolean = oldItem == newItem
}
}

View File

@ -2,20 +2,20 @@ package io.github.wulkanowy.ui.modules.login.studentselect
import android.os.Bundle
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.core.view.isVisible
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.FragmentLoginStudentSelectBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.openEmailClient
import io.github.wulkanowy.utils.openInternetBrowser
import io.github.wulkanowy.utils.serializable
import javax.inject.Inject
@AndroidEntryPoint
@ -32,12 +32,26 @@ class LoginStudentSelectFragment :
@Inject
lateinit var appInfo: AppInfo
companion object {
const val ARG_STUDENTS = "STUDENTS"
@Inject
lateinit var preferencesRepository: PreferencesRepository
fun newInstance(studentsWithSemesters: List<StudentWithSemesters>) =
private lateinit var symbolsNames: Array<String>
private lateinit var symbolsValues: Array<String>
override val symbols: Map<String, String> by lazy {
symbolsValues.zip(symbolsNames).toMap()
}
companion object {
private const val ARG_LOGIN = "LOGIN"
private const val ARG_STUDENTS = "STUDENTS"
fun newInstance(loginData: LoginData, registerUser: RegisterUser) =
LoginStudentSelectFragment().apply {
arguments = bundleOf(ARG_STUDENTS to studentsWithSemesters)
arguments = bundleOf(
ARG_LOGIN to loginData,
ARG_STUDENTS to registerUser,
)
}
}
@ -45,62 +59,50 @@ class LoginStudentSelectFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentLoginStudentSelectBinding.bind(view)
symbolsNames = resources.getStringArray(R.array.symbols)
symbolsValues = resources.getStringArray(R.array.symbols_values)
presenter.onAttachView(
view = this,
students = requireArguments().getSerializable(ARG_STUDENTS) as List<StudentWithSemesters>,
loginData = requireArguments().serializable(ARG_LOGIN),
registerUser = requireArguments().serializable(ARG_STUDENTS),
)
}
override fun initView() {
(requireActivity() as LoginActivity).showActionBar(true)
loginAdapter.onClickListener = presenter::onItemSelected
with(binding) {
loginStudentSelectSignIn.setOnClickListener { presenter.onSignIn() }
loginStudentSelectContactDiscord.setOnClickListener { presenter.onDiscordClick() }
loginStudentSelectContactEmail.setOnClickListener { presenter.onEmailClick() }
with(loginStudentSelectRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = loginAdapter
}
loginStudentSelectRecycler.adapter = loginAdapter
}
}
override fun updateData(data: List<Pair<StudentWithSemesters, Boolean>>) {
with(loginAdapter) {
items = data
notifyDataSetChanged()
}
override fun updateData(data: List<LoginStudentSelectItem>) {
loginAdapter.submitList(data)
}
override fun openMainView() {
startActivity(MainActivity.getStartIntent(requireContext()))
requireActivity().finish()
override fun navigateToSymbol(loginData: LoginData) {
(requireActivity() as LoginActivity).navigateToSymbolFragment(loginData)
}
override fun navigateToNext() {
(requireActivity() as LoginActivity).navigateToNotifications()
}
override fun showProgress(show: Boolean) {
binding.loginStudentSelectProgress.visibility = if (show) VISIBLE else GONE
binding.loginStudentSelectProgress.isVisible = show
}
override fun showContent(show: Boolean) {
binding.loginStudentSelectContent.visibility = if (show) VISIBLE else GONE
binding.loginStudentSelectContent.isVisible = show
}
override fun enableSignIn(enable: Boolean) {
binding.loginStudentSelectSignIn.isEnabled = enable
}
override fun showContact(show: Boolean) {
binding.loginStudentSelectContact.visibility = if (show) VISIBLE else GONE
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
override fun openDiscordInvite() {
context?.openInternetBrowser("https://discord.gg/vccAQBr", ::showMessage)
}
@ -111,12 +113,19 @@ class LoginStudentSelectFragment :
email = "wulkanowyinc@gmail.com",
subject = requireContext().getString(R.string.login_email_subject),
body = requireContext().getString(
R.string.login_email_text, appInfo.systemModel,
R.string.login_email_text,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
appInfo.versionName,
"${appInfo.versionName}-${appInfo.buildFlavor}",
"Select users to log in",
preferencesRepository.installationId,
lastError
)
)
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
}

View File

@ -0,0 +1,50 @@
package io.github.wulkanowy.ui.modules.login.studentselect
import io.github.wulkanowy.data.pojos.RegisterStudent
import io.github.wulkanowy.data.pojos.RegisterSymbol
import io.github.wulkanowy.data.pojos.RegisterUnit
sealed class LoginStudentSelectItem(val type: LoginStudentSelectItemType) {
data class EmptySymbolsHeader(
val isExpanded: Boolean,
val onClick: () -> Unit,
) : LoginStudentSelectItem(LoginStudentSelectItemType.EMPTY_SYMBOLS_HEADER)
data class SymbolHeader(
val symbol: RegisterSymbol,
val humanReadableName: String?,
val isErrorExpanded: Boolean,
val onClick: (RegisterSymbol) -> Unit,
) : LoginStudentSelectItem(LoginStudentSelectItemType.SYMBOL_HEADER)
data class SchoolHeader(
val unit: RegisterUnit,
val isErrorExpanded: Boolean,
val onClick: (RegisterUnit) -> Unit,
) : LoginStudentSelectItem(LoginStudentSelectItemType.SCHOOL_HEADER)
data class Student(
val symbol: RegisterSymbol,
val unit: RegisterUnit,
val student: RegisterStudent,
val isEnabled: Boolean,
val isSelected: Boolean,
val onClick: (Student) -> Unit,
) : LoginStudentSelectItem(LoginStudentSelectItemType.STUDENT)
data class Help(
val onEnterSymbolClick: () -> Unit,
val onContactUsClick: () -> Unit,
val onDiscordClick: () -> Unit,
val isSymbolButtonVisible: Boolean,
) : LoginStudentSelectItem(LoginStudentSelectItemType.HELP)
}
enum class LoginStudentSelectItemType {
EMPTY_SYMBOLS_HEADER,
SYMBOL_HEADER,
SCHOOL_HEADER,
STUDENT,
HELP,
}

View File

@ -1,15 +1,23 @@
package io.github.wulkanowy.ui.modules.login.studentselect
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.dataOrNull
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.mappers.mapToStudentWithSemesters
import io.github.wulkanowy.data.pojos.RegisterStudent
import io.github.wulkanowy.data.pojos.RegisterSymbol
import io.github.wulkanowy.data.pojos.RegisterUnit
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.sdk.scrapper.login.AccountPermissionException
import io.github.wulkanowy.services.sync.SyncManager
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.ifNullOrBlank
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
@ -19,18 +27,30 @@ class LoginStudentSelectPresenter @Inject constructor(
studentRepository: StudentRepository,
private val loginErrorHandler: LoginErrorHandler,
private val syncManager: SyncManager,
private val analytics: AnalyticsHelper
private val analytics: AnalyticsHelper,
private val appInfo: AppInfo,
) : BasePresenter<LoginStudentSelectView>(loginErrorHandler, studentRepository) {
private var lastError: Throwable? = null
private val selectedStudents = mutableListOf<StudentWithSemesters>()
private lateinit var registerUser: RegisterUser
private lateinit var loginData: LoginData
fun onAttachView(view: LoginStudentSelectView, students: List<StudentWithSemesters>) {
private lateinit var students: List<StudentWithSemesters>
private var isEmptySymbolsExpanded = false
private var expandedSymbolError: RegisterSymbol? = null
private var expandedSchoolError: RegisterUnit? = null
private val selectedStudents = mutableListOf<LoginStudentSelectItem.Student>()
fun onAttachView(
view: LoginStudentSelectView,
loginData: LoginData,
registerUser: RegisterUser,
) {
super.onAttachView(view)
with(view) {
initView()
showContact(false)
enableSignIn(false)
loginErrorHandler.onStudentDuplicate = {
showMessage(it)
@ -38,50 +58,171 @@ class LoginStudentSelectPresenter @Inject constructor(
}
}
if (students.size == 1) registerStudents(students)
loadData(students)
this.loginData = loginData
this.registerUser = registerUser
loadData()
}
private fun loadData() {
resetSelectedState()
resourceFlow { studentRepository.getSavedStudents(false) }.onEach {
students = it.dataOrNull.orEmpty()
when (it) {
is Resource.Loading -> Timber.d("Login student select students load started")
is Resource.Success -> refreshItems()
is Resource.Error -> {
errorHandler.dispatch(it.error)
lastError = it.error
refreshItems()
}
}
}.launch()
}
private fun createItems(): List<LoginStudentSelectItem> = buildList {
val notEmptySymbols = registerUser.symbols.filter { it.schools.isNotEmpty() }
val emptySymbols = registerUser.symbols.filter { it.schools.isEmpty() }
if (emptySymbols.isNotEmpty() && notEmptySymbols.isNotEmpty() && emptySymbols.any { it.symbol == loginData.symbol }) {
add(createEmptySymbolItem(emptySymbols.first { it.symbol == loginData.symbol }))
}
addAll(createNotEmptySymbolItems(notEmptySymbols, students))
addAll(createEmptySymbolItems(emptySymbols, notEmptySymbols.isNotEmpty()))
val helpItem = LoginStudentSelectItem.Help(
onEnterSymbolClick = ::onEnterSymbol,
onContactUsClick = ::onEmailClick,
onDiscordClick = ::onDiscordClick,
isSymbolButtonVisible = "login" !in loginData.baseUrl,
)
add(helpItem)
}
private fun createNotEmptySymbolItems(
notEmptySymbols: List<RegisterSymbol>,
students: List<StudentWithSemesters>,
) = buildList {
notEmptySymbols.forEach { registerSymbol ->
val symbolHeader = LoginStudentSelectItem.SymbolHeader(
symbol = registerSymbol,
humanReadableName = view?.symbols?.get(registerSymbol.symbol),
isErrorExpanded = expandedSymbolError == registerSymbol,
onClick = ::onSymbolItemClick,
)
add(symbolHeader)
registerSymbol.schools.forEach { registerUnit ->
val schoolHeader = LoginStudentSelectItem.SchoolHeader(
unit = registerUnit,
isErrorExpanded = expandedSchoolError == registerUnit,
onClick = ::onUnitItemClick,
)
add(schoolHeader)
registerUnit.students.forEach {
add(createStudentItem(it, registerSymbol, registerUnit, students))
}
}
}
}
private fun createStudentItem(
student: RegisterStudent,
symbol: RegisterSymbol,
school: RegisterUnit,
students: List<StudentWithSemesters>,
) = LoginStudentSelectItem.Student(
symbol = symbol,
unit = school,
student = student,
onClick = ::onItemSelected,
isEnabled = students.none {
it.student.email == registerUser.login
&& it.student.symbol == symbol.symbol
&& it.student.studentId == student.studentId
&& it.student.schoolSymbol == school.schoolId
&& it.student.classId == student.classId
},
isSelected = selectedStudents
.filter { it.symbol.symbol == symbol.symbol }
.filter { it.unit.schoolId == school.schoolId }
.filter { it.student.studentId == student.studentId }
.filter { it.student.classId == student.classId }
.size == 1,
)
private fun createEmptySymbolItems(
emptySymbols: List<RegisterSymbol>,
isNotEmptySymbolsExist: Boolean,
) = buildList {
val filteredEmptySymbols = emptySymbols.filter {
it.error !is AccountPermissionException
}.ifEmpty { emptySymbols.takeIf { !isNotEmptySymbolsExist }.orEmpty() }
if (filteredEmptySymbols.isNotEmpty() && isNotEmptySymbolsExist) {
val emptyHeader = LoginStudentSelectItem.EmptySymbolsHeader(
isExpanded = isEmptySymbolsExpanded,
onClick = ::onEmptySymbolsToggle,
)
add(emptyHeader)
if (isEmptySymbolsExpanded) {
filteredEmptySymbols.forEach {
add(createEmptySymbolItem(it))
}
}
}
if (filteredEmptySymbols.isNotEmpty() && !isNotEmptySymbolsExist) {
filteredEmptySymbols.forEach {
add(createEmptySymbolItem(it))
}
}
}
private fun createEmptySymbolItem(registerSymbol: RegisterSymbol) =
LoginStudentSelectItem.SymbolHeader(
symbol = registerSymbol,
humanReadableName = view?.symbols?.get(registerSymbol.symbol),
isErrorExpanded = expandedSymbolError == registerSymbol,
onClick = ::onSymbolItemClick,
)
fun onSignIn() {
registerStudents(selectedStudents)
}
fun onItemSelected(studentWithSemester: StudentWithSemesters, alreadySaved: Boolean) {
if (alreadySaved) return
private fun onEmptySymbolsToggle() {
isEmptySymbolsExpanded = !isEmptySymbolsExpanded
refreshItems()
}
private fun onItemSelected(item: LoginStudentSelectItem.Student) {
if (!item.isEnabled) return
selectedStudents
.removeAll { it == studentWithSemester }
.let { if (!it) selectedStudents.add(studentWithSemester) }
.removeAll {
it.student.studentId == item.student.studentId &&
it.student.classId == item.student.classId &&
it.unit.schoolId == item.unit.schoolId &&
it.symbol.symbol == item.symbol.symbol
}
.let { if (!it) selectedStudents.add(item) }
view?.enableSignIn(selectedStudents.isNotEmpty())
refreshItems()
}
private fun compareStudents(a: Student, b: Student): Boolean {
return a.email == b.email
&& a.symbol == b.symbol
&& a.studentId == b.studentId
&& a.schoolSymbol == b.schoolSymbol
&& a.classId == b.classId
private fun onSymbolItemClick(symbol: RegisterSymbol) {
expandedSymbolError = if (symbol != expandedSymbolError) symbol else null
refreshItems()
}
private fun loadData(studentsWithSemesters: List<StudentWithSemesters>) {
resetSelectedState()
resourceFlow { studentRepository.getSavedStudents(false) }.onEach {
when (it) {
is Resource.Loading -> Timber.d("Login student select students load started")
is Resource.Success -> view?.updateData(studentsWithSemesters.map { studentWithSemesters ->
studentWithSemesters to it.data.any { item ->
compareStudents(studentWithSemesters.student, item.student)
}
})
is Resource.Error -> {
errorHandler.dispatch(it.error)
lastError = it.error
view?.updateData(studentsWithSemesters.map { student -> student to false })
}
}
}.launch()
private fun onUnitItemClick(unit: RegisterUnit) {
expandedSchoolError = if (unit != expandedSchoolError) unit else null
refreshItems()
}
private fun resetSelectedState() {
@ -89,7 +230,20 @@ class LoginStudentSelectPresenter @Inject constructor(
view?.enableSignIn(false)
}
private fun registerStudents(studentsWithSemesters: List<StudentWithSemesters>) {
private fun refreshItems() {
view?.updateData(createItems())
}
private fun registerStudents(students: List<LoginStudentSelectItem>) {
val studentsWithSemesters = students
.filterIsInstance<LoginStudentSelectItem.Student>().map { item ->
item.student.mapToStudentWithSemesters(
user = registerUser,
symbol = item.symbol,
unit = item.unit,
colors = appInfo.defaultColorsForAvatar,
)
}
resourceFlow { studentRepository.saveStudents(studentsWithSemesters) }
.logResourceStatus("registration")
.onEach {
@ -100,14 +254,13 @@ class LoginStudentSelectPresenter @Inject constructor(
}
is Resource.Success -> {
syncManager.startOneTimeSyncWorker(quiet = true)
view?.openMainView()
view?.navigateToNext()
logRegisterEvent(studentsWithSemesters)
}
is Resource.Error -> {
view?.apply {
showProgress(false)
showContent(true)
showContact(true)
}
lastError = it.error
loginErrorHandler.dispatch(it.error)
@ -117,12 +270,22 @@ class LoginStudentSelectPresenter @Inject constructor(
}.launch("register")
}
fun onDiscordClick() {
private fun onEnterSymbol() {
view?.navigateToSymbol(loginData)
}
private fun onDiscordClick() {
view?.openDiscordInvite()
}
fun onEmailClick() {
view?.openEmail(lastError?.message.ifNullOrBlank { "empty" })
private fun onEmailClick() {
view?.openEmail(lastError?.message.ifNullOrBlank {
registerUser.symbols.flatMap { symbol ->
symbol.schools.map { it.error?.message } + symbol.error?.message
}.filterNotNull().distinct().joinToString("; ") {
it.take(46) + "..."
}.ifEmpty { "blank" }
})
}
private fun logRegisterEvent(

View File

@ -1,15 +1,19 @@
package io.github.wulkanowy.ui.modules.login.studentselect
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.login.LoginData
interface LoginStudentSelectView : BaseView {
val symbols: Map<String, String>
fun initView()
fun updateData(data: List<Pair<StudentWithSemesters, Boolean>>)
fun updateData(data: List<LoginStudentSelectItem>)
fun openMainView()
fun navigateToSymbol(loginData: LoginData)
fun navigateToNext()
fun showProgress(show: Boolean)
@ -17,8 +21,6 @@ interface LoginStudentSelectView : BaseView {
fun enableSignIn(enable: Boolean)
fun showContact(show: Boolean)
fun openDiscordInvite()
fun openEmail(lastError: String)

View File

@ -12,16 +12,13 @@ import androidx.core.text.parseAsHtml
import androidx.core.widget.doOnTextChanged
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.FragmentLoginSymbolBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.hideSoftInput
import io.github.wulkanowy.utils.openEmailClient
import io.github.wulkanowy.utils.openInternetBrowser
import io.github.wulkanowy.utils.showSoftInput
import io.github.wulkanowy.utils.*
import javax.inject.Inject
@AndroidEntryPoint
@ -34,6 +31,9 @@ class LoginSymbolFragment :
@Inject
lateinit var appInfo: AppInfo
@Inject
lateinit var preferencesRepository: PreferencesRepository
companion object {
private const val SAVED_LOGIN_DATA = "LOGIN_DATA"
@ -42,6 +42,8 @@ class LoginSymbolFragment :
}
}
override val symbolValue: String? get() = binding.loginSymbolName.text?.toString()
override val symbolNameError: CharSequence?
get() = binding.loginSymbolNameLayout.error
@ -50,7 +52,7 @@ class LoginSymbolFragment :
binding = FragmentLoginSymbolBinding.bind(view)
presenter.onAttachView(
view = this,
loginData = requireArguments().getSerializable(SAVED_LOGIN_DATA) as LoginData,
loginData = requireArguments().serializable(SAVED_LOGIN_DATA),
)
}
@ -58,7 +60,7 @@ class LoginSymbolFragment :
(requireActivity() as LoginActivity).showActionBar(true)
with(binding) {
loginSymbolSignIn.setOnClickListener { presenter.attemptLogin(loginSymbolName.text.toString()) }
loginSymbolSignIn.setOnClickListener { presenter.attemptLogin() }
loginSymbolFaq.setOnClickListener { presenter.onFaqClick() }
loginSymbolContactEmail.setOnClickListener { presenter.onEmailClick() }
@ -92,9 +94,13 @@ class LoginSymbolFragment :
}
override fun setErrorSymbolRequire() {
binding.loginSymbolNameLayout.apply {
setErrorSymbol(getString(R.string.error_field_required))
}
override fun setErrorSymbol(message: String) {
with(binding.loginSymbolNameLayout) {
requestFocus()
error = getString(R.string.error_field_required)
error = message
}
}
@ -125,8 +131,8 @@ class LoginSymbolFragment :
binding.loginSymbolContainer.visibility = if (show) VISIBLE else GONE
}
override fun navigateToStudentSelect(studentsWithSemesters: List<StudentWithSemesters>) {
(activity as? LoginActivity)?.navigateToStudentSelect(studentsWithSemesters)
override fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser) {
(activity as? LoginActivity)?.navigateToStudentSelect(loginData, registerUser)
}
override fun onSaveInstanceState(outState: Bundle) {
@ -159,8 +165,9 @@ class LoginSymbolFragment :
R.string.login_email_text,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
appInfo.versionName,
"${appInfo.versionName}-${appInfo.buildFlavor}",
"$host/${binding.loginSymbolName.text}",
preferencesRepository.installationId,
lastError
)
)

View File

@ -1,9 +1,12 @@
package io.github.wulkanowy.ui.modules.login.symbol
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.dataOrNull
import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.sdk.scrapper.getNormalizedSymbol
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
@ -23,9 +26,14 @@ class LoginSymbolPresenter @Inject constructor(
lateinit var loginData: LoginData
private var registerUser: RegisterUser? = null
fun onAttachView(view: LoginSymbolView, loginData: LoginData) {
super.onAttachView(view)
this.loginData = loginData
loginErrorHandler.onBadCredentials = {
view.setErrorSymbol(it.orEmpty())
}
with(view) {
initView()
showContact(false)
@ -39,20 +47,24 @@ class LoginSymbolPresenter @Inject constructor(
view?.apply { if (symbolNameError != null) clearSymbolError() }
}
fun attemptLogin(symbol: String) {
if (symbol.isBlank()) {
fun attemptLogin() {
if (view?.symbolValue.isNullOrBlank()) {
view?.setErrorSymbolRequire()
return
}
loginData = loginData.copy(
symbol = view?.symbolValue?.getNormalizedSymbol(),
)
resourceFlow {
studentRepository.getStudentsScrapper(
studentRepository.getUserSubjectsFromScrapper(
email = loginData.login,
password = loginData.password,
scrapperBaseUrl = loginData.baseUrl,
symbol = symbol,
symbol = view?.symbolValue.orEmpty(),
)
}.onEach {
registerUser = it.dataOrNull
when (it) {
is Resource.Loading -> view?.run {
Timber.i("Login with symbol started")
@ -61,7 +73,7 @@ class LoginSymbolPresenter @Inject constructor(
showContent(false)
}
is Resource.Success -> {
when (it.data.size) {
when (it.data.symbols.size) {
0 -> {
Timber.i("Login with symbol result: Empty student list")
view?.run {
@ -71,15 +83,14 @@ class LoginSymbolPresenter @Inject constructor(
}
else -> {
Timber.i("Login with symbol result: Success")
view?.navigateToStudentSelect(requireNotNull(it.data))
view?.navigateToStudentSelect(loginData, requireNotNull(it.data))
}
}
analytics.logEvent(
"registration_symbol",
"success" to true,
"students" to it.data.size,
"scrapperBaseUrl" to loginData.baseUrl,
"symbol" to symbol,
"symbol" to view?.symbolValue,
"error" to "No error"
)
}
@ -90,7 +101,7 @@ class LoginSymbolPresenter @Inject constructor(
"success" to false,
"students" to -1,
"scrapperBaseUrl" to loginData.baseUrl,
"symbol" to symbol,
"symbol" to view?.symbolValue,
"error" to it.error.message.ifNullOrBlank { "No message" }
)
loginErrorHandler.dispatch(it.error)
@ -111,6 +122,12 @@ class LoginSymbolPresenter @Inject constructor(
}
fun onEmailClick() {
view?.openEmail(loginData.baseUrl, lastError?.message.ifNullOrBlank { "empty" })
view?.openEmail(loginData.baseUrl, lastError?.message.ifNullOrBlank {
registerUser?.symbols?.flatMap { symbol ->
symbol.schools.map { it.error?.message } + symbol.error?.message
}?.filterNotNull()?.distinct()?.joinToString(";") {
it.take(46) + "..."
} ?: "blank"
})
}
}

View File

@ -1,10 +1,13 @@
package io.github.wulkanowy.ui.modules.login.symbol
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.login.LoginData
interface LoginSymbolView : BaseView {
val symbolValue: String?
val symbolNameError: CharSequence?
fun initView()
@ -15,6 +18,8 @@ interface LoginSymbolView : BaseView {
fun setErrorSymbolRequire()
fun setErrorSymbol(message: String)
fun clearSymbolError()
fun clearAndFocusSymbol()
@ -27,7 +32,7 @@ interface LoginSymbolView : BaseView {
fun showContent(show: Boolean)
fun navigateToStudentSelect(studentsWithSemesters: List<StudentWithSemesters>)
fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser)
fun showContact(show: Boolean)

View File

@ -6,6 +6,8 @@ import android.os.Build.VERSION_CODES.P
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.activity.OnBackPressedCallback
import androidx.activity.addCallback
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment
@ -50,6 +52,8 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
@Inject
lateinit var appInfo: AppInfo
private var onBackCallback: OnBackPressedCallback? = null
private var accountMenu: MenuItem? = null
private val overlayProvider by lazy { ElevationOverlayProvider(this) }
@ -88,6 +92,9 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
this.savedInstanceState = savedInstanceState
messageContainer = binding.mainMessageContainer
updateHelper.messageContainer = binding.mainFragmentContainer
onBackCallback = onBackPressedDispatcher.addCallback(this, enabled = false) {
presenter.onBackPressed()
}
val destination = intent.getStringExtra(EXTRA_START_DESTINATION)
?.takeIf { savedInstanceState == null }
@ -266,6 +273,7 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
analytics.popCurrentScreen(navController.currentFrag!!::class.simpleName)
navController.pushFragment(fragment)
onBackCallback?.isEnabled = !isRootView
}
override fun popView(depth: Int) {
@ -273,10 +281,7 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
analytics.popCurrentScreen(navController.currentFrag!!::class.simpleName)
navController.safelyPopFragments(depth)
}
override fun onBackPressed() {
presenter.onBackPressed { super.onBackPressed() }
onBackCallback?.isEnabled = !isRootView
}
override fun showStudentAvatar(student: Student) {

View File

@ -139,12 +139,9 @@ class MainPresenter @Inject constructor(
return true
}
fun onBackPressed(default: () -> Unit) {
fun onBackPressed() {
Timber.i("Back pressed in main view")
view?.run {
if (isRootView) default()
else popView()
}
view?.popView()
}
fun onTabSelected(index: Int, wasSelected: Boolean): Boolean {

View File

@ -0,0 +1,81 @@
package io.github.wulkanowy.ui.modules.message.mailboxchooser
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil.ItemCallback
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.MailboxType
import io.github.wulkanowy.databinding.ItemMailboxChooserBinding
import javax.inject.Inject
class MailboxChooserAdapter @Inject constructor() :
ListAdapter<MailboxChooserItem, MailboxChooserAdapter.ItemViewHolder>(Differ) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder(
ItemMailboxChooserBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
)
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
holder.bind(getItem(position))
}
class ItemViewHolder(
private val binding: ItemMailboxChooserBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: MailboxChooserItem) {
with(binding) {
mailboxItemName.text = item.mailbox?.getFirstLine()
?: root.resources.getString(R.string.message_chip_all_mailboxes)
mailboxItemSchool.text = item.mailbox?.getSecondLine()
mailboxItemSchool.isVisible = !item.isAll
root.setOnClickListener { item.onClickListener(item.mailbox) }
}
}
private fun Mailbox.getFirstLine() = buildString {
if (studentName.isNotBlank() && studentName != userName) {
append(studentName)
append(" - ")
}
append(userName)
}
private fun Mailbox.getSecondLine() = buildString {
append(schoolNameShort)
append(" - ")
append(getMailboxType(type))
}
private fun getMailboxType(type: MailboxType): String = when (type) {
MailboxType.STUDENT -> R.string.message_mailbox_type_student
MailboxType.PARENT -> R.string.message_mailbox_type_parent
MailboxType.GUARDIAN -> R.string.message_mailbox_type_guardian
MailboxType.EMPLOYEE -> R.string.message_mailbox_type_employee
MailboxType.UNKNOWN -> null
}.let { it?.let { it1 -> binding.root.resources.getString(it1) }.orEmpty() }
}
private object Differ : ItemCallback<MailboxChooserItem>() {
override fun areItemsTheSame(
oldItem: MailboxChooserItem,
newItem: MailboxChooserItem
): Boolean {
return oldItem.mailbox?.globalKey == newItem.mailbox?.globalKey
}
override fun areContentsTheSame(
oldItem: MailboxChooserItem,
newItem: MailboxChooserItem
): Boolean {
return oldItem == newItem
}
}
}

View File

@ -0,0 +1,75 @@
package io.github.wulkanowy.ui.modules.message.mailboxchooser
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.setFragmentResult
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.databinding.DialogMailboxChooserBinding
import io.github.wulkanowy.ui.base.BaseDialogFragment
import io.github.wulkanowy.utils.parcelableArray
import javax.inject.Inject
@AndroidEntryPoint
class MailboxChooserDialog : BaseDialogFragment<DialogMailboxChooserBinding>(), MailboxChooserView {
@Inject
lateinit var presenter: MailboxChooserPresenter
@Inject
lateinit var mailboxAdapter: MailboxChooserAdapter
companion object {
const val LISTENER_KEY = "mailbox_selected"
const val MAILBOX_KEY = "selected_mailbox"
const val REQUIRED_KEY = "is_mailbox_required"
fun newInstance(mailboxes: List<Mailbox>, isMailboxRequired: Boolean, folder: String) =
MailboxChooserDialog().apply {
arguments = bundleOf(
MAILBOX_KEY to mailboxes.toTypedArray(),
REQUIRED_KEY to isMailboxRequired,
LISTENER_KEY to folder,
)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = DialogMailboxChooserBinding.inflate(inflater).apply { binding = this }.root
@Suppress("UNCHECKED_CAST")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
presenter.onAttachView(
view = this,
requireMailbox = requireArguments().getBoolean(REQUIRED_KEY, false),
mailboxes = requireArguments().parcelableArray<Mailbox>(MAILBOX_KEY).orEmpty().toList(),
)
}
override fun initView() {
binding.accountQuickDialogRecycler.adapter = mailboxAdapter
}
override fun submitData(items: List<MailboxChooserItem>) {
mailboxAdapter.submitList(items)
}
override fun onMailboxSelected(item: Mailbox?) {
setFragmentResult(
requestKey = requireArguments().getString(LISTENER_KEY).orEmpty(),
result = bundleOf(MAILBOX_KEY to item),
)
dismiss()
}
}

View File

@ -0,0 +1,9 @@
package io.github.wulkanowy.ui.modules.message.mailboxchooser
import io.github.wulkanowy.data.db.entities.Mailbox
data class MailboxChooserItem(
val mailbox: Mailbox? = null,
val isAll: Boolean = false,
val onClickListener: (Mailbox?) -> Unit,
)

View File

@ -0,0 +1,38 @@
package io.github.wulkanowy.ui.modules.message.mailboxchooser
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import timber.log.Timber
import javax.inject.Inject
class MailboxChooserPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository
) : BasePresenter<MailboxChooserView>(errorHandler, studentRepository) {
fun onAttachView(view: MailboxChooserView, mailboxes: List<Mailbox>, requireMailbox: Boolean) {
super.onAttachView(view)
view.initView()
Timber.i("Mailbox chooser view was initialized")
view.submitData(getMailboxItems(mailboxes, requireMailbox))
}
private fun getMailboxItems(
mailboxes: List<Mailbox>,
requireMailbox: Boolean,
): List<MailboxChooserItem> = buildList {
if (!requireMailbox) {
add(MailboxChooserItem(isAll = true, onClickListener = ::onMailboxSelect))
}
addAll(mailboxes.map {
MailboxChooserItem(mailbox = it, isAll = false, onClickListener = ::onMailboxSelect)
})
}
fun onMailboxSelect(item: Mailbox?) {
view?.onMailboxSelected(item)
}
}

View File

@ -0,0 +1,13 @@
package io.github.wulkanowy.ui.modules.message.mailboxchooser
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.ui.base.BaseView
interface MailboxChooserView : BaseView {
fun initView()
fun submitData(items: List<MailboxChooserItem>)
fun onMailboxSelected(item: Mailbox?)
}

View File

@ -1,6 +1,5 @@
package io.github.wulkanowy.ui.modules.message.preview
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -74,15 +73,20 @@ class MessagePreviewAdapter @Inject constructor() :
}
}
@SuppressLint("SetTextI18n")
private fun bindMessage(holder: MessageViewHolder, message: Message) {
val context = holder.binding.root.context
val recipientCount = (message.unreadBy ?: 0) + (message.readBy ?: 0)
val isReceived = message.unreadBy == null
val readTextValue = when {
!message.unread -> R.string.all_yes
else -> R.string.all_no
val readText = when {
recipientCount > 1 -> {
context.getString(R.string.message_read_by, message.readBy, recipientCount)
}
message.readBy == 1 || (isReceived && !message.unread) -> {
context.getString(R.string.message_read, context.getString(R.string.all_yes))
}
else -> context.getString(R.string.message_read, context.getString(R.string.all_no))
}
val readText = context.getString(R.string.message_read, context.getString(readTextValue))
with(holder.binding) {
messagePreviewSubject.text = message.subject.ifBlank {

View File

@ -13,6 +13,7 @@ import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.core.content.getSystemService
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
@ -23,6 +24,7 @@ import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.message.send.SendMessageActivity
import io.github.wulkanowy.utils.serializable
import io.github.wulkanowy.utils.shareText
import javax.inject.Inject
@ -66,13 +68,12 @@ class MessagePreviewFragment :
companion object {
const val MESSAGE_ID_KEY = "message_id"
fun newInstance(message: Message): MessagePreviewFragment {
return MessagePreviewFragment().apply {
arguments = Bundle().apply { putSerializable(MESSAGE_ID_KEY, message) }
}
fun newInstance(message: Message) = MessagePreviewFragment().apply {
arguments = bundleOf(MESSAGE_ID_KEY to message)
}
}
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
@ -83,8 +84,8 @@ class MessagePreviewFragment :
binding = FragmentMessagePreviewBinding.bind(view)
messageContainer = binding.messagePreviewContainer
presenter.onAttachView(
this,
(savedInstanceState ?: arguments)?.getSerializable(MESSAGE_ID_KEY) as? Message
view = this,
message = (savedInstanceState ?: arguments)?.serializable(MESSAGE_ID_KEY),
)
}

View File

@ -6,7 +6,6 @@ import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageAttachment
import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.repositories.MailboxRepository
import io.github.wulkanowy.data.repositories.MessageRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
@ -21,7 +20,6 @@ class MessagePreviewPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val messageRepository: MessageRepository,
private val mailboxRepository: MailboxRepository,
private val analytics: AnalyticsHelper
) : BasePresenter<MessagePreviewView>(errorHandler, studentRepository) {
@ -187,7 +185,7 @@ class MessagePreviewPresenter @Inject constructor(
presenterScope.launch {
runCatching {
val student = studentRepository.getCurrentStudent(decryptPass = true)
val mailbox = mailboxRepository.getMailbox(student)
val mailbox = messageRepository.getMailboxByStudent(student)
messageRepository.deleteMessage(student, mailbox, message!!)
}
.onFailure {

View File

@ -19,11 +19,16 @@ import androidx.core.text.toHtml
import androidx.core.widget.doOnTextChanged
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.databinding.ActivitySendMessageBinding
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog
import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog.Companion.MAILBOX_KEY
import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog.Companion.LISTENER_KEY
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.hideSoftInput
import io.github.wulkanowy.utils.nullableSerializable
import io.github.wulkanowy.utils.showSoftInput
import javax.inject.Inject
@ -100,13 +105,17 @@ class SendMessageActivity : BaseActivity<SendMessagePresenter, ActivitySendMessa
formSubjectValue = binding.sendMessageSubject.text.toString()
formContentValue =
binding.sendMessageMessageContent.text.toString().parseAsHtml().toString()
binding.sendMessageFrom.setOnClickListener { presenter.onOpenMailboxChooser() }
presenter.onAttachView(
view = this,
reason = intent.getSerializableExtra(EXTRA_REASON) as? String,
message = intent.getSerializableExtra(EXTRA_MESSAGE) as? Message,
reply = intent.getSerializableExtra(EXTRA_REPLY) as? Boolean
reason = intent.nullableSerializable(EXTRA_REASON),
message = intent.nullableSerializable(EXTRA_MESSAGE),
reply = intent.nullableSerializable(EXTRA_REPLY)
)
supportFragmentManager.setFragmentResultListener(LISTENER_KEY, this) { _, bundle ->
presenter.onMailboxSelected(bundle.nullableSerializable(MAILBOX_KEY))
}
}
@SuppressLint("ClickableViewAccessibility")
@ -205,6 +214,14 @@ class SendMessageActivity : BaseActivity<SendMessagePresenter, ActivitySendMessa
}
}
override fun showMailboxChooser(mailboxes: List<Mailbox>) {
MailboxChooserDialog.newInstance(
mailboxes = mailboxes,
isMailboxRequired = true,
folder = LISTENER_KEY,
).show(supportFragmentManager, "chooser")
}
override fun popView() {
finish()
}

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