1
0

Compare commits

...

62 Commits

Author SHA1 Message Date
9f87b92937 Merge branch 'release/0.19.0' 2020-06-14 22:44:36 +02:00
c13f12f729 Version 0.19.0 2020-06-14 22:40:36 +02:00
dfe7981e7f New Crowdin translations (#874)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
2020-06-14 22:37:58 +02:00
6e1ddb482e Message fuzzy search (#869) 2020-06-14 14:05:24 +02:00
924bcb0d64 Message sharing and printing (#866) 2020-06-14 00:50:09 +02:00
a6682c9b73 Add predicted and final grade notifications (#872) 2020-06-13 17:11:18 +02:00
a529836937 Fix lint errors (#873) 2020-06-13 14:12:01 +02:00
a05da2656a Add account headers in student picker (#871) 2020-06-12 21:35:51 +02:00
30af77614e Fix showing summary summary for subjects without partial grades (#877) 2020-06-11 14:38:04 +02:00
eedaa63771 Bump sonarqube-gradle-plugin from 2.8 to 3.0 (#882) 2020-06-11 12:05:54 +00:00
f151f7bd62 Bump about_libraries from 8.1.6 to 8.2.0 (#879) 2020-06-11 12:05:30 +00:00
00943717a2 Bump firebase-crashlytics from 17.0.0 to 17.0.1 (#880) 2020-06-11 12:02:38 +00:00
8cce81585a Bump firebase-analytics from 17.4.2 to 17.4.3 (#881) 2020-06-11 12:02:10 +00:00
5529ffcf73 Bump fragment-ktx from 1.2.4 to 1.2.5 (#878) 2020-06-11 12:01:52 +00:00
7e6f892e23 Merge tag '0.18.3' into develop
Version 0.18.3
2020-06-10 19:18:31 +02:00
d3a6ea5acf Merge branch 'release/0.18.3' 2020-06-10 19:18:23 +02:00
674a78b661 Version 0.18.3 2020-06-10 19:18:09 +02:00
5c84c8d5b1 Fix force average calc from two semesters (#870) 2020-06-10 17:28:49 +02:00
522a36d670 Fix message deleting (#875) 2020-06-10 16:26:45 +02:00
2d0cfc3e8e Merge tag '0.18.2' into develop
Version 0.18.2
2020-06-02 17:08:02 +02:00
4b6b722f87 Merge branch 'release/0.18.2' 2020-06-02 17:07:58 +02:00
419675066f Version 0.18.2 2020-06-02 17:07:52 +02:00
191b1ad022 Emulate summaries from grade list when summaries are empty (#855) 2020-06-02 15:51:15 +02:00
792e44a9d0 Fix login button state in student select login fragment (#863) 2020-06-02 15:50:32 +02:00
ff5a47b0df New Crowdin translations (#856) 2020-06-02 15:18:41 +02:00
7bf0acb703 Revert "Bump sonarqube-gradle-plugin from 2.8 to 3.0" (#864)
This reverts commit ab7d30c995.
2020-06-02 15:13:41 +02:00
ba5dbf90d8 Fixes in updating adapter items (#854) 2020-06-02 01:04:41 +02:00
54f41aaa63 Fix too many alarms on samsung devices (#859) 2020-06-02 01:04:02 +02:00
1db42210e8 Bump about_libraries from 8.1.4 to 8.1.6 (#861)
Bumps `about_libraries` from 8.1.4 to 8.1.6.

Updates `aboutlibraries-plugin` from 8.1.4 to 8.1.6
- [Release notes](https://github.com/mikepenz/AboutLibraries/releases)
- [Changelog](https://github.com/mikepenz/AboutLibraries/blob/develop/gradle-release.gradle)
- [Commits](https://github.com/mikepenz/AboutLibraries/compare/v8.1.4...v8.1.6)

Updates `aboutlibraries-core` from 8.1.4 to 8.1.6
- [Release notes](https://github.com/mikepenz/AboutLibraries/releases)
- [Changelog](https://github.com/mikepenz/AboutLibraries/blob/develop/gradle-release.gradle)
- [Commits](https://github.com/mikepenz/AboutLibraries/compare/v8.1.4...v8.1.6)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-06-02 01:02:16 +02:00
fb554a4a3b Fix capitalization in new message activity (#860) 2020-06-02 01:01:58 +02:00
d8d13c73fb Filter out empty items in grade summary (#857) 2020-06-02 01:01:02 +02:00
5c0160a24d Don't capture click on login student select checkbox (#862) 2020-06-02 00:57:22 +02:00
ab7d30c995 Bump sonarqube-gradle-plugin from 2.8 to 3.0 (#853) 2020-05-30 12:19:52 +00:00
1cfa1f15c0 Bump gradle from 3.6.3 to 4.0.0 (#852) 2020-05-30 11:33:43 +00:00
2149a4db9f Bump about_libraries from 8.1.3 to 8.1.4 (#851) 2020-05-30 11:17:22 +00:00
df57d16d21 Bump dagger from 2.27 to 2.28 (#850) 2020-05-30 11:16:58 +00:00
2ff031005e Fix displaying the feature disabled message in completed lessons (#849) 2020-05-30 13:15:28 +02:00
b9ab85ee55 Merge tag '0.18.1' into develop
Version 0.18.1
2020-05-24 21:20:28 +02:00
064998129e Merge branch 'release/0.18.1' 2020-05-24 21:20:23 +02:00
4044cdd9a5 Version 0.18.1 2020-05-24 21:20:17 +02:00
1ee10a5902 New Crowdin translations (#847) 2020-05-24 21:18:49 +02:00
27b1d076c7 Bump firebase-inappmessaging-ktx from 19.0.6 to 19.0.7 (#844) 2020-05-24 21:03:35 +02:00
c8b32fdb3b Bump firebase-analytics from 17.4.1 to 17.4.2 (#843) 2020-05-24 21:02:42 +02:00
0b4434fdb6 Bump firebase-crashlytics-gradle from 2.1.0 to 2.1.1 (#841) 2020-05-24 20:53:59 +02:00
699fbff082 Fix debug channel (#846) 2020-05-24 20:53:23 +02:00
4c295f2ab4 Bump firebase-messaging from 20.1.7 to 20.2.0 (#839) 2020-05-24 18:42:24 +00:00
dcbaa170db Bump about_libraries from 8.1.2 to 8.1.3 (#845) 2020-05-24 20:36:09 +02:00
c71b533645 Bump firebase-inappmessaging-display-ktx from 19.0.6 to 19.0.7 (#838) 2020-05-24 18:10:47 +00:00
63f2576ff1 Hide advanced login options button (#837) 2020-05-24 20:07:24 +02:00
b744a4182b Open dontkillmyapp.com if no action found in app killer manager (#835) 2020-05-24 20:04:09 +02:00
0c4364609b Show check for updates dialog before report a bug (#834) 2020-05-24 20:03:46 +02:00
3308d7fe6f Wrap long preference titles (#836) 2020-05-24 19:52:01 +02:00
2cdde78c54 Allow access to saturday in timetable and attendance (#833) 2020-05-24 19:48:56 +02:00
428b599be0 Improve firebase logging (#832) 2020-05-24 19:48:14 +02:00
3541ab81b8 Destroy webview in password recover before setting binding to null (#829) 2020-05-24 19:47:20 +02:00
7fa14e5077 Set app_name in build.gradle (#830) 2020-05-24 19:36:40 +02:00
cec1068f2e Fix crash in timetable time left (#826) 2020-05-24 19:34:10 +02:00
f737018548 Add debug statements to get/update methods in grade details adapter (#827) 2020-05-24 19:33:04 +02:00
9c01316178 Fix mark message as read in search mode (#828) 2020-05-24 19:24:01 +02:00
c3a6f8253a Fix grade sorting (#825) 2020-05-24 19:23:35 +02:00
d558c4db66 New Crowdin translations (#817) 2020-05-23 16:34:26 +02:00
722886aaf2 Merge tag '0.18.0' into develop
Version 0.18.0
2020-05-21 00:59:19 +02:00
122 changed files with 3640 additions and 571 deletions

View File

@ -14,7 +14,7 @@ cache:
branches:
only:
- develop
- 0.18.0
- 0.19.0
android:
licenses:

View File

@ -17,9 +17,10 @@ android {
testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 17
targetSdkVersion 29
versionCode 59
versionName "0.18.0"
versionCode 63
versionName "0.19.0"
multiDexEnabled true
resValue "string", "app_name", "Wulkanowy"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
manifestPlaceholders = [
@ -56,6 +57,7 @@ android {
signingConfig signingConfigs.release
}
debug {
resValue "string", "app_name", "Wulkanowy DEV " + defaultConfig.versionCode
applicationIdSuffix ".dev"
versionNameSuffix "-dev"
testCoverageEnabled = project.hasProperty('coverage')
@ -75,8 +77,8 @@ android {
}
}
viewBinding {
enabled = true
buildFeatures {
viewBinding = true
}
lintOptions {
@ -112,7 +114,7 @@ play {
ext {
work_manager = "2.3.4"
room = "2.2.5"
dagger = "2.27"
dagger = "2.28"
chucker = "3.2.0"
mockk = "1.9.2"
}
@ -122,14 +124,14 @@ configurations.all {
}
dependencies {
implementation "io.github.wulkanowy:sdk:0.18.0"
implementation "io.github.wulkanowy:sdk:0.19.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "androidx.core:core-ktx:1.2.0"
implementation "androidx.activity:activity-ktx:1.1.0"
implementation "androidx.appcompat:appcompat:1.2.0-rc01"
implementation "androidx.appcompat:appcompat-resources:1.1.0"
implementation "androidx.fragment:fragment-ktx:1.2.4"
implementation "androidx.fragment:fragment-ktx:1.2.5"
implementation "androidx.annotation:annotation:1.1.0"
implementation "androidx.multidex:multidex:2.0.1"
@ -140,7 +142,7 @@ dependencies {
implementation "androidx.constraintlayout:constraintlayout:1.1.3"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0"
implementation "com.google.android.material:material:1.1.0"
implementation "com.github.wulkanowy:material-chips-input:2.0.1"
implementation "com.github.wulkanowy:material-chips-input:2.1.1"
implementation "com.github.PhilJay:MPAndroidChart:v3.1.0"
implementation "me.zhanghai.android.materialprogressbar:library:1.6.1"
@ -178,12 +180,13 @@ dependencies {
implementation 'com.wdullaer:materialdatetimepicker:4.2.3'
implementation "io.coil-kt:coil:0.11.0"
implementation "io.github.wulkanowy:AppKillerManager:3.0.0"
implementation 'me.xdrop:fuzzywuzzy:1.3.1'
playImplementation 'com.google.firebase:firebase-analytics:17.4.1'
playImplementation 'com.google.firebase:firebase-inappmessaging-display-ktx:19.0.6'
playImplementation "com.google.firebase:firebase-inappmessaging-ktx:19.0.6"
playImplementation 'com.google.firebase:firebase-messaging:20.1.7'
playImplementation 'com.google.firebase:firebase-crashlytics:17.0.0'
playImplementation 'com.google.firebase:firebase-analytics:17.4.3'
playImplementation 'com.google.firebase:firebase-inappmessaging-display-ktx:19.0.7'
playImplementation "com.google.firebase:firebase-inappmessaging-ktx:19.0.7"
playImplementation 'com.google.firebase:firebase-messaging:20.2.0'
playImplementation 'com.google.firebase:firebase-crashlytics:17.0.1'
playImplementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava'
releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker"

View File

@ -1741,4 +1741,4 @@
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd101f5a26a024f62e6fee161e421b882')"
]
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -120,23 +120,23 @@ class Migration13Test : AbstractMigrationTest() {
assertEquals(2, first.diaryId)
}
getSemesters(db, "SELECT * FROM Semesters WHERE student_id = 2 AND class_id = 5").let {
assertTrue { it.single { it.second }.second }
assertEquals(1970, it[0].first.schoolYear)
assertEquals(of(1970, 1, 1), it[0].first.end)
assertEquals(of(1970, 1, 1), it[0].first.start)
assertFalse(it[0].second)
assertFalse(it[1].second)
assertFalse(it[2].second)
assertTrue(it[3].second)
getSemesters(db, "SELECT * FROM Semesters WHERE student_id = 2 AND class_id = 5").let { semesters ->
assertTrue { semesters.single { it.second }.second }
assertEquals(1970, semesters[0].first.schoolYear)
assertEquals(of(1970, 1, 1), semesters[0].first.end)
assertEquals(of(1970, 1, 1), semesters[0].first.start)
assertFalse(semesters[0].second)
assertFalse(semesters[1].second)
assertFalse(semesters[2].second)
assertTrue(semesters[3].second)
}
getSemesters(db, "SELECT * FROM Semesters WHERE student_id = 2 AND class_id = 5").let {
assertTrue { it.single { it.second }.second }
assertFalse(it[0].second)
assertFalse(it[1].second)
assertFalse(it[2].second)
assertTrue(it[3].second)
getSemesters(db, "SELECT * FROM Semesters WHERE student_id = 2 AND class_id = 5").let { semesters ->
assertTrue { semesters.single { it.second }.second }
assertFalse(semesters[0].second)
assertFalse(semesters[1].second)
assertFalse(semesters[2].second)
assertTrue(semesters[3].second)
}
}

View File

@ -10,6 +10,7 @@ import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDate.now
import org.threeten.bp.LocalDate.of
import kotlin.test.assertEquals
@ -35,9 +36,18 @@ class AttendanceLocalTest {
@Test
fun saveAndReadTest() {
attendanceLocal.saveAttendance(listOf(
Attendance(1, 2, 3, of(2018, 9, 10), 0, "", "", false, false, false, false, false, false, false, SentExcuseStatus.ACCEPTED.name),
Attendance(1, 2, 3, of(2018, 9, 14), 0, "", "", false, false, false, false, false, false, false, SentExcuseStatus.WAITING.name),
Attendance(1, 2, 3, of(2018, 9, 17), 0, "", "", false, false, false, false, false, false, false, SentExcuseStatus.ACCEPTED.name)
getAttendanceEntity(
of(2018, 9, 10),
SentExcuseStatus.ACCEPTED
),
getAttendanceEntity(
of(2018, 9, 14),
SentExcuseStatus.WAITING
),
getAttendanceEntity(
of(2018, 9, 17),
SentExcuseStatus.ACCEPTED
)
))
val attendance = attendanceLocal
@ -50,4 +60,25 @@ class AttendanceLocalTest {
assertEquals(attendance[0].date, of(2018, 9, 10))
assertEquals(attendance[1].date, of(2018, 9, 14))
}
private fun getAttendanceEntity(
date: LocalDate,
excuseStatus: SentExcuseStatus
) = Attendance(
studentId = 1,
diaryId = 2,
timeId = 3,
date = date,
number = 0,
subject = "",
name = "",
presence = false,
absence = false,
exemption = false,
lateness = false,
excused = false,
deleted = false,
excusable = false,
excuseStatus = excuseStatus.name
)
}

View File

@ -5,7 +5,6 @@ import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import org.junit.After
import org.junit.Before

View File

@ -1,3 +0,0 @@
<resources>
<string name="app_name">Wulkanowy DEV</string>
</resources>

View File

@ -2,11 +2,8 @@
package io.github.wulkanowy.utils
import android.content.Context
import timber.log.Timber
fun initCrashlytics(context: Context, appInfo: AppInfo) {}
open class TimberTreeNoOp : Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {}
}

View File

@ -1,13 +1,18 @@
package io.github.wulkanowy.utils
import android.app.Activity
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
@Suppress("UNUSED_PARAMETER")
class FirebaseAnalyticsHelper @Inject constructor() {
@Suppress("UNUSED_PARAMETER")
fun logEvent(name: String, vararg params: Pair<String, Any?>) {
// do nothing
}
fun setCurrentScreen(activity: Activity, name: String?) {
// do nothing
}
}

View File

@ -0,0 +1,94 @@
<!doctype html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>%SUBJECT% | Wulkanowy</title>
<style>
@page {
margin: 2.5cm;
size: A4;
}
body {
margin: 0;
font-family: sans-serif;
}
.title {
line-height: 1.5;
letter-spacing: 1pt;
font-size: 24pt;
font-weight: 200;
margin: 0 0 0.5cm;
}
.info {
margin: 0.5cm 0;
}
.info div {
font-size: 14pt;
font-weight: 400;
margin: 0.5cm 0;
}
h4 {
font-weight: 200;
text-transform: uppercase;
letter-spacing: 1pt;
font-size: 10pt;
margin: 0;
margin-bottom: 0.25cm;
font-family: sans-serif;
}
.content {
margin-top: 0.5cm;
font-size: 14pt;
font-weight: 400;
text-align: justify;
font-family: serif;
line-height: 1.5;
}
.content p {
page-break-after: auto;
page-break-inside: auto;
margin-bottom: 0.6cm;
}
.footer {
font-size: 11pt;
font-weight: 200;
display: flex;
align-items: center;
color: rgba(0, 0, 0, 0.5)
margin: 0;
margin-bottom: 0.5cm;
}
.footer .logo {
height: 0.5cm;
width: 0.5cm;
display: block;
margin-right: 0.2cm;
}
</style>
</head>
<body>
<h1 class="title">%SUBJECT%</h1>
<hr>
<div class="info">
%INFO%
</div>
<div class="footer">
<img src="wulkanowy-logo-black.svg" class="logo">
Wulkanowy Dzienniczek
</div>
<hr>
<div class="content">
<h4>Treść wiadomości</h4>
%CONTENT%
</div>
</body>
</html>

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 1024 1024"
xml:space="preserve"
width="1024"
height="1024"><metadata
id="metadata15"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs13" /><style
type="text/css"
id="style2">
.st0{fill:#D32F2F;}
.st1{fill:#AD2A2A;}
.st2{fill:#FFFFFF;}
</style><g
id="layer4"
style="display:none;fill:#808080"><rect
id="XMLID_57_"
x="0"
y="0"
class="st0"
width="3584"
height="1024"
style="display:inline;fill:#808080;stroke-width:1.02195609" /></g><g
id="layer3"
style="display:none;fill:#808080"><path
style="font-style:normal;font-variant:normal;font-weight:300;font-stretch:normal;font-size:341.33334351px;line-height:1.25;font-family:Roboto;-inkscape-font-specification:'Roboto Light';letter-spacing:0px;word-spacing:0px;display:inline;fill:#808080;fill-opacity:1;stroke:none"
d="M 3046.8164,390.66602 3134.3164,542 v 91.33398 L 3524.9824,1024 H 3584 V 732.18359 L 3242.4824,390.66602 h -23.666 l -53.0352,94.63086 -94.6308,-94.63086 z"
id="path18992" /><path
style="font-style:normal;font-variant:normal;font-weight:300;font-stretch:normal;font-size:341.33334351px;line-height:1.25;font-family:Roboto;-inkscape-font-specification:'Roboto Light';letter-spacing:0px;word-spacing:0px;display:inline;fill:#808080;fill-opacity:1;stroke:none"
d="m 2746.9824,390.66602 62,242.66796 L 3199.6484,1024 H 3584 V 940.68359 L 3033.9824,390.66602 h -21 l -21.9043,90.92773 -90.9277,-90.92773 h -18.5 l -25.4043,88.26367 -88.2637,-88.26367 z"
id="path18990" /><path
style="font-style:normal;font-variant:normal;font-weight:300;font-stretch:normal;font-size:341.33334351px;line-height:1.25;font-family:Roboto;-inkscape-font-specification:'Roboto Light';letter-spacing:0px;word-spacing:0px;display:inline;fill:#808080;fill-opacity:1;stroke:none"
d="m 2620.8164,387.33398 c -18.6667,0 -35.1667,4.60982 -49.5,13.83204 -14.3333,9.11111 -25.4451,22.22287 -33.334,39.33398 -7.7778,17 -11.666,36.5549 -11.666,58.66602 v 25 c 0,34.44444 8.7216,61.83463 26.166,82.16796 L 2970.1484,1024 h 323.168 l -623.166,-623.16602 c -14.2222,-9 -30.6673,-13.5 -49.334,-13.5 z"
id="path18988" /><path
style="font-style:normal;font-variant:normal;font-weight:300;font-stretch:normal;font-size:341.33334351px;line-height:1.25;font-family:Roboto;-inkscape-font-specification:'Roboto Light';letter-spacing:0px;word-spacing:0px;display:inline;fill:#808080;fill-opacity:1;stroke:none"
d="M 2293.4824,390.66602 V 633.33398 L 2684.1484,1024 h 423.336 l -633.334,-633.33398 h -20.334 v 139.66601 l -139.666,-139.66601 z"
id="path18984" /><path
style="font-style:normal;font-variant:normal;font-weight:300;font-stretch:normal;font-size:341.33334351px;line-height:1.25;font-family:Roboto;-inkscape-font-specification:'Roboto Light';letter-spacing:0px;word-spacing:0px;display:inline;fill:#808080;fill-opacity:1;stroke:none"
d="M 1864.8164,390.66602 V 633.33398 L 2255.4824,1024 h 413.334 l -633.334,-633.33398 h -25.832 l -60.584,63.75 -63.75,-63.75 z"
id="path18978" /><path
style="font-style:normal;font-variant:normal;font-weight:300;font-stretch:normal;font-size:341.33334351px;line-height:1.25;font-family:Roboto;-inkscape-font-specification:'Roboto Light';letter-spacing:0px;word-spacing:0px;display:inline;fill:#808080;fill-opacity:1;stroke:none"
d="M 1684.8164,390.66602 V 633.33398 L 2075.4824,1024 h 263.334 l -633.334,-633.33398 z"
id="path18976" /><path
style="font-style:normal;font-variant:normal;font-weight:300;font-stretch:normal;font-size:341.33334351px;line-height:1.25;font-family:Roboto;-inkscape-font-specification:'Roboto Light';letter-spacing:0px;word-spacing:0px;display:inline;fill:#808080;fill-opacity:1;stroke:none"
d="m 1133.6504,390.66602 62,242.66796 L 1586.3164,1024 h 467.668 l -633.334,-633.33398 h -21 l -21.9043,90.92773 -90.9277,-90.92773 h -18.5 l -25.4043,88.26367 -88.2637,-88.26367 z"
id="path19059" /><path
style="font-style:normal;font-variant:normal;font-weight:300;font-stretch:normal;font-size:341.33334351px;line-height:1.25;font-family:Roboto;-inkscape-font-specification:'Roboto Light';letter-spacing:0px;word-spacing:0px;display:inline;fill:#808080;fill-opacity:1;stroke:none"
d="m 1456.4824,390.66602 v 167.16796 c 0.5556,24.66667 8.5007,44 23.834,58 L 1888.4824,1024 h 372.168 l -633.334,-633.33398 h -20.666 V 520.5 l -129.834,-129.83398 z"
id="path18966" /><path
style="font-style:normal;font-variant:normal;font-weight:300;font-stretch:normal;font-size:341.33334351px;line-height:1.25;font-family:Roboto;-inkscape-font-specification:'Roboto Light';letter-spacing:0px;word-spacing:0px;display:inline;fill:#808080;fill-opacity:1;stroke:none"
d="M 2146.3164,390.66602 2054.4824,633.33398 2445.1484,1024 h 354.002 l -633.334,-633.33398 z"
id="path18982" /><path
style="display:inline;fill:#808080;stroke-width:0.78179646"
d="M 637.15234,214.95703 487.75,364.35742 466.01562,386.0918 c 0.31273,0.31271 0.54872,0.54666 0.70508,0.85937 0.0782,0.23454 0.23432,0.54671 0.3125,0.78125 0.31272,0.54726 0.47071,1.17339 0.47071,1.79883 0.0782,0.54726 -0.0799,1.01725 -0.31446,1.48633 -0.23454,0.54725 -0.70285,1.40597 -1.09375,1.79687 l 150.8086,149.71485 -23.68946,23.6875 -12.74414,-12.74219 -13.44726,-13.44727 -78.80469,-78.80664 -11.17969,-11.17968 -7.5039,-7.50391 -35.41602,-35.17969 -3.08984,-0.98047 -4.33594,4.26367 v 0.46876 c 0,7.34888 0.38998,15.00865 -1.48633,22.20117 -0.85998,3.28355 -2.34444,6.25595 -4.14258,8.91406 -0.15636,0.15636 -0.23627,0.23426 -0.31445,0.39062 -1.87631,2.57993 -4.06471,4.84619 -6.48828,6.95704 -5.3944,4.53442 -11.25752,8.67896 -17.27734,12.50976 -0.15637,0.0782 -0.23427,0.1562 -0.39063,0.23438 -2.11085,1.40723 -4.3012,2.7354 -6.49023,4.06445 -8.91248,5.39439 -18.37192,10.08772 -28.37891,13.13672 -1.25087,0.31272 -2.42317,-0.001 -3.36133,-0.70508 l -6.01953,5.94141 c 1.25087,0.62543 2.03136,1.87776 1.875,3.51953 -10e-6,0.15636 -0.0762,0.23231 -0.0762,0.38867 0,0.0782 -0.0781,0.23628 -0.0781,0.31445 -1.32905,4.45624 -2.34505,8.98897 -3.2832,13.60156 -0.15636,0.70363 -0.23622,1.33154 -0.39258,2.03516 -0.85997,4.37806 -1.64209,8.83288 -2.3457,13.21094 0.23453,5.3944 0.39263,11.0234 0.31445,16.65234 v 0.39258 c -0.0782,7.66161 -0.78373,15.32114 -2.8164,22.51367 -2.26721,8.28704 -6.64376,15.63728 -10.55274,23.22071 -0.0782,0.15636 -0.15815,0.23426 -0.23633,0.39062 -1.25088,2.42357 -2.49924,4.92399 -3.59375,7.50391 -4.84714,11.33605 -7.42749,23.92328 -10.55468,35.88476 -0.23454,0.70362 -0.39046,1.48578 -0.625,2.26758 0,0.15636 -0.0801,0.23427 -0.0801,0.39063 -2.97082,11.10151 -6.09819,22.28173 -10.94532,32.75781 -1.40724,2.97082 -2.81531,5.86322 -4.3789,8.75586 -0.15636,0.23454 -0.23231,0.46858 -0.38867,0.70312 -0.62544,1.09451 -1.25152,2.26871 -1.87696,3.44141 -0.0782,0.15636 -0.15619,0.23426 -0.23437,0.39062 -3.51809,6.25438 -7.27098,12.43118 -10.78906,18.68555 -5.0035,8.8343 -8.99075,18.13635 -13.83789,27.04883 -0.0782,0.15636 -0.1562,0.23426 -0.23438,0.39062 -0.70362,1.32905 -1.48579,2.65728 -2.26758,3.98633 -5.0035,8.20887 -10.63256,16.0279 -16.57422,23.61133 -0.15635,0.15636 -0.23426,0.3124 -0.39062,0.46875 -0.7818,1.01634 -1.48578,1.95443 -2.26758,2.89258 -3.90898,4.92532 -7.97378,9.85009 -11.96094,14.77539 -0.0782,0.15637 -0.23432,0.23622 -0.3125,0.39258 -8.75612,10.71061 -17.35628,21.49761 -24.54883,33.30273 0,0.70362 -0.15602,1.33159 -0.46874,1.95703 -1.25087,2.42357 -2.65734,4.68971 -3.90821,7.11328 -0.0782,0.15636 0.62511,1.24989 0.46875,1.40625 L 429.86133,1024 H 1463.0215 L 661.85547,222.92969 c -0.93816,2.11087 -5.23681,1.40935 -7.34766,-0.23242 -1.71995,-1.32906 -3.12603,-3.05147 -4.45508,-4.84961 -0.62544,-0.31271 -1.25168,-0.62288 -1.64257,-0.85743 -2.89265,-1.40723 -6.09933,-1.48632 -9.30469,-1.48632 -0.7818,-0.0782 -1.40588,-0.23416 -1.95313,-0.54688 z m -206.12304,191.41992 0.11914,-0.11523 -0.23438,0.0781 z"
id="XMLID_64_" /></g><g
id="layer2"
style="display:inline;fill:#000000;fill-opacity:0.49803922"><path
id="XMLID_42_"
d="m 295.17362,965.05417 c 1.0692,3.47527 0.5346,7.21786 -1.3367,10.29214 l -25.7972,41.83679 c -2.5396,4.1436 -7.2178,6.8169 -12.297,6.8169 H 14.345318 C 3.1176178,1024 -3.6991822,1012.2376 2.3157178,1003.4158 L 157.76692,774.44928 c 0.9356,-1.33663 1.4704,-2.80694 1.8713,-4.27723 l 71.2428,-304.21933 c 0.8021,-3.60893 3.2081,-6.6832 6.6833,-8.55449 l 96.5054,-52.93096 c 3.4753,-1.8713 5.8812,-4.94557 6.6832,-8.68816 l 12.9654,-56.53988 c 2.6733,-11.76242 19.5151,-14.30205 26.1981,-4.00991 l 4.6783,7.48519 c 2.0049,3.20793 2.5396,7.21785 1.2031,10.82678 l -87.9511,254.22895 c -0.6683,2.00497 -0.9355,4.1436 -0.5346,6.28223 l 21.9209,121.63426 c 0.401,2.40595 0.1334,4.94556 -0.9357,7.21785 l -52.2625,117.357 c -1.203,2.80696 -1.4704,5.88123 -0.5347,8.68817 z M 1009.7413,1024 H 843.46322 c -4.8117,0 -9.2228,-2.4059 -11.8959,-6.1485 L 719.69042,860.52891 c -0.6683,-1.0693 -1.3366,-2.13861 -1.7375,-3.3416 l -55.4707,-162.00078 c -1.0692,-3.20793 -3.6088,-6.01489 -6.8169,-7.61886 l -135.8026,-68.56965 c -3.7426,-1.87127 -6.4159,-5.34655 -7.2179,-9.22281 l -20.0495,-99.44603 c -0.2674,-1.60396 -0.9357,-3.20793 -2.005,-4.67824 l -46.1141,-67.76766 c -2.5396,-3.74259 -2.9405,-8.28717 -1.0693,-12.2971 l 28.0694,-60.01513 c 2.1387,-4.54457 6.817,-7.61886 12.1634,-7.88619 l 52.129,-3.07427 c 3.0742,-0.1337 5.8812,-1.20296 8.1536,-3.07427 l 38.3615,-29.80707 c 7.2178,-5.61388 18.1784,-3.20794 22.0546,4.67824 l 132.1937,268.93201 c 0.5346,1.20297 0.9357,2.40595 1.2029,3.60894 l 16.3072,108.13418 c 0.4009,2.53963 1.4701,4.8119 3.2079,6.6832 l 263.31808,288.17958 c 7.7525,8.5545 1.203,22.0546 -10.8269,22.0546 z M 363.20852,182.58501 c 0,-30.60907 19.3812,-56.94088 47.1834,-69.23798 -2.005,-3.3416 -3.2079,-6.95052 -3.2079,-10.82678 0,-14.836705 17.109,-26.866465 38.0942,-26.866465 0.5346,0 0.9356,0 1.4704,0 8.688,-14.43572 25.2624,-24.19318 44.2426,-24.19318 1.3367,0 2.6733,0 4.01,0.1337 1.7377,0.13369 3.4753,-0.66833 4.4109,-2.00497 14.0347,-21.38624 49.5894,-36.62394 91.159,-36.62394 15.3712,0 29.9406,2.13863 42.906,5.74756 3.0744,-5.07924 9.8911,-8.5545 17.7773,-8.5545 8.9556,0 16.5744,4.54458 18.8466,10.82678 10.9606,-12.69809 29.5398,-20.98524 50.6587,-20.98524 33.6834,0 60.9508,21.25257 60.9508,47.45072 0,3.20793 -0.401,6.2822 -1.203,9.35647 -0.5346,2.13864 0.6683,4.27725 2.9407,5.07924 21.5199,7.88618 36.0893,22.85655 36.0893,39.965535 0,19.51495 -18.8466,36.22296 -45.4458,42.77249 -2.1387,0.53466 -3.4753,2.40595 -3.4753,4.41092 0,0.1337 0,0.26731 0,0.40098 0,15.10404 -14.9704,27.5348 -34.218,28.87144 0.1333,0.66833 0.1333,1.33663 0.1333,2.13862 0,29.00509 -55.2031,52.3963 -123.2382,52.3963 -14.7029,0 -28.7377,-1.06932 -41.7031,-3.07427 0,0.26733 0,0.40099 0,0.66832 0,12.02975 -15.5051,21.78723 -34.4854,21.78723 -1.0692,0 -2.0049,0 -2.9405,-0.13369 1.3367,2.9406 2.005,6.01487 2.005,9.22281 0,18.71296 -23.6586,33.81699 -52.9311,33.81699 -3.2079,0 -6.2821,-0.1337 -9.3563,-0.53466 -2.4061,-0.26731 -4.6783,1.20299 -5.2131,3.47529 -2.5396,9.35647 -10.693,16.17333 -20.4504,16.17333 -11.7625,0 -21.119,-10.0248 -21.119,-22.32189 0,-5.74755 2.005,-10.96045 5.3466,-14.83671 1.203,-1.33663 1.6039,-3.20793 0.8019,-4.81191 -1.8713,-3.47526 -2.6733,-7.08419 -2.6733,-10.96044 v 0 c 0,-2.13862 -1.7376,-3.87626 -3.8763,-4.4109 -36.2228,-8.01985 -63.4903,-38.22792 -63.4903,-74.3172 z m 306.8925,726.06294 c 0.5348,1.60398 0.6683,3.20796 0.6683,4.94558 l -7.7525,97.97577 c -0.5346,6.9505 -6.6832,12.4307 -14.1683,12.4307 h -250.219 c -5.3466,0 -10.2921,-3.0743 -12.6982,-7.4852 l -41.3021,-76.72312 c -0.2673,-0.401 -0.401,-0.80199 -0.5347,-1.20298 l -38.8962,-94.23313 c -1.4702,-3.3416 -1.203,-7.21785 0.4011,-10.42581 l 64.5596,-126.31249 c 1.604,-3.07427 1.8712,-6.6832 0.6683,-9.89114 l -31.5447,-87.41626 c -1.0693,-3.07428 -0.9356,-6.54955 0.4011,-9.49015 l 52.6636,-112.14412 c 5.3464,-11.22778 22.8565,-10.29212 26.5991,1.47031 l 16.4407,51.05965 50.124,134.19868 c 1.3367,3.7426 4.5446,6.6832 8.5545,8.01985 l 106.9312,36.49027 c 4.1435,1.47032 7.3516,4.54458 8.6881,8.42084 z"
style="fill:#000000;stroke-width:0.78179646;fill-opacity:0.49803922" /><g
aria-label="WULKANOWY"
style="font-style:normal;font-variant:normal;font-weight:300;font-stretch:normal;font-size:341.33334351px;line-height:1.25;font-family:Roboto;-inkscape-font-specification:'Roboto Light';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:0.49803922;stroke:none"
id="text4752" /></g></svg>

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,17 +1,15 @@
package io.github.wulkanowy.data
import android.app.AlarmManager
import android.content.Context
import android.content.SharedPreferences
import android.content.res.AssetManager
import android.content.res.Resources
import androidx.core.content.getSystemService
import androidx.preference.PreferenceManager
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.strategy.WalledGardenInternetObservingStrategy
import com.chuckerteam.chucker.api.ChuckerCollector
import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.chuckerteam.chucker.api.RetentionManager
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.strategy.WalledGardenInternetObservingStrategy
import dagger.Module
import dagger.Provides
import io.github.wulkanowy.data.db.AppDatabase

View File

@ -68,6 +68,7 @@ import io.github.wulkanowy.data.db.migrations.Migration22
import io.github.wulkanowy.data.db.migrations.Migration23
import io.github.wulkanowy.data.db.migrations.Migration24
import io.github.wulkanowy.data.db.migrations.Migration25
import io.github.wulkanowy.data.db.migrations.Migration26
import io.github.wulkanowy.data.db.migrations.Migration3
import io.github.wulkanowy.data.db.migrations.Migration4
import io.github.wulkanowy.data.db.migrations.Migration5
@ -110,7 +111,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() {
companion object {
const val VERSION_SCHEMA = 25
const val VERSION_SCHEMA = 26
fun getMigrations(sharedPrefProvider: SharedPrefProvider): Array<Migration> {
return arrayOf(
@ -137,7 +138,8 @@ abstract class AppDatabase : RoomDatabase() {
Migration22(),
Migration23(),
Migration24(),
Migration25()
Migration25(),
Migration26()
)
}

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import org.threeten.bp.LocalDateTime
@Entity(tableName = "GradesSummary")
data class GradeSummary(
@ -36,4 +37,16 @@ data class GradeSummary(
) {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
@ColumnInfo(name = "is_predicted_grade_notified")
var isPredictedGradeNotified: Boolean = true
@ColumnInfo(name = "is_final_grade_notified")
var isFinalGradeNotified: Boolean = true
@ColumnInfo(name = "predicted_grade_last_change")
var predictedGradeLastChange: LocalDateTime = LocalDateTime.now()
@ColumnInfo(name = "final_grade_last_change")
var finalGradeLastChange: LocalDateTime = LocalDateTime.now()
}

View File

@ -0,0 +1,14 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration26 : Migration(25, 26) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE GradesSummary ADD COLUMN is_predicted_grade_notified INTEGER NOT NULL DEFAULT 1")
database.execSQL("ALTER TABLE GradesSummary ADD COLUMN is_final_grade_notified INTEGER NOT NULL DEFAULT 1")
database.execSQL("ALTER TABLE GradesSummary ADD COLUMN predicted_grade_last_change INTEGER NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE GradesSummary ADD COLUMN final_grade_last_change INTEGER NOT NULL DEFAULT 0")
}
}

View File

@ -10,7 +10,7 @@ import javax.inject.Singleton
@Singleton
class AppCreatorRepository @Inject constructor(private val assets: AssetManager) {
fun getAppCreators(): Single<List<Contributor>> {
return Single.fromCallable<List<Contributor>> {
return Single.fromCallable {
Gson().fromJson(
assets.open("contributors.json").bufferedReader().use { it.readText() },
Array<Contributor>::class.java

View File

@ -5,7 +5,7 @@ import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.Inter
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Single
@ -22,19 +22,19 @@ class AttendanceRepository @Inject constructor(
) {
fun getAttendance(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean): Single<List<Attendance>> {
return local.getAttendance(semester, start.monday, end.friday).filter { !forceRefresh }
return local.getAttendance(semester, start.monday, end.sunday).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings).flatMap {
if (it) remote.getAttendance(student, semester, start.monday, end.friday)
if (it) remote.getAttendance(student, semester, start.monday, end.sunday)
else Single.error(UnknownHostException())
}.flatMap { newAttendance ->
local.getAttendance(semester, start.monday, end.friday)
local.getAttendance(semester, start.monday, end.sunday)
.toSingle(emptyList())
.doOnSuccess { oldAttendance ->
local.deleteAttendance(oldAttendance.uniqueSubtract(newAttendance))
local.saveAttendance(newAttendance.uniqueSubtract(oldAttendance))
}
}.flatMap {
local.getAttendance(semester, start.monday, end.friday)
local.getAttendance(semester, start.monday, end.sunday)
.toSingle(emptyList())
}).map { list -> list.filter { it.date in start..end } }
}

View File

@ -5,7 +5,7 @@ import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.Inter
import io.github.wulkanowy.data.db.entities.CompletedLesson
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Single
@ -22,20 +22,20 @@ class CompletedLessonsRepository @Inject constructor(
) {
fun getCompletedLessons(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean = false): Single<List<CompletedLesson>> {
return local.getCompletedLessons(semester, start.monday, end.friday).filter { !forceRefresh }
return local.getCompletedLessons(semester, start.monday, end.sunday).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) remote.getCompletedLessons(student, semester, start.monday, end.friday)
if (it) remote.getCompletedLessons(student, semester, start.monday, end.sunday)
else Single.error(UnknownHostException())
}.flatMap { new ->
local.getCompletedLessons(semester, start.monday, end.friday)
local.getCompletedLessons(semester, start.monday, end.sunday)
.toSingle(emptyList())
.doOnSuccess { old ->
local.deleteCompleteLessons(old.uniqueSubtract(new))
local.saveCompletedLessons(new.uniqueSubtract(old))
}
}.flatMap {
local.getCompletedLessons(semester, start.monday, end.friday)
local.getCompletedLessons(semester, start.monday, end.sunday)
.toSingle(emptyList())
}).map { list -> list.filter { it.date in start..end } }
}

View File

@ -13,7 +13,7 @@ class ExamLocal @Inject constructor(private val examDb: ExamDao) {
fun getExams(semester: Semester, startDate: LocalDate, endDate: LocalDate): Maybe<List<Exam>> {
return examDb.loadAll(semester.diaryId, semester.studentId, startDate, endDate)
.filter { !it.isEmpty() }
.filter { it.isNotEmpty() }
}
fun saveExams(exams: List<Exam>) {

View File

@ -5,7 +5,7 @@ import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.Inter
import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Single
@ -22,20 +22,20 @@ class ExamRepository @Inject constructor(
) {
fun getExams(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean = false): Single<List<Exam>> {
return local.getExams(semester, start.monday, end.friday).filter { !forceRefresh }
return local.getExams(semester, start.monday, end.sunday).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) remote.getExams(student, semester, start.monday, end.friday)
if (it) remote.getExams(student, semester, start.monday, end.sunday)
else Single.error(UnknownHostException())
}.flatMap { new ->
local.getExams(semester, start.monday, end.friday)
local.getExams(semester, start.monday, end.sunday)
.toSingle(emptyList())
.doOnSuccess { old ->
local.deleteExams(old.uniqueSubtract(new))
local.saveExams(new.uniqueSubtract(old))
}
}.flatMap {
local.getExams(semester, start.monday, end.friday)
local.getExams(semester, start.monday, end.sunday)
.toSingle(emptyList())
}).map { list -> list.filter { it.date in start..end } }
}

View File

@ -27,6 +27,10 @@ class GradeLocal @Inject constructor(
gradeDb.updateAll(grades)
}
fun updateGradesSummary(gradesSummary: List<GradeSummary>) {
gradeSummaryDb.updateAll(gradesSummary)
}
fun getGradesDetails(semester: Semester): Maybe<List<Grade>> {
return gradeDb.loadAll(semester.semesterId, semester.studentId).filter { it.isNotEmpty() }
}

View File

@ -9,6 +9,7 @@ import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Completable
import io.reactivex.Single
import org.threeten.bp.LocalDateTime
import java.net.UnknownHostException
import javax.inject.Inject
import javax.inject.Singleton
@ -43,7 +44,31 @@ class GradeRepository @Inject constructor(
local.getGradesSummary(semester).toSingle(emptyList())
.doOnSuccess { old ->
local.deleteGradesSummary(old.uniqueSubtract(newSummary))
local.saveGradesSummary(newSummary.uniqueSubtract(old))
local.saveGradesSummary(newSummary.uniqueSubtract(old)
.onEach { summary ->
val oldSummary = old.find { oldSummary -> oldSummary.subject == summary.subject }
summary.isPredictedGradeNotified = when {
summary.predictedGrade.isEmpty() -> true
notify && oldSummary?.predictedGrade != summary.predictedGrade -> false
else -> true
}
summary.isFinalGradeNotified = when {
summary.finalGrade.isEmpty() -> true
notify && oldSummary?.finalGrade != summary.finalGrade -> false
else -> true
}
summary.predictedGradeLastChange = when {
oldSummary == null -> LocalDateTime.now()
summary.predictedGrade != oldSummary.predictedGrade -> LocalDateTime.now()
else -> oldSummary.predictedGradeLastChange
}
summary.finalGradeLastChange = when {
oldSummary == null -> LocalDateTime.now()
summary.finalGrade != oldSummary.finalGrade -> LocalDateTime.now()
else -> oldSummary.finalGradeLastChange
}
})
}
}
}.flatMap {
@ -63,6 +88,14 @@ class GradeRepository @Inject constructor(
return local.getGradesDetails(semester).map { it.filter { grade -> !grade.isNotified } }.toSingle(emptyList())
}
fun getNotNotifiedPredictedGrades(semester: Semester): Single<List<GradeSummary>> {
return local.getGradesSummary(semester).map { it.filter { gradeSummary -> !gradeSummary.isPredictedGradeNotified } }.toSingle(emptyList())
}
fun getNotNotifiedFinalGrades(semester: Semester): Single<List<GradeSummary>> {
return local.getGradesSummary(semester).map { it.filter { gradeSummary -> !gradeSummary.isFinalGradeNotified } }.toSingle(emptyList())
}
fun updateGrade(grade: Grade): Completable {
return Completable.fromCallable { local.updateGrades(listOf(grade)) }
}
@ -70,4 +103,8 @@ class GradeRepository @Inject constructor(
fun updateGrades(grades: List<Grade>): Completable {
return Completable.fromCallable { local.updateGrades(grades) }
}
fun updateGradesSummary(gradesSummary: List<GradeSummary>): Completable {
return Completable.fromCallable { local.updateGradesSummary(gradesSummary) }
}
}

View File

@ -5,7 +5,7 @@ import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.Inter
import io.github.wulkanowy.data.db.entities.Homework
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Completable
@ -23,7 +23,7 @@ class HomeworkRepository @Inject constructor(
) {
fun getHomework(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean = false): Single<List<Homework>> {
return Single.fromCallable { start.monday to end.friday }.flatMap { (monday, friday) ->
return Single.fromCallable { start.monday to end.sunday }.flatMap { (monday, friday) ->
local.getHomework(semester, monday, friday).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {

View File

@ -75,6 +75,6 @@ class MessageRemote @Inject constructor(private val sdk: Sdk) {
}
fun deleteMessage(student: Student, message: Message): Single<Boolean> {
return sdk.init(student).deleteMessages(listOf(Pair(message.realId, message.folderId)))
return sdk.init(student).deleteMessages(listOf(message.messageId to message.folderId))
}
}

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.data.repositories.preferences
import android.content.Context
import android.content.SharedPreferences
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.modules.grade.GradeAverageMode
import javax.inject.Inject
import javax.inject.Singleton
@ -17,8 +18,8 @@ class PreferencesRepository @Inject constructor(
val isShowPresent: Boolean
get() = getBoolean(R.string.pref_key_attendance_present, R.bool.pref_default_attendance_present)
val gradeAverageMode: String
get() = getString(R.string.pref_key_grade_average_mode, R.string.pref_default_grade_average_mode)
val gradeAverageMode: GradeAverageMode
get() = GradeAverageMode.getByValue(getString(R.string.pref_key_grade_average_mode, R.string.pref_default_grade_average_mode))
val gradeAverageForceCalc: Boolean
get() = getBoolean(R.string.pref_key_grade_average_force_calc, R.bool.pref_default_grade_average_force_calc)

View File

@ -12,7 +12,7 @@ import javax.inject.Singleton
class RecipientLocal @Inject constructor(private val recipientDb: RecipientDao) {
fun getRecipients(student: Student, role: Int, unit: ReportingUnit): Maybe<List<Recipient>> {
return recipientDb.load(student.studentId, role, unit.realId).filter { !it.isEmpty() }
return recipientDb.load(student.studentId, role, unit.realId).filter { it.isNotEmpty() }
}
fun saveRecipients(recipients: List<Recipient>): List<Long> {

View File

@ -11,7 +11,7 @@ import javax.inject.Singleton
class ReportingUnitLocal @Inject constructor(private val reportingUnitDb: ReportingUnitDao) {
fun getReportingUnits(student: Student): Maybe<List<ReportingUnit>> {
return reportingUnitDb.load(student.studentId).filter { !it.isEmpty() }
return reportingUnitDb.load(student.studentId).filter { it.isNotEmpty() }
}
fun getReportingUnit(student: Student, unitId: Int): Maybe<ReportingUnit> {

View File

@ -14,7 +14,7 @@ class StudentRemote @Inject constructor(private val sdk: Sdk) {
private fun mapStudents(students: List<SdkStudent>, email: String, password: String): List<Student> {
return students.map { student ->
Student(
email = email,
email = email.ifBlank { student.email },
password = password,
isParent = student.isParent,
symbol = student.symbol,
@ -39,7 +39,7 @@ class StudentRemote @Inject constructor(private val sdk: Sdk) {
}
fun getStudentsMobileApi(token: String, pin: String, symbol: String): Single<List<Student>> {
return sdk.getStudentsFromMobileApi(token, pin, symbol).map { mapStudents(it, "", "") }
return sdk.getStudentsFromMobileApi(token, pin, symbol, "").map { mapStudents(it, "", "") }
}
fun getStudentsScrapper(email: String, password: String, scrapperBaseUrl: String, symbol: String): Single<List<Student>> {
@ -47,6 +47,6 @@ class StudentRemote @Inject constructor(private val sdk: Sdk) {
}
fun getStudentsHybrid(email: String, password: String, scrapperBaseUrl: String, symbol: String): Single<List<Student>> {
return sdk.getStudentsHybrid(email, password, scrapperBaseUrl, symbol).map { mapStudents(it, email, password) }
return sdk.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol).map { mapStudents(it, email, password) }
}
}

View File

@ -6,7 +6,7 @@ import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Single
@ -24,13 +24,13 @@ class TimetableRepository @Inject constructor(
) {
fun getTimetable(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean = false): Single<List<Timetable>> {
return Single.fromCallable { start.monday to end.friday }.flatMap { (monday, friday) ->
local.getTimetable(semester, monday, friday).filter { !forceRefresh }
return Single.fromCallable { start.monday to end.sunday }.flatMap { (monday, sunday) ->
local.getTimetable(semester, monday, sunday).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings).flatMap {
if (it) remote.getTimetable(student, semester, monday, friday)
if (it) remote.getTimetable(student, semester, monday, sunday)
else Single.error(UnknownHostException())
}.flatMap { new ->
local.getTimetable(semester, monday, friday)
local.getTimetable(semester, monday, sunday)
.toSingle(emptyList())
.doOnSuccess { old ->
local.deleteTimetable(old.uniqueSubtract(new).also { schedulerHelper.cancelScheduled(it) })
@ -46,7 +46,7 @@ class TimetableRepository @Inject constructor(
})
}
}.flatMap {
local.getTimetable(semester, monday, friday).toSingle(emptyList())
local.getTimetable(semester, monday, sunday).toSingle(emptyList())
}).map { list -> list.filter { it.date in start..end }.also { schedulerHelper.scheduleNotifications(it, student) } }
}
}

View File

@ -3,7 +3,7 @@ package io.github.wulkanowy.services.alarm
import android.app.AlarmManager
import android.app.AlarmManager.RTC_WAKEUP
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_CANCEL_CURRENT
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.Context
import android.content.Intent
import androidx.core.app.AlarmManagerCompat
@ -55,7 +55,7 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
private fun cancelScheduledTo(range: ClosedRange<LocalDateTime>, requestCode: Int) {
if (now() in range) cancelNotification()
alarmManager.cancel(PendingIntent.getBroadcast(context, requestCode, Intent(), FLAG_CANCEL_CURRENT))
alarmManager.cancel(PendingIntent.getBroadcast(context, requestCode, Intent(), FLAG_UPDATE_CURRENT))
}
fun cancelNotification() = NotificationManagerCompat.from(context).cancel(MainView.Section.TIMETABLE.id)
@ -102,7 +102,7 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also {
it.putExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id)
it.putExtra(LESSON_TYPE, notificationType)
}, FLAG_CANCEL_CURRENT)
}, FLAG_UPDATE_CURRENT)
)
Timber.d("TimetableNotification scheduled: type: $notificationType, subject: ${intent.getStringExtra(LESSON_TITLE)}, start: $time, student: $studentId")
}

View File

@ -22,7 +22,7 @@ class DebugChannel @Inject constructor(
}
override fun create() {
if (appInfo.isDebug) return
if (!appInfo.isDebug) return
notificationManager.createNotificationChannel(
NotificationChannel(CHANNEL_ID, context.getString(R.string.channel_debug), IMPORTANCE_DEFAULT)
.apply {

View File

@ -3,7 +3,7 @@ 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.repositories.attendance.AttendanceRepository
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.monday
import io.reactivex.Completable
import org.threeten.bp.LocalDate.now
@ -12,7 +12,7 @@ import javax.inject.Inject
class AttendanceWork @Inject constructor(private val attendanceRepository: AttendanceRepository) : Work {
override fun create(student: Student, semester: Semester): Completable {
return attendanceRepository.getAttendance(student, semester, now().monday, now().friday, true)
return attendanceRepository.getAttendance(student, semester, now().monday, now().sunday, true)
.ignoreElement()
}
}

View File

@ -3,7 +3,7 @@ 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.repositories.completedlessons.CompletedLessonsRepository
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.monday
import io.reactivex.Completable
import org.threeten.bp.LocalDate.now
@ -14,7 +14,7 @@ class CompletedLessonWork @Inject constructor(
) : Work {
override fun create(student: Student, semester: Semester): Completable {
return completedLessonsRepository.getCompletedLessons(student, semester, now().monday, now().friday, true)
return completedLessonsRepository.getCompletedLessons(student, semester, now().monday, now().sunday, true)
.ignoreElement()
}
}

View File

@ -3,7 +3,7 @@ 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.repositories.exam.ExamRepository
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.monday
import io.reactivex.Completable
import org.threeten.bp.LocalDate.now
@ -12,6 +12,6 @@ import javax.inject.Inject
class ExamWork @Inject constructor(private val examRepository: ExamRepository) : Work {
override fun create(student: Student, semester: Semester): Completable {
return examRepository.getExams(student, semester, now().monday, now().friday, true).ignoreElement()
return examRepository.getExams(student, semester, now().monday, now().sunday, true).ignoreElement()
}
}

View File

@ -9,7 +9,7 @@ import javax.inject.Inject
class GradeStatisticsWork @Inject constructor(private val gradeStatisticsRepository: GradeStatisticsRepository) : Work {
override fun create(student: Student, semester: Semester): Completable {
return gradeStatisticsRepository.getGradesStatistics(student, semester, "Wszystkie", false, true)
return gradeStatisticsRepository.getGradesStatistics(student, semester, "Wszystkie", false, forceRefresh = true)
.ignoreElement()
}
}

View File

@ -9,6 +9,7 @@ import androidx.core.app.NotificationCompat.PRIORITY_HIGH
import androidx.core.app.NotificationManagerCompat
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.grade.GradeRepository
@ -30,17 +31,21 @@ class GradeWork @Inject constructor(
override fun create(student: Student, semester: Semester): Completable {
return gradeRepository.getGrades(student, semester, true, preferencesRepository.isNotificationsEnable)
.flatMap { gradeRepository.getNotNotifiedGrades(semester) }
.flatMapCompletable {
if (it.isNotEmpty()) notify(it)
.ignoreElement()
.concatWith(Completable.concatArray(gradeRepository.getNotNotifiedGrades(semester).flatMapCompletable {
if (it.isNotEmpty()) notifyDetails(it)
gradeRepository.updateGrades(it.onEach { grade -> grade.isNotified = true })
}
}, gradeRepository.getNotNotifiedPredictedGrades(semester).flatMapCompletable {
if (it.isNotEmpty()) notifyPredicted(it)
gradeRepository.updateGradesSummary(it.onEach { grade -> grade.isPredictedGradeNotified = true })
}, gradeRepository.getNotNotifiedFinalGrades(semester).flatMapCompletable {
if (it.isNotEmpty()) notifyFinal(it)
gradeRepository.updateGradesSummary(it.onEach { grade -> grade.isFinalGradeNotified = true })
}))
}
private fun notify(grades: List<Grade>) {
notificationManager.notify(Random.nextInt(Int.MAX_VALUE), NotificationCompat.Builder(context, NewGradesChannel.CHANNEL_ID)
.setContentTitle(context.resources.getQuantityString(R.plurals.grade_new_items, grades.size, grades.size))
.setContentText(context.resources.getQuantityString(R.plurals.grade_notify_new_items, grades.size, grades.size))
private fun getNotificationBuilder(): NotificationCompat.Builder {
return NotificationCompat.Builder(context, NewGradesChannel.CHANNEL_ID)
.setSmallIcon(R.drawable.ic_stat_grade)
.setAutoCancel(true)
.setPriority(PRIORITY_HIGH)
@ -49,6 +54,12 @@ class GradeWork @Inject constructor(
.setContentIntent(
PendingIntent.getActivity(context, MainView.Section.GRADE.id,
MainActivity.getStartIntent(context, MainView.Section.GRADE, true), FLAG_UPDATE_CURRENT))
}
private fun notifyDetails(grades: List<Grade>) {
notificationManager.notify(Random.nextInt(Int.MAX_VALUE), getNotificationBuilder()
.setContentTitle(context.resources.getQuantityString(R.plurals.grade_new_items, grades.size, grades.size))
.setContentText(context.resources.getQuantityString(R.plurals.grade_notify_new_items, grades.size, grades.size))
.setStyle(NotificationCompat.InboxStyle().run {
setSummaryText(context.resources.getQuantityString(R.plurals.grade_number_item, grades.size, grades.size))
grades.forEach { addLine("${it.subject}: ${it.entry}") }
@ -57,4 +68,30 @@ class GradeWork @Inject constructor(
.build()
)
}
private fun notifyPredicted(gradesSummary: List<GradeSummary>) {
notificationManager.notify(Random.nextInt(Int.MAX_VALUE), getNotificationBuilder()
.setContentTitle(context.resources.getQuantityString(R.plurals.grade_new_items_predicted, gradesSummary.size, gradesSummary.size))
.setContentText(context.resources.getQuantityString(R.plurals.grade_notify_new_items_predicted, gradesSummary.size, gradesSummary.size))
.setStyle(NotificationCompat.InboxStyle().run {
setSummaryText(context.resources.getQuantityString(R.plurals.grade_number_item, gradesSummary.size, gradesSummary.size))
gradesSummary.forEach { addLine("${it.subject}: ${it.predictedGrade}") }
this
})
.build()
)
}
private fun notifyFinal(gradesSummary: List<GradeSummary>) {
notificationManager.notify(Random.nextInt(Int.MAX_VALUE), getNotificationBuilder()
.setContentTitle(context.resources.getQuantityString(R.plurals.grade_new_items_final, gradesSummary.size, gradesSummary.size))
.setContentText(context.resources.getQuantityString(R.plurals.grade_notify_new_items_final, gradesSummary.size, gradesSummary.size))
.setStyle(NotificationCompat.InboxStyle().run {
setSummaryText(context.resources.getQuantityString(R.plurals.grade_number_item, gradesSummary.size, gradesSummary.size))
gradesSummary.forEach { addLine("${it.subject}: ${it.finalGrade}") }
this
})
.build()
)
}
}

View File

@ -3,7 +3,7 @@ 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.repositories.homework.HomeworkRepository
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.monday
import io.reactivex.Completable
import org.threeten.bp.LocalDate.now
@ -12,6 +12,6 @@ import javax.inject.Inject
class HomeworkWork @Inject constructor(private val homeworkRepository: HomeworkRepository) : Work {
override fun create(student: Student, semester: Semester): Completable {
return homeworkRepository.getHomework(student, semester, now().monday, now().friday, true).ignoreElement()
return homeworkRepository.getHomework(student, semester, now().monday, now().sunday, true).ignoreElement()
}
}

View File

@ -3,7 +3,7 @@ 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.repositories.timetable.TimetableRepository
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.monday
import io.reactivex.Completable
import org.threeten.bp.LocalDate.now
@ -12,7 +12,7 @@ import javax.inject.Inject
class TimetableWork @Inject constructor(private val timetableRepository: TimetableRepository) : Work {
override fun create(student: Student, semester: Semester): Completable {
return timetableRepository.getTimetable(student, semester, now().monday, now().friday, true)
return timetableRepository.getTimetable(student, semester, now().monday, now().sunday, true)
.ignoreElement()
}
}

View File

@ -9,6 +9,7 @@ import android.view.ViewGroup
import android.widget.HorizontalScrollView
import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
import androidx.appcompat.app.AlertDialog
import androidx.core.content.getSystemService
import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.DialogErrorBinding
@ -17,6 +18,7 @@ import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException
import io.github.wulkanowy.sdk.exception.ServiceUnavailableException
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.getString
import io.github.wulkanowy.utils.openAppInMarket
import io.github.wulkanowy.utils.openEmailClient
import io.github.wulkanowy.utils.openInternetBrowser
import java.io.InterruptedIOException
@ -74,7 +76,9 @@ class ErrorDialog : BaseDialogFragment<DialogErrorBinding>() {
Toast.makeText(context, R.string.all_copied, LENGTH_LONG).show()
}
errorDialogCancel.setOnClickListener { dismiss() }
errorDialogReport.setOnClickListener { openEmailClient(stringWriter.toString()) }
errorDialogReport.setOnClickListener {
openConfirmDialog { openEmailClient(stringWriter.toString()) }
}
errorDialogMessage.text = resources.getString(error)
errorDialogReport.isEnabled = when (error) {
is UnknownHostException,
@ -88,6 +92,17 @@ class ErrorDialog : BaseDialogFragment<DialogErrorBinding>() {
}
}
private fun openConfirmDialog(callback: () -> Unit) {
AlertDialog.Builder(requireContext())
.setTitle(R.string.dialog_error_check_update)
.setMessage(R.string.dialog_error_check_update_message)
.setNeutralButton(R.string.about_feedback) { _, _ -> callback() }
.setPositiveButton(R.string.dialog_error_check_update) { _, _ ->
requireContext().openAppInMarket(::showMessage)
}
.show()
}
private fun openEmailClient(content: String) {
requireContext().openEmailClient(
chooserTitle = getString(R.string.about_feedback),

View File

@ -14,6 +14,7 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.getCompatDrawable
import io.github.wulkanowy.utils.openAppInMarket
import io.github.wulkanowy.utils.openEmailClient
import io.github.wulkanowy.utils.openInternetBrowser
import javax.inject.Inject
@ -98,8 +99,12 @@ class AboutFragment : BaseFragment<FragmentAboutBinding>(R.layout.fragment_about
}
}
override fun openAppInMarket() {
context?.openAppInMarket(::showMessage)
}
override fun openLogViewer() {
if (appInfo.isDebug) (activity as? MainActivity)?.pushView(LogViewerFragment.newInstance())
(activity as? MainActivity)?.pushView(LogViewerFragment.newInstance())
}
override fun openDiscordInvite() {
@ -115,7 +120,7 @@ class AboutFragment : BaseFragment<FragmentAboutBinding>(R.layout.fragment_about
chooserTitle = getString(R.string.about_feedback),
email = "wulkanowyinc@gmail.com",
subject = "Zgłoszenie błędu",
body = requireContext().getString(R.string.about_feedback_template,
body = getString(R.string.about_feedback_template,
"${appInfo.systemManufacturer} ${appInfo.systemModel}", appInfo.systemVersion.toString(), appInfo.versionName
),
onActivityNotFound = {

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.modules.about
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import timber.log.Timber
@ -12,6 +13,7 @@ class AboutPresenter @Inject constructor(
schedulers: SchedulersProvider,
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val appInfo: AppInfo,
private val analytics: FirebaseAnalyticsHelper
) : BasePresenter<AboutView>(errorHandler, studentRepository, schedulers) {
@ -27,7 +29,8 @@ class AboutPresenter @Inject constructor(
when (name) {
versionRes?.first -> {
Timber.i("Opening log viewer")
openLogViewer()
if (appInfo.isDebug) openLogViewer()
else openAppInMarket()
analytics.logEvent("about_open", "name" to "log_viewer")
}
feedbackRes?.first -> {

View File

@ -25,6 +25,8 @@ interface AboutView : BaseView {
fun updateData(data: List<Triple<String, String, Drawable?>>)
fun openAppInMarket()
fun openLogViewer()
fun openDiscordInvite()

View File

@ -25,9 +25,9 @@ class LogViewerPresenter @Inject constructor(
disposable.add(loggerRepository.getLogFiles()
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.subscribe({
Timber.i("Loading logs files result: ${it.joinToString { it.name }}")
view?.shareLogs(it)
.subscribe({ files ->
Timber.i("Loading logs files result: ${files.joinToString { it.name }}")
view?.shareLogs(files)
}, {
Timber.i("Loading logs files result: An exception occurred")
errorHandler.dispatch(it)

View File

@ -0,0 +1,3 @@
package io.github.wulkanowy.ui.modules.account
data class Account(val email: String, val isParent: Boolean)

View File

@ -3,33 +3,72 @@ package io.github.wulkanowy.ui.modules.account
import android.annotation.SuppressLint
import android.graphics.PorterDuff
import android.view.LayoutInflater
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.databinding.HeaderAccountBinding
import io.github.wulkanowy.databinding.ItemAccountBinding
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject
class AccountAdapter @Inject constructor() : RecyclerView.Adapter<AccountAdapter.ItemViewHolder>() {
class AccountAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var items = emptyList<Student>()
var items = emptyList<AccountItem<*>>()
var onClickListener: (Student) -> Unit = {}
override fun getItemCount() = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder(
ItemAccountBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
override fun getItemViewType(position: Int) = items[position].viewType.id
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
AccountItem.ViewType.HEADER.id -> HeaderViewHolder(HeaderAccountBinding.inflate(inflater, parent, false))
AccountItem.ViewType.ITEM.id -> ItemViewHolder(ItemAccountBinding.inflate(inflater, parent, false))
else -> throw IllegalStateException()
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is HeaderViewHolder -> bindHeaderViewHolder(holder.binding, items[position].value as Account)
is ItemViewHolder -> bindItemViewHolder(holder.binding, items[position].value as Student)
}
}
private fun bindHeaderViewHolder(binding: HeaderAccountBinding, account: Account) {
with(binding) {
accountHeaderEmail.text = account.email
accountHeaderType.setText(if (account.isParent) R.string.account_type_parent else R.string.account_type_student)
}
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val student = items[position]
with(holder.binding) {
private fun bindItemViewHolder(binding: ItemAccountBinding, student: Student) {
with(binding) {
accountItemName.text = "${student.studentName} ${student.className}"
accountItemSchool.text = student.schoolName
with(accountItemLoginMode) {
visibility = when (Sdk.Mode.valueOf(student.loginMode)) {
Sdk.Mode.API -> {
setText(R.string.account_login_mobile_api)
VISIBLE
}
Sdk.Mode.HYBRID -> {
setText(R.string.account_login_hybrid)
VISIBLE
}
Sdk.Mode.SCRAPPER -> {
GONE
}
}
}
with(accountItemImage) {
val colorImage = if (student.isCurrent) context.getThemeAttrColor(R.attr.colorPrimary)
@ -42,5 +81,8 @@ class AccountAdapter @Inject constructor() : RecyclerView.Adapter<AccountAdapter
}
}
class HeaderViewHolder(val binding: HeaderAccountBinding) :
RecyclerView.ViewHolder(binding.root)
class ItemViewHolder(val binding: ItemAccountBinding) : RecyclerView.ViewHolder(binding.root)
}

View File

@ -9,7 +9,6 @@ import android.widget.Toast.LENGTH_LONG
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.LinearLayoutManager
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.databinding.DialogAccountBinding
import io.github.wulkanowy.ui.base.BaseDialogFragment
import io.github.wulkanowy.ui.modules.login.LoginActivity
@ -54,7 +53,7 @@ class AccountDialog : BaseDialogFragment<DialogAccountBinding>(), AccountView {
}
}
override fun updateData(data: List<Student>) {
override fun updateData(data: List<AccountItem<*>>) {
with(accountAdapter) {
items = data
notifyDataSetChanged()

View File

@ -0,0 +1,9 @@
package io.github.wulkanowy.ui.modules.account
data class AccountItem<out T>(val value: T, val viewType: ViewType) {
enum class ViewType(val id: Int) {
HEADER(1),
ITEM(2)
}
}

View File

@ -83,11 +83,20 @@ class AccountPresenter @Inject constructor(
}
}
private fun createAccountItems(items: List<Student>): List<AccountItem<*>> {
return items.groupBy { Account(it.email, it.isParent) }.map { (account, students) ->
listOf(AccountItem(account, AccountItem.ViewType.HEADER)) + students.map { student ->
AccountItem(student, AccountItem.ViewType.ITEM)
}
}.flatten()
}
private fun loadData() {
Timber.i("Loading account data started")
disposable.add(studentRepository.getSavedStudents(false)
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.map { createAccountItems(it) }
.subscribe({
Timber.i("Loading account result: Success")
view?.updateData(it)

View File

@ -1,13 +1,12 @@
package io.github.wulkanowy.ui.modules.account
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.ui.base.BaseView
interface AccountView : BaseView {
fun initView()
fun updateData(data: List<Student>)
fun updateData(data: List<AccountItem<*>>)
fun dismissView()

View File

@ -20,7 +20,6 @@ import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDate.now
import org.threeten.bp.LocalDate.ofEpochDay
import timber.log.Timber
import java.util.concurrent.TimeUnit.MILLISECONDS
import javax.inject.Inject
class AttendancePresenter @Inject constructor(
@ -215,7 +214,12 @@ class AttendancePresenter @Inject constructor(
showContent(it.isNotEmpty())
showExcuseButton(it.any { item -> item.excusable })
}
analytics.logEvent("load_attendance", "items" to it.size, "force_refresh" to forceRefresh)
analytics.logEvent(
"load_data",
"type" to "attendance",
"items" to it.size,
"force_refresh" to forceRefresh
)
}) {
Timber.i("Loading attendance result: An exception occurred")
errorHandler.dispatch(it)

View File

@ -99,7 +99,13 @@ class AttendanceSummaryPresenter @Inject constructor(
showContent(it.isNotEmpty())
updateDataSet(it)
}
analytics.logEvent("load_attendance_summary", "items" to it.size, "force_refresh" to forceRefresh, "item_id" to subjectId)
analytics.logEvent(
"load_data",
"type" to "attendance_summary",
"items" to it.size,
"force_refresh" to forceRefresh,
"item_id" to subjectId
)
}) {
Timber.i("Loading attendance summary result: An exception occurred")
errorHandler.dispatch(it)

View File

@ -8,7 +8,7 @@ import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday
import io.github.wulkanowy.utils.isHolidays
import io.github.wulkanowy.utils.monday
@ -110,7 +110,7 @@ class ExamPresenter @Inject constructor(
add(studentRepository.getCurrentStudent()
.flatMap { student ->
semesterRepository.getCurrentSemester(student).flatMap { semester ->
examRepository.getExams(student, semester, currentDate.monday, currentDate.friday, forceRefresh)
examRepository.getExams(student, semester, currentDate.monday, currentDate.sunday, forceRefresh)
}
}
.map { createExamItems(it) }
@ -131,7 +131,12 @@ class ExamPresenter @Inject constructor(
showErrorView(false)
showContent(it.isNotEmpty())
}
analytics.logEvent("load_exam", "items" to it.size, "force_refresh" to forceRefresh)
analytics.logEvent(
"load_data",
"type" to "exam",
"items" to it.size,
"force_refresh" to forceRefresh
)
}) {
Timber.i("Loading exam result: An exception occurred")
errorHandler.dispatch(it)
@ -176,7 +181,7 @@ class ExamPresenter @Inject constructor(
showPreButton(!currentDate.minusDays(7).isHolidays)
showNextButton(!currentDate.plusDays(7).isHolidays)
updateNavigationWeek("${currentDate.monday.toFormattedString("dd.MM")} - " +
currentDate.friday.toFormattedString("dd.MM"))
currentDate.sunday.toFormattedString("dd.MM"))
}
}
}

View File

@ -0,0 +1,11 @@
package io.github.wulkanowy.ui.modules.grade
enum class GradeAverageMode(val value: String) {
ALL_YEAR("all_year"),
ONE_SEMESTER("only_one_semester"),
BOTH_SEMESTERS("both_semesters");
companion object {
fun getByValue(value: String) = values().firstOrNull { it.value == value } ?: ONE_SEMESTER
}
}

View File

@ -1,11 +1,16 @@
package io.github.wulkanowy.ui.modules.grade
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.grade.GradeRepository
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
import io.github.wulkanowy.data.repositories.semester.SemesterRepository
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.ALL_YEAR
import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.BOTH_SEMESTERS
import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.ONE_SEMESTER
import io.github.wulkanowy.utils.calcAverage
import io.github.wulkanowy.utils.changeModifier
import io.reactivex.Single
@ -17,21 +22,21 @@ class GradeAverageProvider @Inject constructor(
private val preferencesRepository: PreferencesRepository
) {
private val plusModifier = preferencesRepository.gradePlusModifier
private val plusModifier get() = preferencesRepository.gradePlusModifier
private val minusModifier = preferencesRepository.gradeMinusModifier
private val minusModifier get() = preferencesRepository.gradeMinusModifier
fun getGradesDetailsWithAverage(student: Student, semesterId: Int, forceRefresh: Boolean = false): Single<List<GradeDetailsWithAverage>> {
return semesterRepository.getSemesters(student).flatMap { semesters ->
when (preferencesRepository.gradeAverageMode) {
"only_one_semester" -> getSemesterDetailsWithAverage(student, semesters.single { it.semesterId == semesterId }, forceRefresh)
"all_year" -> calculateWholeYearAverage(student, semesters, semesterId, forceRefresh)
else -> throw IllegalArgumentException("Incorrect grade average mode: ${preferencesRepository.gradeAverageMode} ")
ONE_SEMESTER -> getSemesterDetailsWithAverage(student, semesters.single { it.semesterId == semesterId }, forceRefresh)
BOTH_SEMESTERS -> calculateBothSemestersAverage(student, semesters, semesterId, forceRefresh)
ALL_YEAR -> calculateAllYearAverage(student, semesters, semesterId, forceRefresh)
}
}
}
private fun calculateWholeYearAverage(student: Student, semesters: List<Semester>, semesterId: Int, forceRefresh: Boolean): Single<List<GradeDetailsWithAverage>> {
private fun calculateBothSemestersAverage(student: Student, semesters: List<Semester>, semesterId: Int, forceRefresh: Boolean): Single<List<GradeDetailsWithAverage>> {
val selectedSemester = semesters.single { it.semesterId == semesterId }
val firstSemester = semesters.single { it.diaryId == selectedSemester.diaryId && it.semesterName == 1 }
@ -42,11 +47,30 @@ class GradeAverageProvider @Inject constructor(
getSemesterDetailsWithAverage(student, firstSemester, forceRefresh).map { secondDetails ->
selectedDetails.map { selected ->
val second = secondDetails.singleOrNull { it.subject == selected.subject }
selected.copy(
average = if (!isAnyAverage || preferencesRepository.gradeAverageForceCalc) {
(selected.grades + second?.grades.orEmpty()).calcAverage()
} else (selected.average + (second?.average ?: selected.average)) / 2
)
selected.copy(average = if (!isAnyAverage || preferencesRepository.gradeAverageForceCalc) {
val selectedGrades = selected.grades.updateModifiers(student).calcAverage()
(selectedGrades + (second?.grades?.updateModifiers(student)?.calcAverage() ?: selectedGrades)) / 2
} else (selected.average + (second?.average ?: selected.average)) / 2)
}
}
} else Single.just(selectedDetails)
}
}
private fun calculateAllYearAverage(student: Student, semesters: List<Semester>, semesterId: Int, forceRefresh: Boolean): Single<List<GradeDetailsWithAverage>> {
val selectedSemester = semesters.single { it.semesterId == semesterId }
val firstSemester = semesters.single { it.diaryId == selectedSemester.diaryId && it.semesterName == 1 }
return getSemesterDetailsWithAverage(student, selectedSemester, forceRefresh).flatMap { selectedDetails ->
val isAnyAverage = selectedDetails.any { it.average != .0 }
if (selectedSemester != firstSemester) {
getSemesterDetailsWithAverage(student, firstSemester, forceRefresh).map { secondDetails ->
selectedDetails.map { selected ->
val second = secondDetails.singleOrNull { it.subject == selected.subject }
selected.copy(average = if (!isAnyAverage || preferencesRepository.gradeAverageForceCalc) {
(selected.grades.updateModifiers(student) + second?.grades?.updateModifiers(student).orEmpty()).calcAverage()
} else selected.average)
}
}
} else Single.just(selectedDetails)
@ -58,15 +82,12 @@ class GradeAverageProvider @Inject constructor(
val isAnyAverage = summaries.any { it.average != .0 }
val allGrades = details.groupBy { it.subject }
summaries.map { summary ->
summaries.emulateEmptySummaries(student, semester, allGrades.toList(), isAnyAverage).map { summary ->
val grades = allGrades[summary.subject].orEmpty()
GradeDetailsWithAverage(
subject = summary.subject,
average = if (!isAnyAverage || preferencesRepository.gradeAverageForceCalc) {
grades.map {
if (student.loginMode == Sdk.Mode.SCRAPPER.name) it.changeModifier(plusModifier, minusModifier)
else it
}.calcAverage()
grades.updateModifiers(student).calcAverage()
} else summary.average,
points = summary.pointsSum,
summary = summary,
@ -75,4 +96,30 @@ class GradeAverageProvider @Inject constructor(
}
}
}
private fun List<GradeSummary>.emulateEmptySummaries(student: Student, semester: Semester, grades: List<Pair<String, List<Grade>>>, calcAverage: Boolean): List<GradeSummary> {
if (isNotEmpty() && size > grades.size) return this
return grades.mapIndexed { i, (subject, details) ->
singleOrNull { it.subject == subject }?.let { return@mapIndexed it }
GradeSummary(
studentId = student.studentId,
semesterId = semester.semesterId,
position = i,
subject = subject,
predictedGrade = "",
finalGrade = "",
proposedPoints = "",
finalPoints = "",
pointsSum = "",
average = if (calcAverage) details.updateModifiers(student).calcAverage() else .0
)
}
}
private fun List<Grade>.updateModifiers(student: Student): List<Grade> {
return if (student.loginMode == Sdk.Mode.SCRAPPER.name) {
map { it.changeModifier(plusModifier, minusModifier) }
} else this
}
}

View File

@ -133,6 +133,7 @@ class GradePresenter @Inject constructor(
}
private fun loadChild(index: Int, forceRefresh: Boolean = false) {
Timber.d("Load grade tab child. Selected semester: $selectedIndex, semesters: ${semesters.joinToString { it.semesterName.toString() }}")
semesters.first { it.semesterName == selectedIndex }.semesterId.also {
if (forceRefresh || loadedSemesterId[index] != it) {
Timber.i("Load grade child view index: $index")

View File

@ -7,6 +7,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.NO_POSITION
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.databinding.HeaderGradeDetailsBinding
@ -14,6 +15,7 @@ import io.github.wulkanowy.databinding.ItemGradeDetailsBinding
import io.github.wulkanowy.ui.base.BaseExpandableAdapter
import io.github.wulkanowy.utils.getBackgroundColor
import io.github.wulkanowy.utils.toFormattedString
import timber.log.Timber
import javax.inject.Inject
class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<RecyclerView.ViewHolder>() {
@ -22,7 +24,7 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
private var items = mutableListOf<GradeDetailsItem>()
private var expandedPosition = RecyclerView.NO_POSITION
private var expandedPosition = NO_POSITION
private var isExpandable = false
@ -34,36 +36,53 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
headers = data.filter { it.viewType == ViewType.HEADER }.toMutableList()
items = if (isExpanded) headers else data.toMutableList()
isExpandable = isExpanded
expandedPosition = RecyclerView.NO_POSITION
expandedPosition = NO_POSITION
}
fun updateDetailsItem(position: Int, grade: Grade) {
if (items.getOrNull(position)?.viewType != ViewType.ITEM) {
Timber.e("Trying to update item $position on list ${items.size} size, expanded position: $expandedPosition")
return
}
items[position] = GradeDetailsItem(grade, ViewType.ITEM)
notifyItemChanged(position)
}
fun getHeaderItem(subject: String): GradeDetailsItem {
return headers.single { (it.value as GradeDetailsHeader).subject == subject }
val candidates = headers.filter { (it.value as GradeDetailsHeader).subject == subject }
if (candidates.size > 1) {
Timber.e("Header with subject $subject found ${candidates.size} times! Expanded: $expandedPosition. Items: $candidates")
}
return candidates.first()
}
fun updateHeaderItem(item: GradeDetailsItem) {
headers[headers.indexOf(item)] = item
items[items.indexOf(item)] = item
notifyItemChanged(items.indexOf(item))
val headerPosition = headers.indexOf(item)
val itemPosition = items.indexOf(item)
if (headerPosition == NO_POSITION || itemPosition == NO_POSITION) {
Timber.e("Invalid update header positions! Header: $headerPosition, item: $itemPosition")
}
headers[headerPosition] = item
items[itemPosition] = item
notifyItemChanged(itemPosition)
}
fun collapseAll() {
if (expandedPosition != -1) {
refreshList(headers)
expandedPosition = RecyclerView.NO_POSITION
expandedPosition = NO_POSITION
}
}
@Synchronized
private fun refreshList(newItems: List<GradeDetailsItem>) {
private fun refreshList(newItems: MutableList<GradeDetailsItem>) {
val diffCallback = GradeDetailsDiffUtil(items, newItems)
val diffResult = DiffUtil.calculateDiff(diffCallback)
items = newItems.toMutableList()
items = newItems
diffResult.dispatchUpdatesTo(this)
}
@ -84,23 +103,24 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is HeaderViewHolder -> bindHeaderViewHolder(
binding = holder.binding,
holder = holder,
header = items[position].value as GradeDetailsHeader,
headerPosition = headers.indexOf(items[position]),
adapterPosition = position
position = position
)
is ItemViewHolder -> bindItemViewHolder(
binding = holder.binding,
grade = items[position].value as Grade,
position = position
holder = holder,
grade = items[position].value as Grade
)
}
}
private fun bindHeaderViewHolder(binding: HeaderGradeDetailsBinding, header: GradeDetailsHeader, headerPosition: Int, adapterPosition: Int) {
with(binding) {
private fun bindHeaderViewHolder(holder: HeaderViewHolder, header: GradeDetailsHeader, position: Int) {
val headerPosition = headers.indexOf(items[position])
val adapterPosition = holder.adapterPosition
with(holder.binding) {
gradeHeaderDivider.visibility = if (adapterPosition == 0) View.GONE else View.VISIBLE
gradeHeaderSubject.apply {
with(gradeHeaderSubject) {
text = header.subject
maxLines = if (headerPosition == expandedPosition) 2 else 1
}
@ -115,7 +135,7 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
gradeHeaderContainer.setOnClickListener {
expandedPosition = if (expandedPosition == adapterPosition) -1 else adapterPosition
if (expandedPosition != RecyclerView.NO_POSITION) {
if (expandedPosition != NO_POSITION) {
refreshList(headers.toMutableList().apply {
addAll(headerPosition + 1, header.grades)
})
@ -133,8 +153,8 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
}
@SuppressLint("SetTextI18n")
private fun bindItemViewHolder(binding: ItemGradeDetailsBinding, grade: Grade, position: Int) {
with(binding) {
private fun bindItemViewHolder(holder: ItemViewHolder, grade: Grade) {
with(holder.binding) {
gradeItemValue.run {
text = grade.entry
setBackgroundResource(grade.getBackgroundColor(colorTheme))
@ -148,7 +168,9 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
gradeItemWeight.text = "${root.context.getString(R.string.grade_weight)}: ${grade.weight}"
gradeItemNote.visibility = if (!grade.isRead) View.VISIBLE else View.GONE
root.setOnClickListener { onClickListener(grade, position) }
root.setOnClickListener {
holder.adapterPosition.let { if (it != NO_POSITION) onClickListener(grade, it) }
}
}
}

View File

@ -43,7 +43,7 @@ class GradeDetailsPresenter @Inject constructor(
}
fun onGradeItemSelected(grade: Grade, position: Int) {
Timber.i("Select grade item ${grade.id}")
Timber.i("Select grade item ${grade.id}, position: $position")
view?.apply {
showGradeDialog(grade, preferencesRepository.gradeColorTheme)
if (!grade.isRead) {
@ -152,7 +152,12 @@ class GradeDetailsPresenter @Inject constructor(
gradeColorTheme = preferencesRepository.gradeColorTheme
)
}
analytics.logEvent("load_grade_details", "items" to grades.size, "force_refresh" to forceRefresh)
analytics.logEvent(
"load_data",
"type" to "grade_details",
"items" to grades.size,
"force_refresh" to forceRefresh
)
}) {
Timber.i("Loading grade details result: An exception occurred")
errorHandler.dispatch(it)
@ -171,19 +176,22 @@ class GradeDetailsPresenter @Inject constructor(
}
private fun createGradeItems(items: List<GradeDetailsWithAverage>): List<GradeDetailsItem> {
return items.filter { it.grades.isNotEmpty() }.map { (subject, average, points, _, grades) ->
val subItems = grades.map {
GradeDetailsItem(it, ViewType.ITEM)
}
return items
.filter { it.grades.isNotEmpty() }
.sortedBy { it.subject }
.map { (subject, average, points, _, grades) ->
val subItems = grades
.sortedByDescending { it.date }
.map { GradeDetailsItem(it, ViewType.ITEM) }
listOf(GradeDetailsItem(GradeDetailsHeader(
subject = subject,
average = average,
pointsSum = points,
newGrades = grades.filter { grade -> !grade.isRead }.size,
grades = subItems
), ViewType.HEADER)) + if (preferencesRepository.isGradeExpandable) emptyList() else subItems
}.flatten()
listOf(GradeDetailsItem(GradeDetailsHeader(
subject = subject,
average = average,
pointsSum = points,
newGrades = grades.filter { grade -> !grade.isRead }.size,
grades = subItems
), ViewType.HEADER)) + if (preferencesRepository.isGradeExpandable) emptyList() else subItems
}.flatten()
}
private fun updateGrade(grade: Grade) {

View File

@ -174,7 +174,12 @@ class GradeStatisticsPresenter @Inject constructor(
updateData(it, preferencesRepository.gradeColorTheme, preferencesRepository.showAllSubjectsOnStatisticsList)
showSubjects(!preferencesRepository.showAllSubjectsOnStatisticsList)
}
analytics.logEvent("load_grade_statistics", "items" to it.size, "force_refresh" to forceRefresh)
analytics.logEvent(
"load_data",
"type" to "grade_statistics",
"items" to it.size,
"force_refresh" to forceRefresh
)
}) {
Timber.i("Loading grade stats result: An exception occurred")
errorHandler.dispatch(it)

View File

@ -49,7 +49,12 @@ class GradeSummaryPresenter @Inject constructor(
showErrorView(false)
updateData(it)
}
analytics.logEvent("load_grade_summary", "items" to it.size, "force_refresh" to forceRefresh)
analytics.logEvent(
"load_data",
"type" to "grade_summary",
"items" to it.size,
"force_refresh" to forceRefresh
)
}) {
Timber.i("Loading grade summary result: An exception occurred")
errorHandler.dispatch(it)
@ -103,14 +108,18 @@ class GradeSummaryPresenter @Inject constructor(
}
private fun createGradeSummaryItems(items: List<GradeDetailsWithAverage>): List<GradeSummary> {
return items.map {
it.summary.copy(average = it.average)
}
return items
.filter { !checkEmpty(it) }
.sortedBy { it.subject }
.map { it.summary.copy(average = it.average) }
}
private fun checkEmpty(gradeSummary: GradeSummary, averages: List<Triple<String, Double, String>>): Boolean {
private fun checkEmpty(gradeSummary: GradeDetailsWithAverage): Boolean {
return gradeSummary.run {
finalGrade.isBlank() && predictedGrade.isBlank() && averages.singleOrNull { it.first == subject } == null
summary.finalGrade.isBlank()
&& summary.predictedGrade.isBlank()
&& average == .0
&& points.isBlank()
}
}
}

View File

@ -8,7 +8,7 @@ import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday
import io.github.wulkanowy.utils.isHolidays
import io.github.wulkanowy.utils.monday
@ -124,7 +124,12 @@ class HomeworkPresenter @Inject constructor(
showErrorView(false)
showContent(it.isNotEmpty())
}
analytics.logEvent("load_homework", "items" to it.size, "force_refresh" to forceRefresh)
analytics.logEvent(
"load_data",
"type" to "homework",
"items" to it.size,
"force_refresh" to forceRefresh
)
}) {
Timber.i("Loading homework result: An exception occurred")
@ -170,7 +175,7 @@ class HomeworkPresenter @Inject constructor(
showPreButton(!currentDate.minusDays(7).isHolidays)
showNextButton(!currentDate.plusDays(7).isHolidays)
updateNavigationWeek("${currentDate.monday.toFormattedString("dd.MM")} - " +
currentDate.friday.toFormattedString("dd.MM"))
currentDate.sunday.toFormattedString("dd.MM"))
}
}
}

View File

@ -22,6 +22,10 @@ import javax.inject.Inject
class LoginRecoverFragment :
BaseFragment<FragmentLoginRecoverBinding>(R.layout.fragment_login_recover), LoginRecoverView {
private var _binding: FragmentLoginRecoverBinding? = null
private val bindingLocal: FragmentLoginRecoverBinding get() = _binding!!
@Inject
lateinit var presenter: LoginRecoverPresenter
@ -36,13 +40,13 @@ class LoginRecoverFragment :
private lateinit var hostSymbols: Array<String>
override val recoverHostValue: String
get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginRecoverHost.text.toString())).orEmpty()
get() = hostValues.getOrNull(hostKeys.indexOf(bindingLocal.loginRecoverHost.text.toString())).orEmpty()
override val formHostSymbol: String
get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginRecoverHost.text.toString())).orEmpty()
get() = hostSymbols.getOrNull(hostKeys.indexOf(bindingLocal.loginRecoverHost.text.toString())).orEmpty()
override val recoverNameValue: String
get() = binding.loginRecoverName.text.toString().trim()
get() = bindingLocal.loginRecoverName.text.toString().trim()
override val emailHintString: String
get() = getString(R.string.login_email_hint)
@ -55,7 +59,7 @@ class LoginRecoverFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentLoginRecoverBinding.bind(view)
_binding = FragmentLoginRecoverBinding.bind(view)
presenter.onAttachView(this)
}
@ -64,7 +68,7 @@ class LoginRecoverFragment :
hostValues = resources.getStringArray(R.array.hosts_values)
hostSymbols = resources.getStringArray(R.array.hosts_symbols)
with(binding) {
with(bindingLocal) {
loginRecoverWebView.setBackgroundColor(Color.TRANSPARENT)
loginRecoverName.doOnTextChanged { _, _, _, _ -> presenter.onNameTextChanged() }
loginRecoverHost.setOnItemClickListener { _, _, _, _ -> presenter.onHostSelected() }
@ -74,69 +78,69 @@ class LoginRecoverFragment :
loginRecoverLogin.setOnClickListener { (activity as LoginActivity).switchView(0) }
}
with(binding.loginRecoverHost) {
with(bindingLocal.loginRecoverHost) {
setText(hostKeys.getOrNull(0).orEmpty())
setAdapter(LoginSymbolAdapter(context, R.layout.support_simple_spinner_dropdown_item, hostKeys))
setOnClickListener { if (binding.loginRecoverFormContainer.visibility == GONE) dismissDropDown() }
setOnClickListener { if (bindingLocal.loginRecoverFormContainer.visibility == GONE) dismissDropDown() }
}
}
override fun setDefaultCredentials(username: String) {
binding.loginRecoverName.setText(username)
bindingLocal.loginRecoverName.setText(username)
}
override fun setErrorNameRequired() {
with(binding.loginRecoverNameLayout) {
with(bindingLocal.loginRecoverNameLayout) {
requestFocus()
error = getString(R.string.login_field_required)
}
}
override fun setUsernameHint(hint: String) {
binding.loginRecoverNameLayout.hint = hint
bindingLocal.loginRecoverNameLayout.hint = hint
}
override fun setUsernameError(message: String) {
with(binding.loginRecoverNameLayout) {
with(bindingLocal.loginRecoverNameLayout) {
requestFocus()
error = message
}
}
override fun clearUsernameError() {
binding.loginRecoverNameLayout.error = null
bindingLocal.loginRecoverNameLayout.error = null
}
override fun showProgress(show: Boolean) {
binding.loginRecoverProgress.visibility = if (show) VISIBLE else GONE
bindingLocal.loginRecoverProgress.visibility = if (show) VISIBLE else GONE
}
override fun showRecoverForm(show: Boolean) {
binding.loginRecoverFormContainer.visibility = if (show) VISIBLE else GONE
bindingLocal.loginRecoverFormContainer.visibility = if (show) VISIBLE else GONE
}
override fun showCaptcha(show: Boolean) {
binding.loginRecoverCaptchaContainer.visibility = if (show) VISIBLE else GONE
bindingLocal.loginRecoverCaptchaContainer.visibility = if (show) VISIBLE else GONE
}
override fun showErrorView(show: Boolean) {
binding.loginRecoverError.visibility = if (show) VISIBLE else GONE
bindingLocal.loginRecoverError.visibility = if (show) VISIBLE else GONE
}
override fun setErrorDetails(message: String) {
binding.loginRecoverErrorMessage.text = message
bindingLocal.loginRecoverErrorMessage.text = message
}
override fun showSuccessView(show: Boolean) {
binding.loginRecoverSuccess.visibility = if (show) VISIBLE else GONE
bindingLocal.loginRecoverSuccess.visibility = if (show) VISIBLE else GONE
}
override fun setSuccessTitle(title: String) {
binding.loginRecoverSuccessTitle.text = title
bindingLocal.loginRecoverSuccessTitle.text = title
}
override fun setSuccessMessage(message: String) {
binding.loginRecoverSuccessMessage.text = message
bindingLocal.loginRecoverSuccessMessage.text = message
}
override fun showSoftKeyboard() {
@ -157,7 +161,7 @@ class LoginRecoverFragment :
callback:e =>Android.captchaCallback(e)})</script>
""".trimIndent()
with(binding.loginRecoverWebView) {
with(bindingLocal.loginRecoverWebView) {
settings.javaScriptEnabled = true
webViewClient = object : WebViewClient() {
private var recoverWebViewSuccess: Boolean = true
@ -197,6 +201,8 @@ class LoginRecoverFragment :
}
override fun onDestroyView() {
bindingLocal.loginRecoverWebView.destroy()
_binding = null
presenter.onDetachView()
super.onDestroyView()

View File

@ -12,7 +12,13 @@ import javax.inject.Inject
class LoginStudentSelectAdapter @Inject constructor() :
RecyclerView.Adapter<LoginStudentSelectAdapter.ItemViewHolder>() {
private val checkedList = mutableMapOf<Int, Boolean>()
var items = emptyList<Pair<Student, Boolean>>()
set(value) {
field = value
checkedList.clear()
}
var onClickListener: (Student, alreadySaved: Boolean) -> Unit = { _, _ -> }
@ -31,15 +37,21 @@ class LoginStudentSelectAdapter @Inject constructor() :
loginItemSchool.text = student.schoolName
loginItemName.isEnabled = !alreadySaved
loginItemSchool.isEnabled = !alreadySaved
loginItemCheck.isEnabled = !alreadySaved
loginItemSignedIn.visibility = if (alreadySaved) View.VISIBLE else View.GONE
with(loginItemCheck) {
isEnabled = !alreadySaved
keyListener = null
isChecked = checkedList[position] ?: false
}
root.setOnClickListener {
onClickListener(student, alreadySaved)
with(loginItemCheck) {
if (isEnabled) {
isChecked = !isChecked
checkedList[position] = isChecked
}
}
}

View File

@ -22,7 +22,7 @@ class LoginStudentSelectPresenter @Inject constructor(
var students = emptyList<Student>()
private var selectedStudents = mutableListOf<Student>()
private val selectedStudents = mutableListOf<Student>()
fun onAttachView(view: LoginStudentSelectView, students: Serializable?) {
super.onAttachView(view)
@ -69,6 +69,7 @@ class LoginStudentSelectPresenter @Inject constructor(
}
private fun loadData(students: List<Student>) {
resetSelectedState()
this.students = students
disposable.add(studentRepository.getSavedStudents()
.map { savedStudents ->
@ -88,6 +89,11 @@ class LoginStudentSelectPresenter @Inject constructor(
)
}
private fun resetSelectedState() {
selectedStudents.clear()
view?.enableSignIn(false)
}
private fun registerStudents(students: List<Student>) {
disposable.add(studentRepository.saveStudents(students)
.map { students.first().apply { id = it.first() } }

View File

@ -54,7 +54,12 @@ class LuckyNumberPresenter @Inject constructor(
showEmpty(false)
showErrorView(false)
}
analytics.logEvent("load_lucky_number", "lucky_number" to it.luckyNumber, "force_refresh" to forceRefresh)
analytics.logEvent(
"load_item",
"type" to "lucky_number",
"number" to it.luckyNumber,
"force_refresh" to forceRefresh
)
}, {
Timber.i("Loading lucky number result: An exception occurred")
errorHandler.dispatch(it)

View File

@ -31,6 +31,7 @@ import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.more.MoreFragment
import io.github.wulkanowy.ui.modules.note.NoteFragment
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.safelyPopFragments
@ -46,6 +47,9 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
@Inject
lateinit var navController: FragNavController
@Inject
lateinit var analytics: FirebaseAnalyticsHelper
@Inject
lateinit var overlayProvider: Lazy<ElevationOverlayProvider>
@ -136,6 +140,10 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
}
}
override fun setCurrentScreen(name: String?) {
analytics.setCurrentScreen(this, name)
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
return if (item?.itemId == R.id.mainMenuAccount) presenter.onAccountManagerSelected()
else false

View File

@ -37,8 +37,9 @@ class MainPresenter @Inject constructor(
analytics.logEvent("app_open", "destination" to initMenu?.name)
}
fun onViewChange(section: MainView.Section?) {
fun onViewChange(section: MainView.Section?, name: String?) {
view?.apply {
setCurrentScreen(name)
showActionBarElevation(section != GRADE && section != MESSAGE && section != SCHOOL)
currentViewTitle?.let { setViewTitle(it) }
currentViewSubtitle?.let { setViewSubTitle(it.ifBlank { null }) }

View File

@ -24,6 +24,8 @@ interface MainView : BaseView {
fun showAccountPicker()
fun setCurrentScreen(name: String?)
fun showActionBarElevation(show: Boolean)
fun notifyMenuViewReselected()

View File

@ -63,7 +63,7 @@ class MessagePreviewAdapter @Inject constructor() :
@SuppressLint("SetTextI18n")
private fun bindMessage(holder: MessageViewHolder, message: Message) {
with(holder.binding) {
messagePreviewSubject.text = if (message.subject.isNotBlank()) message.subject else root.context.getString(R.string.message_no_subject)
messagePreviewSubject.text = message.subject.ifBlank { root.context.getString(R.string.message_no_subject) }
messagePreviewDate.text = root.context.getString(R.string.message_date, message.date.toFormattedString("yyyy-MM-dd HH:mm:ss"))
messagePreviewContent.text = message.content
messagePreviewAuthor.text = if (message.folderId == MessageFolder.SENT.id) "${root.context.getString(R.string.message_to)} ${message.recipient}"

View File

@ -1,12 +1,20 @@
package io.github.wulkanowy.ui.modules.message.preview
import android.os.Build
import android.os.Bundle
import android.print.PrintAttributes
import android.print.PrintManager
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.VISIBLE
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService
import androidx.recyclerview.widget.LinearLayoutManager
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Message
@ -17,6 +25,8 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.message.send.SendMessageActivity
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.shareText
import javax.inject.Inject
class MessagePreviewFragment :
@ -29,18 +39,31 @@ class MessagePreviewFragment :
@Inject
lateinit var previewAdapter: MessagePreviewAdapter
@Inject
lateinit var appInfo: AppInfo
private var menuReplyButton: MenuItem? = null
private var menuForwardButton: MenuItem? = null
private var menuDeleteButton: MenuItem? = null
private var menuShareButton: MenuItem? = null
private var menuPrintButton: MenuItem? = null
override val titleStringId: Int
get() = R.string.message_title
override val deleteMessageSuccessString: String
get() = getString(R.string.message_delete_success)
override val messageNoSubjectString: String
get() = getString(R.string.message_no_subject)
override val printHTML: String
get() = requireContext().assets.open("message-print-page.html").bufferedReader().use { it.readText() }
companion object {
const val MESSAGE_ID_KEY = "message_id"
@ -77,6 +100,8 @@ class MessagePreviewFragment :
menuReplyButton = menu.findItem(R.id.messagePreviewMenuReply)
menuForwardButton = menu.findItem(R.id.messagePreviewMenuForward)
menuDeleteButton = menu.findItem(R.id.messagePreviewMenuDelete)
menuShareButton = menu.findItem(R.id.messagePreviewMenuShare)
menuPrintButton = menu.findItem(R.id.messagePreviewMenuPrint)
presenter.onCreateOptionsMenu()
}
@ -85,6 +110,8 @@ class MessagePreviewFragment :
R.id.messagePreviewMenuReply -> presenter.onReply()
R.id.messagePreviewMenuForward -> presenter.onForward()
R.id.messagePreviewMenuDelete -> presenter.onMessageDelete()
R.id.messagePreviewMenuShare -> presenter.onShare()
R.id.messagePreviewMenuPrint -> presenter.onPrint()
else -> false
}
}
@ -108,6 +135,8 @@ class MessagePreviewFragment :
menuReplyButton?.isVisible = show
menuForwardButton?.isVisible = show
menuDeleteButton?.isVisible = show
menuShareButton?.isVisible = show
menuPrintButton?.isVisible = show && appInfo.systemVersion >= Build.VERSION_CODES.LOLLIPOP
}
override fun setDeletedOptionsLabels() {
@ -138,6 +167,38 @@ class MessagePreviewFragment :
context?.let { it.startActivity(SendMessageActivity.getStartIntent(it, message)) }
}
override fun shareText(text: String, subject: String) {
context?.shareText(text, subject)
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun printDocument(html: String, jobName: String) {
val webView = WebView(activity)
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest) = false
override fun onPageFinished(view: WebView, url: String) {
createWebPrintJob(view, jobName)
}
}
webView.loadDataWithBaseURL("file:///android_asset/", html, "text/HTML", "UTF-8", null)
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun createWebPrintJob(webView: WebView, jobName: String) {
activity?.getSystemService<PrintManager>()?.let { printManager ->
val printAdapter = webView.createPrintDocumentAdapter(jobName)
printManager.print(
jobName,
printAdapter,
PrintAttributes.Builder().build()
)
}
}
override fun popView() {
(activity as MainActivity).popView()
}

View File

@ -1,12 +1,17 @@
package io.github.wulkanowy.ui.modules.message.preview
import android.annotation.SuppressLint
import android.os.Build
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageAttachment
import io.github.wulkanowy.data.repositories.message.MessageRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.toFormattedString
import timber.log.Timber
import javax.inject.Inject
@ -15,11 +20,14 @@ class MessagePreviewPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val messageRepository: MessageRepository,
private val analytics: FirebaseAnalyticsHelper
private val analytics: FirebaseAnalyticsHelper,
private var appInfo: AppInfo
) : BasePresenter<MessagePreviewView>(errorHandler, studentRepository, schedulers) {
var message: Message? = null
var attachments: List<MessageAttachment>? = null
private lateinit var lastError: Throwable
private var retryCallback: () -> Unit = {}
@ -56,11 +64,16 @@ class MessagePreviewPresenter @Inject constructor(
.subscribe({ message ->
Timber.i("Loading message ${message.message.messageId} preview result: Success ")
this@MessagePreviewPresenter.message = message.message
this@MessagePreviewPresenter.attachments = message.attachments
view?.apply {
setMessageWithAttachment(message)
initOptions()
}
analytics.logEvent("load_message_preview", "length" to message.message.content.length)
analytics.logEvent(
"load_item",
"type" to "message_preview",
"length" to message.message.content.length
)
}) {
Timber.i("Loading message ${message.messageId} preview result: An exception occurred ")
retryCallback = { onMessageLoadRetry(message) }
@ -83,6 +96,60 @@ class MessagePreviewPresenter @Inject constructor(
} else false
}
fun onShare(): Boolean {
message?.let {
var text = "Temat: ${it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }}\n" + when (it.sender.isNotEmpty()) {
true -> "Od: ${it.sender}\n"
false -> "Do: ${it.recipient}\n"
} + "Data: ${it.date.toFormattedString("yyyy-MM-dd HH:mm:ss")}\n\n${it.content}"
attachments?.let { attachments ->
if (attachments.isNotEmpty()) {
text += "\n\nZałączniki:"
attachments.forEach { attachment ->
text += "\n${attachment.filename}: ${attachment.url}"
}
}
}
view?.shareText(text, "FW: ${it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }}")
return true
}
return false
}
@SuppressLint("NewApi")
fun onPrint(): Boolean {
if (appInfo.systemVersion < Build.VERSION_CODES.LOLLIPOP) return false
message?.let {
val dateString = it.date.toFormattedString("yyyy-MM-dd HH:mm:ss")
val infoContent = "<div><h4>Data wysłania</h4>$dateString</div>" + when {
it.sender.isNotEmpty() -> "<div><h4>Od</h4>${it.sender}</div>"
else -> "<div><h4>Do</h4>${it.recipient}</div>"
}
val messageContent = "<p>${it.content}</p>"
.replace(Regex("[\\n\\r]{2,}"), "</p><p>")
.replace(Regex("[\\n\\r]"), "<br>")
val jobName = "Wiadomość " + when {
it.sender.isNotEmpty() -> "od ${it.sender}"
else -> "do ${it.recipient}"
} + " $dateString: ${it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }} | Wulkanowy"
view?.apply {
val html = printHTML
.replace("%SUBJECT%", it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() })
.replace("%CONTENT%", messageContent)
.replace("%INFO%", infoContent)
printDocument(html, jobName)
}
return true
}
return false
}
private fun deleteMessage() {
message?.let { message ->
disposable.add(studentRepository.getCurrentStudent()

View File

@ -1,5 +1,7 @@
package io.github.wulkanowy.ui.modules.message.preview
import android.os.Build
import androidx.annotation.RequiresApi
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageWithAttachment
import io.github.wulkanowy.ui.base.BaseView
@ -8,6 +10,10 @@ interface MessagePreviewView : BaseView {
val deleteMessageSuccessString: String
val messageNoSubjectString: String
val printHTML: String
fun initView()
fun setMessageWithAttachment(item: MessageWithAttachment)
@ -34,5 +40,10 @@ interface MessagePreviewView : BaseView {
fun openMessageForward(message: Message?)
fun shareText(text: String, subject: String)
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun printDocument(html: String, jobName: String)
fun popView()
}

View File

@ -4,9 +4,9 @@ import android.graphics.Typeface
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SortedList
import androidx.recyclerview.widget.SortedListAdapterCallback
import androidx.recyclerview.widget.RecyclerView.NO_POSITION
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.repositories.message.MessageFolder
@ -19,39 +19,23 @@ class MessageTabAdapter @Inject constructor() :
var onClickListener: (Message, position: Int) -> Unit = { _, _ -> }
private val items = SortedList(Message::class.java, object :
SortedListAdapterCallback<Message>(this) {
private var items = mutableListOf<Message>()
override fun compare(item1: Message, item2: Message): Int {
return item2.date.compareTo(item1.date)
}
override fun areContentsTheSame(oldItem: Message?, newItem: Message?): Boolean {
return oldItem == newItem
}
override fun areItemsTheSame(item1: Message, item2: Message): Boolean {
return item1 == item2
}
})
fun replaceAll(models: List<Message>) {
items.beginBatchedUpdates()
for (i in items.size() - 1 downTo 0) {
val model = items.get(i)
if (model !in models) {
items.remove(model)
}
}
items.addAll(models)
items.endBatchedUpdates()
fun setDataItems(data: List<Message>) {
val diffResult = DiffUtil.calculateDiff(MessageTabDiffUtil(items, data))
items = data.toMutableList()
diffResult.dispatchUpdatesTo(this)
}
fun updateItem(position: Int, item: Message) {
items.updateItemAt(position, item)
val currentItem = items[position]
items[position] = item
if (item != currentItem) {
notifyItemChanged(position)
}
}
override fun getItemCount() = items.size()
override fun getItemCount() = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder(
ItemMessageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
@ -77,9 +61,26 @@ class MessageTabAdapter @Inject constructor() :
}
messageItemAttachmentIcon.visibility = if (item.hasAttachments) View.VISIBLE else View.GONE
root.setOnClickListener { onClickListener(item, position) }
root.setOnClickListener {
holder.adapterPosition.let { if (it != NO_POSITION) onClickListener(item, it) }
}
}
}
class ItemViewHolder(val binding: ItemMessageBinding) : RecyclerView.ViewHolder(binding.root)
private class MessageTabDiffUtil(private val old: List<Message>, private val new: List<Message>) :
DiffUtil.Callback() {
override fun getOldListSize(): Int = old.size
override fun getNewListSize(): Int = new.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return old[oldItemPosition].id == new[newItemPosition].id
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return old[oldItemPosition] == new[newItemPosition]
}
}
}

View File

@ -90,7 +90,7 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
}
override fun updateData(data: List<Message>) {
tabAdapter.replaceAll(data)
tabAdapter.setDataItems(data)
}
override fun updateItem(item: Message, position: Int) {

View File

@ -1,6 +1,5 @@
package io.github.wulkanowy.ui.modules.message.tab
import android.annotation.SuppressLint
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.repositories.message.MessageFolder
import io.github.wulkanowy.data.repositories.message.MessageRepository
@ -11,8 +10,13 @@ import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.toFormattedString
import io.reactivex.subjects.PublishSubject
import me.xdrop.fuzzywuzzy.FuzzySearch
import timber.log.Timber
import java.util.Locale
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.math.pow
class MessageTabPresenter @Inject constructor(
schedulers: SchedulersProvider,
@ -31,9 +35,12 @@ class MessageTabPresenter @Inject constructor(
private var messages = emptyList<Message>()
private val searchQuery = PublishSubject.create<String>()
fun onAttachView(view: MessageTabView, folder: MessageFolder) {
super.onAttachView(view)
view.initView()
initializeSearchStream()
errorHandler.showErrorMessage = ::showErrorViewOnError
this.folder = folder
}
@ -64,7 +71,7 @@ class MessageTabPresenter @Inject constructor(
}
fun onMessageItemSelected(message: Message, position: Int) {
Timber.i("Select message ${message.id} item")
Timber.i("Select message ${message.id} item (position: $position)")
view?.run {
openMessage(message)
if (message.unread) {
@ -76,33 +83,35 @@ class MessageTabPresenter @Inject constructor(
private fun loadData(forceRefresh: Boolean) {
Timber.i("Loading $folder message data started")
disposable.apply {
clear()
add(studentRepository.getCurrentStudent()
.flatMap { student ->
semesterRepository.getCurrentSemester(student)
.flatMap { messageRepository.getMessages(student, it, folder, forceRefresh) }
disposable.add(studentRepository.getCurrentStudent()
.flatMap { student ->
semesterRepository.getCurrentSemester(student)
.flatMap { messageRepository.getMessages(student, it, folder, forceRefresh) }
}
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doFinally {
view?.run {
showRefresh(false)
showProgress(false)
enableSwipe(true)
notifyParentDataLoaded()
}
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doFinally {
view?.run {
showRefresh(false)
showProgress(false)
enableSwipe(true)
notifyParentDataLoaded()
}
}
.subscribe({
Timber.i("Loading $folder message result: Success")
messages = it
onSearchQueryTextChange(lastSearchQuery)
analytics.logEvent("load_messages", "items" to it.size, "folder" to folder.name)
}) {
Timber.i("Loading $folder message result: An exception occurred")
errorHandler.dispatch(it)
})
}
}
.subscribe({
Timber.i("Loading $folder message result: Success")
messages = it
view?.updateData(getFilteredData(lastSearchQuery))
analytics.logEvent(
"load_data",
"type" to "messages",
"items" to it.size,
"folder" to folder.name
)
}) {
Timber.i("Loading $folder message result: An exception occurred")
errorHandler.dispatch(it)
})
}
private fun showErrorViewOnError(message: String, error: Throwable) {
@ -116,23 +125,36 @@ class MessageTabPresenter @Inject constructor(
}
}
@SuppressLint("DefaultLocale")
fun onSearchQueryTextChange(query: String) {
lastSearchQuery = query
if (query != searchQuery.toString())
searchQuery.onNext(query)
}
val lowerCaseQuery = query.toLowerCase()
val filteredList = mutableListOf<Message>()
messages.forEach {
if (lowerCaseQuery in it.subject.toLowerCase() ||
lowerCaseQuery in it.sender.toLowerCase() ||
lowerCaseQuery in it.recipient.toLowerCase() ||
lowerCaseQuery in it.date.toFormattedString()
) {
filteredList.add(it)
private fun initializeSearchStream() {
disposable.add(searchQuery
.debounce(250, TimeUnit.MILLISECONDS)
.map { query ->
lastSearchQuery = query
getFilteredData(query)
}
}
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.subscribe({
Timber.d("Applying filter. Full list: ${messages.size}, filtered: ${it.size}")
updateData(it)
}) { Timber.e(it) })
}
updateData(filteredList)
private fun getFilteredData(query: String): List<Message> {
return if (query.trim().isEmpty()) {
messages.sortedByDescending { it.date }
} else {
messages
.map { it to calculateMatchRatio(it, query) }
.sortedByDescending { it.second }
.filter { it.second > 5000 }
.map { it.first }
}
}
private fun updateData(data: List<Message>) {
@ -144,4 +166,42 @@ class MessageTabPresenter @Inject constructor(
resetListPosition()
}
}
private fun calculateMatchRatio(message: Message, query: String): Int {
val subjectRatio = FuzzySearch.tokenSortPartialRatio(
query.toLowerCase(Locale.getDefault()),
message.subject
)
val senderOrRecipientRatio = FuzzySearch.tokenSortPartialRatio(
query.toLowerCase(Locale.getDefault()),
if (message.sender.isNotEmpty()) message.sender.toLowerCase(Locale.getDefault())
else message.recipient.toLowerCase(Locale.getDefault())
)
val dateRatio = listOf(
FuzzySearch.ratio(
query.toLowerCase(Locale.getDefault()),
message.date.toFormattedString("dd.MM").toLowerCase(Locale.getDefault())
),
FuzzySearch.ratio(
query.toLowerCase(Locale.getDefault()),
message.date.toFormattedString("dd.MM.yyyy").toLowerCase(Locale.getDefault())
),
FuzzySearch.ratio(
query.toLowerCase(Locale.getDefault()),
message.date.toFormattedString("d MMMM").toLowerCase(Locale.getDefault())
),
FuzzySearch.ratio(
query.toLowerCase(Locale.getDefault()),
message.date.toFormattedString("d MMMM yyyy").toLowerCase(Locale.getDefault())
)
).max() ?: 0
return (subjectRatio.toDouble().pow(2)
+ senderOrRecipientRatio.toDouble().pow(2)
+ dateRatio.toDouble().pow(2) * 2
).toInt()
}
}

View File

@ -70,7 +70,12 @@ class MobileDevicePresenter @Inject constructor(
showEmpty(it.isEmpty())
showErrorView(false)
}
analytics.logEvent("load_devices", "items" to it.size, "force_refresh" to forceRefresh)
analytics.logEvent(
"load_data",
"type" to "devices",
"items" to it.size,
"force_refresh" to forceRefresh
)
}) {
Timber.i("Loading mobile devices result: An exception occurred")
errorHandler.dispatch(it)

View File

@ -69,7 +69,12 @@ class NotePresenter @Inject constructor(
showErrorView(false)
showContent(it.isNotEmpty())
}
analytics.logEvent("load_note", "items" to it.size, "force_refresh" to forceRefresh)
analytics.logEvent(
"load_data",
"type" to "note",
"items" to it.size,
"force_refresh" to forceRefresh
)
}, {
Timber.i("Loading note result: An exception occurred")
errorHandler.dispatch(it)

View File

@ -88,7 +88,11 @@ class SchoolPresenter @Inject constructor(
showEmpty(false)
showErrorView(false)
}
analytics.logEvent("load_school", "force_refresh" to forceRefresh)
analytics.logEvent(
"load_item",
"type" to "school",
"force_refresh" to forceRefresh
)
}, {
Timber.i("Loading school result: An exception occurred")
errorHandler.dispatch(it)

View File

@ -75,7 +75,12 @@ class TeacherPresenter @Inject constructor(
showEmpty(it.isEmpty())
showErrorView(false)
}
analytics.logEvent("load_teachers", "items" to it.size, "force_refresh" to forceRefresh)
analytics.logEvent(
"load_data",
"type" to "teachers",
"items" to it.size,
"force_refresh" to forceRefresh
)
}) {
Timber.i("Loading teachers result: An exception occurred")
errorHandler.dispatch(it)

View File

@ -7,6 +7,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.thelittlefireman.appkillermanager.AppKillerManager
import com.thelittlefireman.appkillermanager.exceptions.NoActionFoundException
import com.yariksoffice.lingver.Lingver
import dagger.android.support.AndroidSupportInjection
import io.github.wulkanowy.R
@ -14,6 +15,7 @@ import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.base.ErrorDialog
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.openInternetBrowser
import javax.inject.Inject
class SettingsFragment : PreferenceFragmentCompat(),
@ -133,9 +135,13 @@ class SettingsFragment : PreferenceFragmentCompat(),
.setMessage(R.string.pref_notify_fix_sync_issues_message)
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.setPositiveButton(R.string.pref_notify_fix_sync_issues_settings_button) { _, _ ->
AppKillerManager.doActionPowerSaving(requireContext())
AppKillerManager.doActionAutoStart(requireContext())
AppKillerManager.doActionNotification(requireContext())
try {
AppKillerManager.doActionPowerSaving(requireContext())
AppKillerManager.doActionAutoStart(requireContext())
AppKillerManager.doActionNotification(requireContext())
} catch (e: NoActionFoundException) {
requireContext().openInternetBrowser("https://dontkillmyapp.com/${AppKillerManager.getDevice()?.manufacturer}", ::showMessage)
}
}
.show()
}

View File

@ -132,41 +132,46 @@ class TimetableAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
}
private fun updateTimeLeft(binding: ItemTimetableBinding, lesson: Timetable, position: Int) {
val isShowTimeUntil = lesson.isShowTimeUntil(getPreviousLesson(position))
val until = lesson.until
val left = lesson.left
val isJustFinished = lesson.isJustFinished
with(binding) {
when {
// before lesson
lesson.isShowTimeUntil(getPreviousLesson(position)) -> {
isShowTimeUntil -> {
Timber.d("Show time until lesson: $position")
timetableItemTimeLeft.visibility = GONE
with(timetableItemTimeUntil) {
visibility = VISIBLE
text = context.getString(R.string.timetable_time_until,
if (lesson.until.seconds <= 60) {
context.getString(R.string.timetable_seconds, lesson.until.seconds.toString(10))
if (until.seconds <= 60) {
context.getString(R.string.timetable_seconds, until.seconds.toString(10))
} else {
context.getString(R.string.timetable_minutes, lesson.until.toMinutes().toString(10))
context.getString(R.string.timetable_minutes, until.toMinutes().toString(10))
}
)
}
}
// after lesson start
lesson.left != null -> {
left != null -> {
Timber.d("Show time left lesson: $position")
timetableItemTimeUntil.visibility = GONE
with(timetableItemTimeLeft) {
visibility = VISIBLE
text = context.getString(
R.string.timetable_time_left,
if (lesson.left!!.seconds < 60) {
context.getString(R.string.timetable_seconds, lesson.left?.seconds?.toString(10))
if (left.seconds < 60) {
context.getString(R.string.timetable_seconds, left.seconds.toString(10))
} else {
context.getString(R.string.timetable_minutes, lesson.left?.toMinutes()?.toString(10))
context.getString(R.string.timetable_minutes, left.toMinutes().toString(10))
}
)
}
}
// right after lesson finish
lesson.isJustFinished -> {
isJustFinished -> {
Timber.d("Show just finished lesson: $position")
timetableItemTimeUntil.visibility = GONE
timetableItemTimeLeft.visibility = VISIBLE

View File

@ -154,7 +154,12 @@ class TimetablePresenter @Inject constructor(
showErrorView(false)
showContent(it.isNotEmpty())
}
analytics.logEvent("load_timetable", "items" to it.size, "force_refresh" to forceRefresh)
analytics.logEvent(
"load_data",
"type" to "timetable",
"items" to it.size,
"force_refresh" to forceRefresh
)
}) {
Timber.i("Loading timetable result: An exception occurred")
errorHandler.dispatch(it)

View File

@ -43,6 +43,7 @@ class CompletedLessonsPresenter @Inject constructor(
completedLessonsErrorHandler.showErrorMessage = ::showErrorViewOnError
completedLessonsErrorHandler.onFeatureDisabled = {
this.view?.showFeatureDisabled()
this.view?.showEmpty(true)
Timber.i("Completed lessons feature disabled by school")
}
loadData(ofEpochDay(date ?: baseDate.toEpochDay()))
@ -134,7 +135,12 @@ class CompletedLessonsPresenter @Inject constructor(
showErrorView(false)
showContent(it.isNotEmpty())
}
analytics.logEvent("load_completed_lessons", "items" to it.size, "force_refresh" to forceRefresh)
analytics.logEvent(
"load_data",
"type" to "completed_lessons",
"items" to it.size,
"force_refresh" to forceRefresh
)
}) {
Timber.i("Loading completed lessons result: An exception occurred")
completedLessonsErrorHandler.dispatch(it)

View File

@ -10,7 +10,7 @@ import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import io.github.wulkanowy.R
import io.github.wulkanowy.BuildConfig.APPLICATION_ID
@ColorInt
fun Context.getThemeAttrColor(@AttrRes colorAttr: Int): Int {
@ -39,6 +39,12 @@ fun Context.openInternetBrowser(uri: String, onActivityNotFound: (uri: String) -
}
}
fun Context.openAppInMarket(onActivityNotFound: (uri: String) -> Unit) {
openInternetBrowser("market://details?id=${APPLICATION_ID}") {
openInternetBrowser("https://github.com/wulkanowy/wulkanowy/releases", onActivityNotFound)
}
}
fun Context.openEmailClient(chooserTitle: String, email: String, subject: String, body: String, onActivityNotFound: () -> Unit = {}) {
val intent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:")).apply {
putExtra(Intent.EXTRA_EMAIL, arrayOf(email))
@ -65,4 +71,17 @@ fun Context.openDialer(phone: String) {
startActivity(intent)
}
fun Context.shareText(text: String, subject: String?) {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, text)
if (subject != null) {
putExtra(Intent.EXTRA_SUBJECT, subject)
}
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
startActivity(shareIntent)
}
fun Context.dpToPx(dp: Float) = dp * resources.displayMetrics.densityDpi / DENSITY_DEFAULT

View File

@ -4,14 +4,14 @@ import androidx.fragment.app.Fragment
import com.ncapdevi.fragnav.FragNavController
import io.github.wulkanowy.ui.modules.main.MainView
inline fun FragNavController.setOnViewChangeListener(crossinline listener: (section: MainView.Section?) -> Unit) {
inline fun FragNavController.setOnViewChangeListener(crossinline listener: (section: MainView.Section?, name: String?) -> Unit) {
transactionListener = object : FragNavController.TransactionListener {
override fun onFragmentTransaction(fragment: Fragment?, transactionType: FragNavController.TransactionType) {
listener(fragment?.toSection())
listener(fragment?.toSection(), fragment?.let { it::class.java.simpleName })
}
override fun onTabTransaction(fragment: Fragment?, index: Int) {
listener(fragment?.toSection())
listener(fragment?.toSection(), fragment?.let { it::class.java.simpleName })
}
}
}

View File

@ -52,4 +52,4 @@ class LifecycleAwareVariableActivity<T : Any> : ReadWriteProperty<AppCompatActiv
@Suppress("unused")
fun <T : Any> Fragment.lifecycleAwareVariable() = LifecycleAwareVariable<T>()
fun <T : Any> AppCompatActivity.lifecycleAwareVariable() = LifecycleAwareVariableActivity<T>()
fun <T : Any> lifecycleAwareVariable() = LifecycleAwareVariableActivity<T>()

View File

@ -17,7 +17,7 @@ class SchooldaysRangeLimiter : DateRangeLimiter {
override fun isOutOfRange(year: Int, month: Int, day: Int): Boolean {
val date = LocalDate.of(year, month + 1, day)
val dayOfWeek = date.dayOfWeek
return dayOfWeek == DayOfWeek.SUNDAY || dayOfWeek == DayOfWeek.SATURDAY || date.isHolidays
return dayOfWeek == DayOfWeek.SUNDAY || date.isHolidays
}
override fun getStartDate(): Calendar {

View File

@ -23,6 +23,8 @@ fun Sdk.init(student: Student): Sdk {
certKey = student.certificateKey
privateKey = student.privateKey
emptyCookieJarInterceptor = true
Timber.d("Sdk in ${student.loginMode} mode reinitialized")
return this

View File

@ -92,8 +92,8 @@ inline val LocalDate.weekDayName: String
inline val LocalDate.monday: LocalDate
get() = with(MONDAY)
inline val LocalDate.friday: LocalDate
get() = with(FRIDAY)
inline val LocalDate.sunday: LocalDate
get() = with(SUNDAY)
/**
* [Dz.U. 2016 poz. 1335](http://prawo.sejm.gov.pl/isap.nsf/DocDetails.xsp?id=WDU20160001335)

View File

@ -1,10 +1,8 @@
Wersja 0.18.0
- naprawiliśmy odświeżanie zadań domowych
- naprawiliśmy powiadomienia na androidzie 8.0
- oceny powinny się teraz odświeżać trochę szybciej
- dodaliśmy tryb pełnoekranowy w zadaniach
- dodaliśmy wyszukiwanie w wiadomościach
- dodaliśmy opcje oznaczania bieżącej lekcji na planie/w powiadomieniu (domyślnie wyłączone)
- dodaliśmy testową opcję naprawy powiadomień na np. Huawei, Xiaomi (znajdziesz ją w ustawieniach)
Wersja 0.19.0
- naprawiliśmy pokazywanie brakujących przedmiotów na liście podsumowania ocen
- ulepszyliśmy wygląd menadżera kont
- ulepszyliśmy wyszukiwarkę wiadomości
- dodaliśmy powiadomienia o proponowanych i końcowych ocenach
- dodaliśmy opcję udostępniania i drukowania wiadomości
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M19,8h-1L18,3L6,3v5L5,8c-1.66,0 -3,1.34 -3,3v6h4v4h12v-4h4v-6c0,-1.66 -1.34,-3 -3,-3zM8,5h8v3L8,8L8,5zM16,17v2L8,19v-4h8v2zM18,15v-2L6,13v2L4,15v-4c0,-0.55 0.45,-1 1,-1h14c0.55,0 1,0.45 1,1v4h-2z" />
<path
android:fillColor="@android:color/white"
android:pathData="M18,11.5m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92s2.92,-1.31 2.92,-2.92c0,-1.61 -1.31,-2.92 -2.92,-2.92zM18,4c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM6,13c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1zM18,20.02c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1z" />
</vector>

View File

@ -148,7 +148,7 @@
android:hint="@string/message_content"
android:imeOptions="flagNoExtractUi"
android:importantForAutofill="no"
android:inputType="textMultiLine"
android:inputType="textMultiLine|textCapSentences"
android:minHeight="58dp"
android:paddingStart="16dp"
android:paddingLeft="16dp"

View File

@ -5,45 +5,60 @@
android:layout_width="300dp"
android:layout_height="wrap_content">
<TextView
android:id="@+id/accountDialogTitle"
android:layout_width="match_parent"
android:layout_height="64dp"
android:paddingStart="24dp"
android:paddingLeft="24dp"
android:paddingEnd="24dp"
android:paddingRight="24dp"
android:text="@string/account_title"
android:textSize="20sp"
android:textStyle="bold"
app:firstBaselineToTopHeight="40dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/accountDialogRecycler"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/accountDialogTitle"
android:overScrollMode="never"
tools:itemCount="3"
tools:listitem="@layout/item_account" />
android:orientation="vertical">
<com.google.android.material.button.MaterialButton
android:id="@+id/accountDialogAdd"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/accountDialogRecycler"
android:layout_margin="8dp"
android:text="@string/account_add_new"
android:textColor="?android:textColorPrimary" />
<TextView
android:id="@+id/accountDialogTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="24dp"
android:paddingLeft="24dp"
android:paddingEnd="24dp"
android:paddingRight="24dp"
android:text="@string/account_title"
android:textSize="20sp"
android:textStyle="bold"
app:firstBaselineToTopHeight="40dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/accountDialogRemove"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/accountDialogRecycler"
android:layout_alignParentEnd="true"
android:layout_margin="8dp"
android:text="@string/account_logout" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/accountDialogRecycler"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_weight="1"
android:overScrollMode="never"
tools:itemCount="3"
tools:listitem="@layout/item_account" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/accountDialogAdd"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="@string/account_add_new"
android:textColor="?android:textColorPrimary" />
<Space
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/accountDialogRemove"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="@string/account_logout" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>

View File

@ -153,8 +153,8 @@
android:background="?selectableItemBackgroundBorderless"
android:fontFamily="sans-serif"
android:gravity="center"
android:text="@string/app_name"
android:textSize="16sp" />
android:textSize="16sp"
tools:text="@tools:sample/date/ddmmyy" />
<ImageButton
android:id="@+id/attendanceNextButton"

View File

@ -138,8 +138,8 @@
android:layout_height="match_parent"
android:fontFamily="sans-serif"
android:gravity="center"
android:text="@string/app_name"
android:textSize="16sp" />
android:textSize="16sp"
tools:text="@tools:sample/date/ddmmyy" />
<ImageButton
android:id="@+id/examNextButton"

View File

@ -138,8 +138,8 @@
android:layout_height="match_parent"
android:fontFamily="sans-serif"
android:gravity="center"
android:text="@string/app_name"
android:textSize="16sp" />
android:textSize="16sp"
tools:text="@tools:sample/date/ddmmyy" />
<ImageButton
android:id="@+id/homeworkNextButton"

View File

@ -67,7 +67,6 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:layout_weight="1"
android:text="@string/login_contact_email"
app:icon="@drawable/ic_more_messages" />
@ -78,7 +77,6 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_weight="1"
android:text="@string/about_faq"
app:icon="@drawable/ic_about_faq" />
@ -182,7 +180,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:layout_marginRight="24dp"
android:text="@string/login_recover_button"
android:textAppearance="?android:textAppearance"
app:backgroundTint="?android:windowBackground"
@ -223,9 +220,9 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:text="@string/login_advanced"
android:textAppearance="?android:textAppearance"
android:visibility="gone"
app:backgroundTint="?android:windowBackground"
app:fontFamily="sans-serif-medium"
app:layout_constraintBottom_toBottomOf="@id/loginFormSignIn"
@ -241,7 +238,6 @@
android:layout_gravity="center_vertical"
android:layout_marginTop="48dp"
android:layout_marginEnd="24dp"
android:layout_marginRight="24dp"
android:layout_marginBottom="16dp"
android:text="@string/login_sign_in"
app:layout_constraintBottom_toBottomOf="parent"

View File

@ -24,10 +24,10 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:visibility="gone"
tools:visibility="visible">
<View
@ -42,9 +42,9 @@
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="32dp"
android:layout_marginRight="32dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:gravity="center_horizontal"
android:text="@string/login_contact_header"
@ -55,28 +55,29 @@
android:id="@+id/loginStudentSelectContactButtons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp">
android:layout_marginRight="16dp"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:id="@+id/loginStudentSelectContactEmail"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:layout_weight="1"
android:text="@string/login_contact_email"
app:icon="@drawable/ic_more_messages" />
<com.google.android.material.button.MaterialButton
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:id="@+id/loginStudentSelectContactDiscord"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_weight="1"
android:text="@string/login_contact_discord"
app:icon="@drawable/ic_about_discord" />
</LinearLayout>
@ -95,9 +96,9 @@
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="32dp"
android:layout_marginRight="32dp"
android:layout_marginTop="32dp"
android:layout_marginBottom="32dp"
android:gravity="center_horizontal"
android:text="@string/login_select_student"
@ -129,8 +130,8 @@
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginEnd="24dp"
android:layout_marginRight="24dp"
android:layout_marginBottom="32dp"
android:enabled="false"
android:text="@string/login_sign_in"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"

View File

@ -139,8 +139,8 @@
android:background="?selectableItemBackgroundBorderless"
android:fontFamily="sans-serif"
android:gravity="center"
android:text="@string/app_name"
android:textSize="16sp" />
android:textSize="16sp"
tools:text="@tools:sample/date/ddmmyy" />
<ImageButton
android:id="@+id/timetableNextButton"

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