Compare commits

...

32 Commits
1.2.2 ... 1.3.0

Author SHA1 Message Date
689012131f Merge branch 'release/1.3.0' 2021-09-28 23:26:18 +02:00
6cdcf92782 Version 1.3.0 2021-09-28 23:26:10 +02:00
9c8bcbfdd3 New Crowdin updates (#1544) 2021-09-28 21:11:59 +00:00
0b83a66b85 Remove disappearing teachers workaround from timetable repository (#1545) 2021-09-28 23:10:11 +02:00
9711cc868c New Crowdin updates (#1522) 2021-09-28 22:42:06 +02:00
f8cb7599e6 Add missing auto refresh to recipients, subjects and teachers (#1540) 2021-09-28 22:40:43 +02:00
7636618e23 Update License (#1542) 2021-09-28 21:55:40 +02:00
5bc54c12f1 Add option to make upcoming lesson notification not persistent (#1537) 2021-09-28 11:48:25 +02:00
e10e530dee Remove seconds from timetable timer (#1539) 2021-09-27 23:03:59 +02:00
d69118b085 Add notifications center (#1524) 2021-09-27 20:58:25 +02:00
dc90549b9d Fix hiding last element in messages (#1538) 2021-09-27 17:56:43 +02:00
b552dbc904 Bump constraintlayout from 2.1.0 to 2.1.1 (#1535) 2021-09-27 15:56:11 +00:00
a6a1678b47 Bump core from 1.10.1 to 1.10.2 (#1536) 2021-09-27 15:51:06 +00:00
7a46ef5f19 Add calculated average help dialog (#1379)
Co-authored-by: Rafał Borcz <RafalBO99@outlook.com>
2021-09-25 17:19:21 +02:00
f9e0f7b390 Don't stop loading the timetable when error occurs in upcoming lessons notification scheduling (#1532) 2021-09-25 15:18:40 +02:00
9211baf7ec Add notification piggyback (#1503) 2021-09-25 14:02:38 +02:00
de6131f4f5 Add transparency to lucky number widget (#1530) 2021-09-25 13:46:35 +02:00
2cb11e443c Mark teacher with yellow when new and old are the same (#1529) 2021-09-25 13:46:11 +02:00
a43ffcdef4 Display bad credentials error in the message box above login form (#1525) 2021-09-24 21:02:51 +02:00
6615e68430 Bump kotlin_version from 1.5.30 to 1.5.31 (#1528) 2021-09-22 09:25:54 +02:00
36daa7ccc1 Always include all language resources in app bundle (#1527) 2021-09-22 09:25:16 +02:00
6e5481f345 Upgrade Gradle Play Publisher to 3.6.0 (#1526) 2021-09-20 11:38:13 +02:00
ba1c14ca0e Merge branch 'release/1.2.3' into develop 2021-09-16 12:01:58 +02:00
c69bb2ef71 Merge branch 'release/1.2.3' 2021-09-16 12:01:54 +02:00
9cb4754132 Version 1.2.3 2021-09-16 12:01:49 +02:00
5ba8289c87 Display info in timetable as-is when lesson has change flag (#1521) 2021-09-16 11:59:23 +02:00
258782c648 New Crowdin updates (#1482) 2021-09-16 11:30:05 +02:00
c568bc1515 Fix ghost account after logout not current student (#1518) 2021-09-16 11:29:11 +02:00
da668f93cf Fix bugs in dashboard (#1517) 2021-09-16 11:24:52 +02:00
037dbd792f Add conference dialog (#1519) 2021-09-16 10:51:38 +02:00
7ec7afed87 Bump firebase-bom from 28.4.0 to 28.4.1 (#1520) 2021-09-16 08:22:06 +00:00
bea50e6db5 Merge branch 'release/1.2.2' into develop 2021-09-13 14:53:38 +02:00
135 changed files with 4631 additions and 883 deletions

View File

@ -28,15 +28,14 @@ jobs:
SERVICES_ENCRYPT_KEY: ${{ secrets.SERVICES_ENCRYPT_KEY }}
run: |
gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/google-services.json.gpg
gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/key.p12.gpg
gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg
- name: Upload apk to google play
env:
PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }}
PLAY_KEY_ALIAS: ${{ secrets.PLAY_KEY_ALIAS }}
PLAY_KEY_PASSWORD: ${{ secrets.PLAY_KEY_PASSWORD }}
PLAY_SERVICE_ACCOUNT_EMAIL: ${{ secrets.PLAY_SERVICE_ACCOUNT_EMAIL }}
PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }}
run: ./gradlew publishPlayRelease -PenableFirebase --stacktrace;
ANDROID_PUBLISHER_CREDENTIALS: ${{ secrets.ANDROID_PUBLISHER_CREDENTIALS }}
run: ./gradlew publishPlayReleaseApps -PenableFirebase --stacktrace;
deploy-app-gallery:
name: Deploy to AppGallery
@ -60,7 +59,6 @@ jobs:
SERVICES_ENCRYPT_KEY: ${{ secrets.SERVICES_ENCRYPT_KEY }}
run: |
gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/agconnect-services.json.gpg
gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/key.p12.gpg
gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg
- name: Prepare credentials
env:
@ -68,7 +66,7 @@ jobs:
run: echo $AGC_CREDENTIALS > ./app/src/release/agconnect-credentials.json
- name: Build and publish HMS version
env:
PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }}
PLAY_KEY_ALIAS: ${{ secrets.PLAY_KEY_ALIAS }}
PLAY_KEY_PASSWORD: ${{ secrets.PLAY_KEY_PASSWORD }}
PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }}
run: ./gradlew bundleHmsRelease --stacktrace && ./gradlew publishHuaweiAppGalleryHmsRelease --stacktrace

View File

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

View File

@ -21,8 +21,8 @@ android {
testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21
targetSdkVersion 30
versionCode 95
versionName "1.2.2"
versionCode 97
versionName "1.3.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
@ -96,10 +96,20 @@ android {
}
}
playConfigs {
play { enabled.set(true) }
}
buildFeatures {
viewBinding true
}
bundle {
language {
enableSplit = false
}
}
testOptions.unitTests {
includeAndroidResources = true
}
@ -130,11 +140,10 @@ kapt {
}
play {
serviceAccountEmail = System.getenv("PLAY_SERVICE_ACCOUNT_EMAIL") ?: "jan@fakelog.cf"
serviceAccountCredentials = file('key.p12')
defaultToAppBundles = false
track = 'production'
updatePriority = 3
enabled.set(false)
}
huaweiPublish {
@ -157,7 +166,7 @@ ext {
}
dependencies {
implementation "io.github.wulkanowy:sdk:1.2.2"
implementation "io.github.wulkanowy:sdk:1.3.0"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
@ -174,7 +183,7 @@ dependencies {
implementation "androidx.recyclerview:recyclerview:1.2.1"
implementation "androidx.viewpager:viewpager:1.0.0"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.constraintlayout:constraintlayout:2.1.0"
implementation "androidx.constraintlayout:constraintlayout:2.1.1"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0"
implementation "com.google.android.material:material:1.4.0"
implementation "com.github.wulkanowy:material-chips-input:2.3.1"
@ -211,11 +220,11 @@ dependencies {
implementation 'me.xdrop:fuzzywuzzy:1.3.1'
implementation 'com.fredporciuncula:flow-preferences:1.5.0'
playImplementation platform('com.google.firebase:firebase-bom:28.4.0')
playImplementation platform('com.google.firebase:firebase-bom:28.4.1')
playImplementation 'com.google.firebase:firebase-analytics-ktx'
playImplementation 'com.google.firebase:firebase-messaging:'
playImplementation 'com.google.firebase:firebase-crashlytics:'
playImplementation 'com.google.android.play:core:1.10.1'
playImplementation 'com.google.android.play:core:1.10.2'
playImplementation 'com.google.android.play:core-ktx:1.8.1'
hmsImplementation 'com.huawei.hms:hianalytics:6.2.0.301'

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -45,6 +45,7 @@
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
<activity
android:name=".ui.modules.splash.SplashActivity"
android:exported="true"
android:screenOrientation="portrait"
android:theme="@style/WulkanowyTheme.SplashScreen"
tools:ignore="LockedOrientationActivity">
@ -74,6 +75,7 @@
<activity
android:name=".ui.modules.timetablewidget.TimetableWidgetConfigureActivity"
android:excludeFromRecents="true"
android:exported="true"
android:noHistory="true"
android:theme="@style/WulkanowyTheme.WidgetAccountSwitcher">
<intent-filter>
@ -83,6 +85,7 @@
<activity
android:name=".ui.modules.luckynumberwidget.LuckyNumberWidgetConfigureActivity"
android:excludeFromRecents="true"
android:exported="true"
android:noHistory="true"
android:theme="@style/WulkanowyTheme.WidgetAccountSwitcher">
<intent-filter>
@ -93,6 +96,22 @@
<service
android:name=".services.widgets.TimetableWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<service
android:name=".services.piggyback.VulcanNotificationListenerService"
android:exported="true"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
<service
android:name=".services.messaging.AppMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<receiver
android:name=".ui.modules.timetablewidget.TimetableWidgetProvider"
@ -107,6 +126,7 @@
</receiver>
<receiver
android:name=".ui.modules.luckynumberwidget.LuckyNumberWidgetProvider"
android:exported="true"
android:label="@string/lucky_number_title">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />

View File

@ -8,8 +8,8 @@ import androidx.preference.PreferenceManager
import com.chuckerteam.chucker.api.ChuckerCollector
import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.chuckerteam.chucker.api.RetentionManager
import com.squareup.moshi.Moshi
import com.fredporciuncula.flow.preferences.FlowSharedPreferences
import com.squareup.moshi.Moshi
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -202,4 +202,8 @@ internal class RepositoryModule {
@Singleton
@Provides
fun provideSchoolAnnouncementDao(database: AppDatabase) = database.schoolAnnouncementDao
@Singleton
@Provides
fun provideNotificationDao(database: AppDatabase) = database.notificationDao
}

View File

@ -10,7 +10,6 @@ import io.github.wulkanowy.data.db.dao.AttendanceDao
import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao
import io.github.wulkanowy.data.db.dao.CompletedLessonsDao
import io.github.wulkanowy.data.db.dao.ConferenceDao
import io.github.wulkanowy.data.db.dao.SchoolAnnouncementDao
import io.github.wulkanowy.data.db.dao.ExamDao
import io.github.wulkanowy.data.db.dao.GradeDao
import io.github.wulkanowy.data.db.dao.GradePartialStatisticsDao
@ -23,8 +22,10 @@ import io.github.wulkanowy.data.db.dao.MessageAttachmentDao
import io.github.wulkanowy.data.db.dao.MessagesDao
import io.github.wulkanowy.data.db.dao.MobileDeviceDao
import io.github.wulkanowy.data.db.dao.NoteDao
import io.github.wulkanowy.data.db.dao.NotificationDao
import io.github.wulkanowy.data.db.dao.RecipientDao
import io.github.wulkanowy.data.db.dao.ReportingUnitDao
import io.github.wulkanowy.data.db.dao.SchoolAnnouncementDao
import io.github.wulkanowy.data.db.dao.SchoolDao
import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao
@ -38,7 +39,6 @@ import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.db.entities.AttendanceSummary
import io.github.wulkanowy.data.db.entities.CompletedLesson
import io.github.wulkanowy.data.db.entities.Conference
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradePartialStatistics
@ -51,9 +51,11 @@ import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageAttachment
import io.github.wulkanowy.data.db.entities.MobileDevice
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.db.entities.Notification
import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.ReportingUnit
import io.github.wulkanowy.data.db.entities.School
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentInfo
@ -95,6 +97,7 @@ import io.github.wulkanowy.data.db.migrations.Migration37
import io.github.wulkanowy.data.db.migrations.Migration38
import io.github.wulkanowy.data.db.migrations.Migration39
import io.github.wulkanowy.data.db.migrations.Migration4
import io.github.wulkanowy.data.db.migrations.Migration40
import io.github.wulkanowy.data.db.migrations.Migration5
import io.github.wulkanowy.data.db.migrations.Migration6
import io.github.wulkanowy.data.db.migrations.Migration7
@ -134,6 +137,7 @@ import javax.inject.Singleton
StudentInfo::class,
TimetableHeader::class,
SchoolAnnouncement::class,
Notification::class
],
version = AppDatabase.VERSION_SCHEMA,
exportSchema = true
@ -142,7 +146,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() {
companion object {
const val VERSION_SCHEMA = 39
const val VERSION_SCHEMA = 40
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(),
@ -183,6 +187,7 @@ abstract class AppDatabase : RoomDatabase() {
Migration37(),
Migration38(),
Migration39(),
Migration40()
)
fun newInstance(
@ -252,4 +257,6 @@ abstract class AppDatabase : RoomDatabase() {
abstract val timetableHeaderDao: TimetableHeaderDao
abstract val schoolAnnouncementDao: SchoolAnnouncementDao
abstract val notificationDao: NotificationDao
}

View File

@ -0,0 +1,15 @@
package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Query
import io.github.wulkanowy.data.db.entities.Notification
import kotlinx.coroutines.flow.Flow
import javax.inject.Singleton
@Singleton
@Dao
interface NotificationDao : BaseDao<Notification> {
@Query("SELECT * FROM Notifications WHERE student_id = :studentId OR student_id = -1")
fun loadAll(studentId: Long): Flow<List<Notification>>
}

View File

@ -0,0 +1,27 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import io.github.wulkanowy.services.sync.notifications.NotificationType
import java.time.LocalDateTime
@Entity(tableName = "Notifications")
data class Notification(
@ColumnInfo(name = "student_id")
val studentId: Long,
val title: String,
val content: String,
val type: NotificationType,
val date: LocalDateTime,
val data: String? = null
) {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}

View File

@ -0,0 +1,23 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration40 : Migration(39, 40) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `Notifications` (
`student_id` INTEGER NOT NULL,
`title` TEXT NOT NULL,
`content` TEXT NOT NULL,
`type` TEXT NOT NULL,
`date` INTEGER NOT NULL,
`data` TEXT,
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
)
"""
)
}
}

View File

@ -6,7 +6,7 @@ import androidx.annotation.StringRes
import io.github.wulkanowy.services.sync.notifications.NotificationType
import io.github.wulkanowy.ui.modules.main.MainView
sealed interface Notification {
sealed interface NotificationData {
val type: NotificationType
val startMenu: MainView.Section
val icon: Int
@ -14,7 +14,7 @@ sealed interface Notification {
val contentStringRes: Int
}
data class MultipleNotifications(
data class MultipleNotificationsData(
override val type: NotificationType,
override val startMenu: MainView.Section,
@DrawableRes override val icon: Int,
@ -23,9 +23,9 @@ data class MultipleNotifications(
@PluralsRes val summaryStringRes: Int,
val lines: List<String>,
) : Notification
) : NotificationData
data class OneNotification(
data class OneNotificationData(
override val type: NotificationType,
override val startMenu: MainView.Section,
@DrawableRes override val icon: Int,
@ -33,4 +33,4 @@ data class OneNotification(
@StringRes override val contentStringRes: Int,
val contentValues: List<String>,
) : Notification
) : NotificationData

View File

@ -32,10 +32,23 @@ class AttendanceRepository @Inject constructor(
private val cacheKey = "attendance"
fun getAttendance(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource(
fun getAttendance(
student: Student,
semester: Semester,
start: LocalDate,
end: LocalDate,
forceRefresh: Boolean,
) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) },
query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday) },
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(
key = getRefreshKey(cacheKey, semester, start, end)
)
it.isEmpty() || forceRefresh || isExpired
},
query = {
attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday)
},
fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
.getAttendance(start.monday, end.sunday, semester.semesterId)
@ -50,12 +63,17 @@ class AttendanceRepository @Inject constructor(
filterResult = { it.filter { item -> item.date in start..end } }
)
suspend fun excuseForAbsence(student: Student, semester: Semester, absenceList: List<Attendance>, reason: String? = null) {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear).excuseForAbsence(absenceList.map { attendance ->
suspend fun excuseForAbsence(
student: Student, semester: Semester,
absenceList: List<Attendance>, reason: String? = null
) {
val items = absenceList.map { attendance ->
Absent(
date = LocalDateTime.of(attendance.date, LocalTime.of(0, 0)),
timeId = attendance.timeId
)
}, reason)
}
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
.excuseForAbsence(items, reason)
}
}

View File

@ -29,12 +29,12 @@ class AttendanceSummaryRepository @Inject constructor(
student: Student,
semester: Semester,
subjectId: Int,
forceRefresh: Boolean
forceRefresh: Boolean,
) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = {
it.isEmpty() || forceRefresh
|| refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester))
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester))
it.isEmpty() || forceRefresh || isExpired
},
query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, subjectId) },
fetch = {

View File

@ -28,10 +28,28 @@ class CompletedLessonsRepository @Inject constructor(
private val cacheKey = "completed"
fun getCompletedLessons(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource(
fun getCompletedLessons(
student: Student,
semester: Semester,
start: LocalDate,
end: LocalDate,
forceRefresh: Boolean,
) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) },
query = { completedLessonsDb.loadAll(semester.studentId, semester.diaryId, start.monday, end.sunday) },
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(
key = getRefreshKey(cacheKey, semester, start, end)
)
it.isEmpty() || forceRefresh || isExpired
},
query = {
completedLessonsDb.loadAll(
studentId = semester.studentId,
diaryId = semester.diaryId,
from = start.monday,
end = end.sunday
)
},
fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
.getCompletedLessons(start.monday, end.sunday)

View File

@ -35,12 +35,12 @@ class ConferenceRepository @Inject constructor(
semester: Semester,
forceRefresh: Boolean,
notify: Boolean = false,
startDate: LocalDateTime = LocalDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC)
startDate: LocalDateTime = LocalDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC),
) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = {
it.isEmpty() || forceRefresh
|| refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester))
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester))
it.isEmpty() || forceRefresh || isExpired
},
query = {
conferenceDb.loadAll(semester.diaryId, student.studentId, startDate)

View File

@ -36,14 +36,14 @@ class ExamRepository @Inject constructor(
start: LocalDate,
end: LocalDate,
forceRefresh: Boolean,
notify: Boolean = false
notify: Boolean = false,
) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = {
val isShouldBeRefreshed = refreshHelper.isShouldBeRefreshed(
val isExpired = refreshHelper.shouldBeRefreshed(
key = getRefreshKey(cacheKey, semester, start, end)
)
it.isEmpty() || forceRefresh || isShouldBeRefreshed
it.isEmpty() || forceRefresh || isExpired
},
query = {
examDb.loadAll(

View File

@ -37,13 +37,12 @@ class GradeRepository @Inject constructor(
student: Student,
semester: Semester,
forceRefresh: Boolean,
notify: Boolean = false
notify: Boolean = false,
) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { (details, summaries) ->
val isShouldBeRefreshed =
refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester))
details.isEmpty() || summaries.isEmpty() || forceRefresh || isShouldBeRefreshed
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester))
details.isEmpty() || summaries.isEmpty() || forceRefresh || isExpired
},
query = {
val detailsFlow = gradeDb.loadAll(semester.semesterId, semester.studentId)
@ -71,8 +70,8 @@ class GradeRepository @Inject constructor(
newDetails: List<Grade>,
notify: Boolean
) {
val notifyBreakDate =
oldGrades.maxByOrNull { it.date }?.date ?: student.registrationDate.toLocalDate()
val notifyBreakDate = oldGrades.maxByOrNull {it.date }
?.date ?: student.registrationDate.toLocalDate()
gradeDb.deleteAll(oldGrades uniqueSubtract newDetails)
gradeDb.insertAll((newDetails uniqueSubtract oldGrades).onEach {
if (it.date >= notifyBreakDate) it.apply {
@ -89,8 +88,7 @@ class GradeRepository @Inject constructor(
) {
gradeSummaryDb.deleteAll(oldSummaries uniqueSubtract newSummary)
gradeSummaryDb.insertAll((newSummary uniqueSubtract oldSummaries).onEach { summary ->
val oldSummary =
oldSummaries.find { oldSummary -> oldSummary.subject == summary.subject }
val oldSummary = oldSummaries.find { old -> old.subject == summary.subject }
summary.isPredictedGradeNotified = when {
summary.predictedGrade.isEmpty() -> true
notify && oldSummary?.predictedGrade != summary.predictedGrade -> false

View File

@ -39,9 +39,19 @@ class GradeStatisticsRepository @Inject constructor(
private val semesterCacheKey = "grade_stats_semester"
private val pointsCacheKey = "grade_stats_points"
fun getGradesPartialStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource(
fun getGradesPartialStatistics(
student: Student,
semester: Semester,
subjectName: String,
forceRefresh: Boolean,
) = networkBoundResource(
mutex = partialMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(partialCacheKey, semester)) },
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(
key = getRefreshKey(partialCacheKey, semester)
)
it.isEmpty() || forceRefresh || isExpired
},
query = { gradePartialStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
@ -76,9 +86,19 @@ class GradeStatisticsRepository @Inject constructor(
}
)
fun getGradesSemesterStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource(
fun getGradesSemesterStatistics(
student: Student,
semester: Semester,
subjectName: String,
forceRefresh: Boolean,
) = networkBoundResource(
mutex = semesterMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(semesterCacheKey, semester)) },
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(
key = getRefreshKey(semesterCacheKey, semester)
)
it.isEmpty() || forceRefresh || isExpired
},
query = { gradeSemesterStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
@ -94,10 +114,12 @@ class GradeStatisticsRepository @Inject constructor(
val itemsWithAverage = items.map { item ->
item.copy().apply {
val denominator = item.amounts.sum()
average = if (denominator == 0) "" else (item.amounts.mapIndexed { gradeValue, amount ->
(gradeValue + 1) * amount
}.sum().toDouble() / denominator).let {
"%.2f".format(Locale.FRANCE, it)
average = if (denominator == 0) "" else {
(item.amounts.mapIndexed { gradeValue, amount ->
(gradeValue + 1) * amount
}.sum().toDouble() / denominator).let {
"%.2f".format(Locale.FRANCE, it)
}
}
}
}
@ -109,7 +131,9 @@ class GradeStatisticsRepository @Inject constructor(
amounts = itemsWithAverage.map { it.amounts }.sumGradeAmounts(),
studentGrade = 0
).apply {
average = itemsWithAverage.mapNotNull { it.average.replace(",", ".").toDoubleOrNull() }.average().let {
average = itemsWithAverage.mapNotNull {
it.average.replace(",", ".").toDoubleOrNull()
}.average().let {
"%.2f".format(Locale.FRANCE, it)
}
}).reversed()
@ -118,9 +142,17 @@ class GradeStatisticsRepository @Inject constructor(
}
)
fun getGradesPointsStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource(
fun getGradesPointsStatistics(
student: Student,
semester: Semester,
subjectName: String,
forceRefresh: Boolean,
) = networkBoundResource(
mutex = pointsMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(pointsCacheKey, semester)) },
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(pointsCacheKey, semester))
it.isEmpty() || forceRefresh || isExpired
},
query = { gradePointsStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)

View File

@ -30,16 +30,19 @@ class HomeworkRepository @Inject constructor(
private val cacheKey = "homework"
fun getHomework(
student: Student, semester: Semester,
start: LocalDate, end: LocalDate,
forceRefresh: Boolean, notify: Boolean = false
student: Student,
semester: Semester,
start: LocalDate,
end: LocalDate,
forceRefresh: Boolean,
notify: Boolean = false,
) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = {
val isShouldBeRefreshed = refreshHelper.isShouldBeRefreshed(
val isExpired = refreshHelper.shouldBeRefreshed(
key = getRefreshKey(cacheKey, semester, start, end)
)
it.isEmpty() || forceRefresh || isShouldBeRefreshed
it.isEmpty() || forceRefresh || isExpired
},
query = {
homeworkDb.loadAll(

View File

@ -23,11 +23,17 @@ class LuckyNumberRepository @Inject constructor(
private val saveFetchResultMutex = Mutex()
fun getLuckyNumber(student: Student, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource(
fun getLuckyNumber(
student: Student,
forceRefresh: Boolean,
notify: Boolean = false,
) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it == null || forceRefresh },
query = { luckyNumberDb.load(student.studentId, now()) },
fetch = { sdk.init(student).getLuckyNumber(student.schoolShortName)?.mapToEntity(student) },
fetch = {
sdk.init(student).getLuckyNumber(student.schoolShortName)?.mapToEntity(student)
},
saveFetchResult = { old, new ->
if (new != old) {
old?.let { luckyNumberDb.deleteAll(listOfNotNull(it)) }
@ -41,9 +47,11 @@ class LuckyNumberRepository @Inject constructor(
fun getLuckyNumberHistory(student: Student, start: LocalDate, end: LocalDate) =
luckyNumberDb.getAll(student.studentId, start, end)
suspend fun getNotNotifiedLuckyNumber(student: Student) = luckyNumberDb.load(student.studentId, now()).map {
if (it?.isNotified == false) it else null
}.first()
suspend fun getNotNotifiedLuckyNumber(student: Student) =
luckyNumberDb.load(student.studentId, now()).map {
if (it?.isNotified == false) it else null
}.first()
suspend fun updateLuckyNumber(luckyNumber: LuckyNumber?) = luckyNumberDb.updateAll(listOfNotNull(luckyNumber))
suspend fun updateLuckyNumber(luckyNumber: LuckyNumber?) =
luckyNumberDb.updateAll(listOfNotNull(luckyNumber))
}

View File

@ -51,14 +51,18 @@ class MessageRepository @Inject constructor(
@Suppress("UNUSED_PARAMETER")
fun getMessages(
student: Student, semester: Semester,
folder: MessageFolder, forceRefresh: Boolean, notify: Boolean = false
student: Student,
semester: Semester,
folder: MessageFolder,
forceRefresh: Boolean,
notify: Boolean = false,
): Flow<Resource<List<Message>>> = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = {
it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(
getRefreshKey(cacheKey, student, folder)
val isExpired = refreshHelper.shouldBeRefreshed(
key = getRefreshKey(cacheKey, student, folder)
)
it.isEmpty() || forceRefresh || isExpired
},
query = { messagesDb.loadAll(student.id.toInt(), folder.id) },
fetch = {
@ -77,7 +81,8 @@ class MessageRepository @Inject constructor(
)
private fun getMessagesWithReadByChange(
old: List<Message>, new: List<Message>,
old: List<Message>,
new: List<Message>,
setNotified: Boolean
): List<Message> {
val oldMeta = old.map { Triple(it, it.readBy, it.unreadBy) }
@ -96,7 +101,9 @@ class MessageRepository @Inject constructor(
}
fun getMessage(
student: Student, message: Message, markAsRead: Boolean = false
student: Student,
message: Message,
markAsRead: Boolean = false,
): Flow<Resource<MessageWithAttachment?>> = networkBoundResource(
shouldFetch = {
checkNotNull(it, { "This message no longer exist!" })
@ -135,8 +142,10 @@ class MessageRepository @Inject constructor(
}
suspend fun sendMessage(
student: Student, subject: String, content: String,
recipients: List<Recipient>
student: Student,
subject: String,
content: String,
recipients: List<Recipient>,
): SentMessage = sdk.init(student).sendMessage(
subject = subject,
content = content,

View File

@ -28,9 +28,16 @@ class MobileDeviceRepository @Inject constructor(
private val cacheKey = "devices"
fun getDevices(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource(
fun getDevices(
student: Student,
semester: Semester,
forceRefresh: Boolean,
) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, student)) },
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student))
it.isEmpty() || forceRefresh || isExpired
},
query = { mobileDb.loadAll(student.userLoginId.takeIf { it != 0 } ?: student.studentId) },
fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)

View File

@ -12,7 +12,6 @@ import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@ -28,9 +27,19 @@ class NoteRepository @Inject constructor(
private val cacheKey = "note"
fun getNotes(student: Student, semester: Semester, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource(
fun getNotes(
student: Student,
semester: Semester,
forceRefresh: Boolean,
notify: Boolean = false,
) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) },
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(
getRefreshKey(cacheKey, semester)
)
it.isEmpty() || forceRefresh || isExpired
},
query = { noteDb.loadAll(student.studentId) },
fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)

View File

@ -0,0 +1,17 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.db.dao.NotificationDao
import io.github.wulkanowy.data.db.entities.Notification
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class NotificationRepository @Inject constructor(
private val notificationDao: NotificationDao,
) {
fun getNotifications(studentId: Long) = notificationDao.loadAll(studentId)
suspend fun saveNotification(notification: Notification) =
notificationDao.insertAll(listOf(notification))
}

View File

@ -14,7 +14,6 @@ import io.github.wulkanowy.sdk.toLocalDate
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import io.github.wulkanowy.ui.modules.grade.GradeAverageMode
import io.github.wulkanowy.ui.modules.grade.GradeSortingMode
import io.github.wulkanowy.utils.toTimestamp
import io.github.wulkanowy.utils.toLocalDateTime
import io.github.wulkanowy.utils.toTimestamp
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -108,6 +107,22 @@ class PreferencesRepository @Inject constructor(
R.bool.pref_default_notification_upcoming_lessons_enable
)
val isUpcomingLessonsNotificationsPersistentKey =
context.getString(R.string.pref_key_notifications_upcoming_lessons_persistent)
val isUpcomingLessonsNotificationsPersistent: Boolean
get() = getBoolean(
isUpcomingLessonsNotificationsPersistentKey,
R.bool.pref_default_notification_upcoming_lessons_persistent
)
val isNotificationPiggybackEnabledKey =
context.getString(R.string.pref_key_notifications_piggyback)
val isNotificationPiggybackEnabled: Boolean
get() = getBoolean(
R.string.pref_key_notifications_piggyback,
R.bool.pref_default_notification_piggyback
)
val isDebugNotificationEnableKey = context.getString(R.string.pref_key_notification_debug)
val isDebugNotificationEnable: Boolean
get() = getBoolean(isDebugNotificationEnableKey, R.bool.pref_default_notification_debug)
@ -176,10 +191,8 @@ class PreferencesRepository @Inject constructor(
)
var lasSyncDate: LocalDateTime
get() = getLong(
R.string.pref_key_last_sync_date,
R.string.pref_default_last_sync_date
).toLocalDateTime()
get() = getLong(R.string.pref_key_last_sync_date, R.string.pref_default_last_sync_date)
.toLocalDateTime()
set(value) = sharedPref.edit().putLong("last_sync_date", value.toTimestamp()).apply()
var dashboardItemsPosition: Map<DashboardItem.Type, Int>?
@ -230,8 +243,10 @@ class PreferencesRepository @Inject constructor(
set(value) = sharedPref.edit().putInt(PREF_KEY_IN_APP_REVIEW_COUNT, value).apply()
var inAppReviewDate: LocalDate?
get() = sharedPref.getLong(PREF_KEY_IN_APP_REVIEW_DATE, 0).takeIf { it != 0L }?.toLocalDate()
set(value) = sharedPref.edit().putLong(PREF_KEY_IN_APP_REVIEW_DATE, value!!.toTimestamp()).apply()
get() = sharedPref.getLong(PREF_KEY_IN_APP_REVIEW_DATE, 0).takeIf { it != 0L }
?.toLocalDate()
set(value) = sharedPref.edit().putLong(PREF_KEY_IN_APP_REVIEW_DATE, value!!.toTimestamp())
.apply()
var isAppReviewDone: Boolean
get() = sharedPref.getBoolean(PREF_KEY_IN_APP_REVIEW_DONE, false)

View File

@ -7,6 +7,8 @@ import io.github.wulkanowy.data.db.entities.ReportingUnit
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.uniqueSubtract
import javax.inject.Inject
@ -15,26 +17,34 @@ import javax.inject.Singleton
@Singleton
class RecipientRepository @Inject constructor(
private val recipientDb: RecipientDao,
private val sdk: Sdk
private val sdk: Sdk,
private val refreshHelper: AutoRefreshHelper,
) {
private val cacheKey = "recipient"
suspend fun refreshRecipients(student: Student, unit: ReportingUnit, role: Int) {
val new = sdk.init(student).getRecipients(unit.unitId, role).mapToEntities(unit.studentId)
val old = recipientDb.loadAll(unit.studentId, unit.unitId, role)
recipientDb.deleteAll(old uniqueSubtract new)
recipientDb.insertAll(new uniqueSubtract old)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
}
suspend fun getRecipients(student: Student, unit: ReportingUnit, role: Int): List<Recipient> {
return recipientDb.loadAll(unit.studentId, unit.unitId, role).ifEmpty {
refreshRecipients(student, unit, role)
val cached = recipientDb.loadAll(unit.studentId, unit.unitId, role)
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student))
return if (cached.isEmpty() || isExpired) {
refreshRecipients(student, unit, role)
recipientDb.loadAll(unit.studentId, unit.unitId, role)
}
} else cached
}
suspend fun getMessageRecipients(student: Student, message: Message): List<Recipient> {
return sdk.init(student).getMessageRecipients(message.messageId, message.senderId).mapToEntities(student.studentId)
return sdk.init(student).getMessageRecipients(message.messageId, message.senderId)
.mapToEntities(student.studentId)
}
}

View File

@ -11,7 +11,7 @@ class RecoverRepository @Inject constructor(private val sdk: Sdk) {
return sdk.getPasswordResetCaptchaCode(host, symbol)
}
suspend fun sendRecoverRequest(url: String, symbol: String, email: String, reCaptchaResponse: String): String {
return sdk.sendPasswordResetRequest(url, symbol, email, reCaptchaResponse)
}
suspend fun sendRecoverRequest(
url: String, symbol: String, email: String, reCaptchaResponse: String
): String = sdk.sendPasswordResetRequest(url, symbol, email, reCaptchaResponse)
}

View File

@ -2,7 +2,6 @@ package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.db.dao.SchoolAnnouncementDao
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.sdk.Sdk
@ -12,7 +11,6 @@ import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@ -30,17 +28,15 @@ class SchoolAnnouncementRepository @Inject constructor(
fun getSchoolAnnouncements(
student: Student,
forceRefresh: Boolean,
notify: Boolean = false
forceRefresh: Boolean, notify: Boolean = false
) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = {
it.isEmpty() || forceRefresh
|| refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, student))
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student))
it.isEmpty() || forceRefresh || isExpired
},
query = {
schoolAnnouncementDb.loadAll(
student.studentId)
schoolAnnouncementDb.loadAll(student.studentId)
},
fetch = {
sdk.init(student)
@ -57,9 +53,11 @@ class SchoolAnnouncementRepository @Inject constructor(
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
}
)
fun getSchoolAnnouncementFromDatabase(student: Student): Flow<List<SchoolAnnouncement>> {
return schoolAnnouncementDb.loadAll(student.studentId)
}
suspend fun updateSchoolAnnouncement(schoolAnnouncement: List<SchoolAnnouncement>) = schoolAnnouncementDb.updateAll(schoolAnnouncement)
suspend fun updateSchoolAnnouncement(schoolAnnouncement: List<SchoolAnnouncement>) =
schoolAnnouncementDb.updateAll(schoolAnnouncement)
}

View File

@ -5,6 +5,8 @@ import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntity
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import kotlinx.coroutines.sync.Mutex
@ -14,29 +16,41 @@ import javax.inject.Singleton
@Singleton
class SchoolRepository @Inject constructor(
private val schoolDb: SchoolDao,
private val sdk: Sdk
private val sdk: Sdk,
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
fun getSchoolInfo(student: Student, semester: Semester, forceRefresh: Boolean) =
networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it == null || forceRefresh },
query = { schoolDb.load(semester.studentId, semester.classId) },
fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear).getSchool()
.mapToEntity(semester)
},
saveFetchResult = { old, new ->
if (old != null && new != old) {
with(schoolDb) {
deleteAll(listOf(old))
insertAll(listOf(new))
}
} else if (old == null) {
schoolDb.insertAll(listOf(new))
private val cacheKey = "school_info"
fun getSchoolInfo(
student: Student,
semester: Semester,
forceRefresh: Boolean,
) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(
key = getRefreshKey(cacheKey, student)
)
it == null || forceRefresh || isExpired
},
query = { schoolDb.load(semester.studentId, semester.classId) },
fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear).getSchool()
.mapToEntity(semester)
},
saveFetchResult = { old, new ->
if (old != null && new != old) {
with(schoolDb) {
deleteAll(listOf(old))
insertAll(listOf(new))
}
} else if (old == null) {
schoolDb.insertAll(listOf(new))
}
)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
}
)
}

View File

@ -19,24 +19,27 @@ class StudentInfoRepository @Inject constructor(
private val saveFetchResultMutex = Mutex()
fun getStudentInfo(student: Student, semester: Semester, forceRefresh: Boolean) =
networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it == null || forceRefresh },
query = { studentInfoDao.loadStudentInfo(student.studentId) },
fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
.getStudentInfo().mapToEntity(semester)
},
saveFetchResult = { old, new ->
if (old != null && new != old) {
with(studentInfoDao) {
deleteAll(listOf(old))
insertAll(listOf(new))
}
} else if (old == null) {
studentInfoDao.insertAll(listOf(new))
fun getStudentInfo(
student: Student,
semester: Semester,
forceRefresh: Boolean,
) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it == null || forceRefresh },
query = { studentInfoDao.loadStudentInfo(student.studentId) },
fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
.getStudentInfo().mapToEntity(semester)
},
saveFetchResult = { old, new ->
if (old != null && new != old) {
with(studentInfoDao) {
deleteAll(listOf(old))
insertAll(listOf(new))
}
} else if (old == null) {
studentInfoDao.insertAll(listOf(new))
}
)
}
)
}

View File

@ -5,6 +5,8 @@ import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract
@ -15,14 +17,24 @@ import javax.inject.Singleton
@Singleton
class SubjectRepository @Inject constructor(
private val subjectDao: SubjectDao,
private val sdk: Sdk
private val sdk: Sdk,
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
fun getSubjects(student: Student, semester: Semester, forceRefresh: Boolean = false) = networkBoundResource(
private val cacheKey = "subjects"
fun getSubjects(
student: Student,
semester: Semester,
forceRefresh: Boolean = false,
) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh },
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester))
it.isEmpty() || forceRefresh || isExpired
},
query = { subjectDao.loadAll(semester.diaryId, semester.studentId) },
fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
@ -31,6 +43,8 @@ class SubjectRepository @Inject constructor(
saveFetchResult = { old, new ->
subjectDao.deleteAll(old uniqueSubtract new)
subjectDao.insertAll(new uniqueSubtract old)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
}
)
}

View File

@ -5,6 +5,8 @@ import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract
@ -15,14 +17,24 @@ import javax.inject.Singleton
@Singleton
class TeacherRepository @Inject constructor(
private val teacherDb: TeacherDao,
private val sdk: Sdk
private val sdk: Sdk,
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
fun getTeachers(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource(
private val cacheKey = "teachers"
fun getTeachers(
student: Student,
semester: Semester,
forceRefresh: Boolean,
) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh },
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester))
it.isEmpty() || forceRefresh || isExpired
},
query = { teacherDb.loadAll(semester.studentId, semester.classId) },
fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
@ -32,6 +44,8 @@ class TeacherRepository @Inject constructor(
saveFetchResult = { old, new ->
teacherDb.deleteAll(old uniqueSubtract new)
teacherDb.insertAll(new uniqueSubtract old)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
}
)
}

View File

@ -41,18 +41,22 @@ class TimetableRepository @Inject constructor(
private val cacheKey = "timetable"
fun getTimetable(
student: Student, semester: Semester, start: LocalDate, end: LocalDate,
forceRefresh: Boolean, refreshAdditional: Boolean = false
student: Student,
semester: Semester,
start: LocalDate,
end: LocalDate,
forceRefresh: Boolean,
refreshAdditional: Boolean = false,
) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { (timetable, additional, headers) ->
val refreshKey = getRefreshKey(cacheKey, semester, start, end)
val isShouldRefresh = refreshHelper.isShouldBeRefreshed(refreshKey)
val isExpired = refreshHelper.shouldBeRefreshed(refreshKey)
val isRefreshAdditional = additional.isEmpty() && refreshAdditional
val isNoData = timetable.isEmpty() || isRefreshAdditional || headers.isEmpty()
isNoData || forceRefresh || isShouldRefresh
isNoData || forceRefresh || isExpired
},
query = { getFullTimetableFromDatabase(student, semester, start, end) },
fetch = {
@ -79,8 +83,10 @@ class TimetableRepository @Inject constructor(
)
private fun getFullTimetableFromDatabase(
student: Student, semester: Semester,
start: LocalDate, end: LocalDate
student: Student,
semester: Semester,
start: LocalDate,
end: LocalDate,
): Flow<TimetableFull> {
val timetableFlow = timetableDb.loadAll(
diaryId = semester.diaryId,
@ -113,20 +119,11 @@ class TimetableRepository @Inject constructor(
private suspend fun refreshTimetable(
student: Student,
lessonsOld: List<Timetable>, lessonsNew: List<Timetable>
lessonsOld: List<Timetable>,
lessonsNew: List<Timetable>,
) {
val lessonsToRemove = lessonsOld uniqueSubtract lessonsNew
val lessonsToAdd = (lessonsNew uniqueSubtract lessonsOld).map { new ->
val matchingOld = lessonsOld.singleOrNull { new.start == it.start }
if (matchingOld != null) {
val useOldTeacher = new.teacher.isEmpty() && !new.changes && !matchingOld.changes
new.copy(
room = if (new.room.isEmpty()) matchingOld.room else new.room,
teacher = if (useOldTeacher) matchingOld.teacher
else new.teacher
)
} else new
}
val lessonsToAdd = lessonsNew uniqueSubtract lessonsOld
timetableDb.deleteAll(lessonsToRemove)
timetableDb.insertAll(lessonsToAdd)

View File

@ -11,6 +11,7 @@ import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.services.HiltBroadcastReceiver
import io.github.wulkanowy.services.sync.channels.UpcomingLessonsChannel.Companion.CHANNEL_ID
@ -32,6 +33,9 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() {
@Inject
lateinit var studentRepository: StudentRepository
@Inject
lateinit var preferencesRepository: PreferencesRepository
companion object {
const val NOTIFICATION_TYPE_CURRENT = 1
const val NOTIFICATION_TYPE_UPCOMING = 2
@ -68,6 +72,7 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() {
private fun prepareNotification(context: Context, intent: Intent) {
val type = intent.getIntExtra(LESSON_TYPE, 0)
val notificationId = intent.getIntExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id)
val isPersistent = preferencesRepository.isUpcomingLessonsNotificationsPersistent
if (type == NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION) {
return NotificationManagerCompat.from(context).cancel(notificationId)
@ -87,33 +92,57 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() {
Timber.d("TimetableNotification receive: type: $type, subject: $subject, start: ${start.toLocalDateTime()}, student: $studentId")
showNotification(context, notificationId, studentName,
showNotification(context, notificationId, isPersistent, studentName,
if (type == NOTIFICATION_TYPE_CURRENT) end else start, end - start,
context.getString(if (type == NOTIFICATION_TYPE_CURRENT) R.string.timetable_now else R.string.timetable_next, "($room) $subject".removePrefix("()")),
nextSubject?.let { context.getString(R.string.timetable_later, "($nextRoom) $nextSubject".removePrefix("()")) }
context.getString(
if (type == NOTIFICATION_TYPE_CURRENT) R.string.timetable_now else R.string.timetable_next,
"($room) $subject".removePrefix("()")
),
nextSubject?.let {
context.getString(
R.string.timetable_later,
"($nextRoom) $nextSubject".removePrefix("()")
)
}
)
}
private fun showNotification(context: Context, notificationId: Int, studentName: String?, countDown: Long, timeout: Long, title: String, next: String?) {
NotificationManagerCompat.from(context).notify(notificationId, NotificationCompat.Builder(context, CHANNEL_ID)
.setContentTitle(title)
.setContentText(next)
.setAutoCancel(false)
.setOngoing(true)
.setWhen(countDown)
.apply {
if (Build.VERSION.SDK_INT >= N) setUsesChronometer(true)
}
.setTimeoutAfter(timeout)
.setSmallIcon(R.drawable.ic_stat_timetable)
.setColor(context.getCompatColor(R.color.colorPrimary))
.setStyle(NotificationCompat.InboxStyle().also {
it.setSummaryText(studentName)
it.addLine(next)
})
.setContentIntent(PendingIntent.getActivity(context, MainView.Section.TIMETABLE.id,
MainActivity.getStartIntent(context, MainView.Section.TIMETABLE, true), FLAG_UPDATE_CURRENT))
.build()
)
private fun showNotification(
context: Context,
notificationId: Int,
isPersistent: Boolean,
studentName: String?,
countDown: Long,
timeout: Long,
title: String,
next: String?
) {
NotificationManagerCompat.from(context)
.notify(notificationId, NotificationCompat.Builder(context, CHANNEL_ID)
.setContentTitle(title)
.setContentText(next)
.setAutoCancel(false)
.setWhen(countDown)
.setOngoing(isPersistent)
.apply {
if (Build.VERSION.SDK_INT >= N) setUsesChronometer(true)
}
.setTimeoutAfter(timeout)
.setSmallIcon(R.drawable.ic_stat_timetable)
.setColor(context.getCompatColor(R.color.colorPrimary))
.setStyle(NotificationCompat.InboxStyle().also {
it.setSummaryText(studentName)
it.addLine(next)
})
.setContentIntent(
PendingIntent.getActivity(
context,
MainView.Section.TIMETABLE.id,
MainActivity.getStartIntent(context, MainView.Section.TIMETABLE, true),
FLAG_UPDATE_CURRENT
)
)
.build()
)
}
}

View File

@ -31,6 +31,7 @@ import io.github.wulkanowy.utils.nickOrName
import io.github.wulkanowy.utils.toTimestamp
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalDateTime.now
import javax.inject.Inject
@ -57,10 +58,13 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
lessons.sortedBy { it.start }.forEachIndexed { index, lesson ->
val upcomingTime = getUpcomingLessonTime(index, lessons, lesson)
cancelScheduledTo(
upcomingTime..lesson.start,
getRequestCode(upcomingTime, studentId)
range = upcomingTime..lesson.start,
requestCode = getRequestCode(upcomingTime, studentId)
)
cancelScheduledTo(
range = lesson.start..lesson.end,
requestCode = getRequestCode(lesson.start, studentId)
)
cancelScheduledTo(lesson.start..lesson.end, getRequestCode(lesson.start, studentId))
Timber.d("TimetableNotification canceled: type 1 & 2, subject: ${lesson.subject}, start: ${lesson.start}, student: $studentId")
}
@ -82,6 +86,11 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
return cancelScheduled(lessons, student)
}
if (lessons.firstOrNull()?.date?.isAfter(LocalDate.now().plusDays(2)) == true) {
Timber.d("Timetable notification scheduling skipped - lessons are too far")
return
}
withContext(dispatchersProvider.backgroundThread) {
lessons.groupBy { it.date }
.map { it.value.sortedBy { lesson -> lesson.start } }
@ -96,26 +105,26 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
if (lesson.start > now()) {
scheduleBroadcast(
intent,
student.studentId,
NOTIFICATION_TYPE_UPCOMING,
getUpcomingLessonTime(index, active, lesson)
intent = intent,
studentId = student.studentId,
notificationType = NOTIFICATION_TYPE_UPCOMING,
time = getUpcomingLessonTime(index, active, lesson)
)
}
if (lesson.end > now()) {
scheduleBroadcast(
intent,
student.studentId,
NOTIFICATION_TYPE_CURRENT,
lesson.start
intent = intent,
studentId = student.studentId,
notificationType = NOTIFICATION_TYPE_CURRENT,
time = lesson.start
)
if (active.lastIndex == index) {
scheduleBroadcast(
intent,
student.studentId,
NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION,
lesson.end
intent = intent,
studentId = student.studentId,
notificationType = NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION,
time = lesson.end
)
}
}
@ -143,17 +152,21 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
notificationType: Int,
time: LocalDateTime
) {
AlarmManagerCompat.setExactAndAllowWhileIdle(
alarmManager, RTC_WAKEUP, time.toTimestamp(),
PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also {
it.putExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id)
it.putExtra(LESSON_TYPE, notificationType)
}, FLAG_UPDATE_CURRENT)
)
Timber.d(
"TimetableNotification scheduled: type: $notificationType, subject: ${
intent.getStringExtra(LESSON_TITLE)
}, start: $time, student: $studentId"
)
try {
AlarmManagerCompat.setExactAndAllowWhileIdle(
alarmManager, RTC_WAKEUP, time.toTimestamp(),
PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also {
it.putExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id)
it.putExtra(LESSON_TYPE, notificationType)
}, FLAG_UPDATE_CURRENT)
)
Timber.d(
"TimetableNotification scheduled: type: $notificationType, subject: ${
intent.getStringExtra(LESSON_TITLE)
}, start: $time, student: $studentId"
)
} catch (e: IllegalStateException) {
Timber.e(e)
}
}
}

View File

@ -0,0 +1,24 @@
package io.github.wulkanowy.services.piggyback
import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.services.sync.SyncManager
import javax.inject.Inject
@AndroidEntryPoint
class VulcanNotificationListenerService : NotificationListenerService() {
@Inject
lateinit var syncManager: SyncManager
@Inject
lateinit var preferenceRepository: PreferencesRepository
override fun onNotificationPosted(statusBarNotification: StatusBarNotification?) {
if (statusBarNotification?.packageName == "pl.edu.vulcan.hebe" && preferenceRepository.isNotificationPiggybackEnabled) {
syncManager.startOneTimeSyncWorker()
}
}
}

View File

@ -57,14 +57,20 @@ class SyncManager @Inject constructor(
fun startPeriodicSyncWorker(restart: Boolean = false) {
if (preferencesRepository.isServiceEnabled && !now().isHolidays) {
workManager.enqueueUniquePeriodicWork(SyncWorker::class.java.simpleName, if (restart) REPLACE else KEEP,
PeriodicWorkRequestBuilder<SyncWorker>(preferencesRepository.servicesInterval, MINUTES)
val serviceInterval = preferencesRepository.servicesInterval
workManager.enqueueUniquePeriodicWork(
SyncWorker::class.java.simpleName, if (restart) REPLACE else KEEP,
PeriodicWorkRequestBuilder<SyncWorker>(serviceInterval, MINUTES)
.setInitialDelay(10, MINUTES)
.setBackoffCriteria(EXPONENTIAL, 30, MINUTES)
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(if (preferencesRepository.isServicesOnlyWifi) UNMETERED else CONNECTED)
.build())
.build())
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(if (preferencesRepository.isServicesOnlyWifi) UNMETERED else CONNECTED)
.build()
)
.build()
)
}
}
@ -77,7 +83,11 @@ class SyncManager @Inject constructor(
)
.build()
workManager.enqueueUniqueWork("${SyncWorker::class.java.simpleName}_one_time", ExistingWorkPolicy.REPLACE, work)
workManager.enqueueUniqueWork(
"${SyncWorker::class.java.simpleName}_one_time",
ExistingWorkPolicy.REPLACE,
work
)
return workManager.getWorkInfoByIdLiveData(work.id).asFlow()
}

View File

@ -0,0 +1,145 @@
package io.github.wulkanowy.services.sync.notifications
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Notification
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.MultipleNotificationsData
import io.github.wulkanowy.data.pojos.NotificationData
import io.github.wulkanowy.data.pojos.OneNotificationData
import io.github.wulkanowy.data.repositories.NotificationRepository
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.getCompatBitmap
import io.github.wulkanowy.utils.getCompatColor
import io.github.wulkanowy.utils.nickOrName
import java.time.LocalDateTime
import javax.inject.Inject
import kotlin.random.Random
class AppNotificationManager @Inject constructor(
private val notificationManager: NotificationManagerCompat,
@ApplicationContext private val context: Context,
private val appInfo: AppInfo,
private val notificationRepository: NotificationRepository
) {
suspend fun sendNotification(notificationData: NotificationData, student: Student) =
when (notificationData) {
is OneNotificationData -> sendOneNotification(notificationData, student)
is MultipleNotificationsData -> sendMultipleNotifications(notificationData, student)
}
private suspend fun sendOneNotification(
notificationData: OneNotificationData,
student: Student
) {
val content = context.getString(
notificationData.contentStringRes,
*notificationData.contentValues.toTypedArray()
)
val title = context.getString(notificationData.titleStringRes)
val notification = getDefaultNotificationBuilder(notificationData)
.setContentTitle(title)
.setContentText(content)
.setStyle(
NotificationCompat.BigTextStyle()
.setSummaryText(student.nickOrName)
.bigText(content)
)
.build()
notificationManager.notify(Random.nextInt(Int.MAX_VALUE), notification)
saveNotification(title, content, notificationData, student)
}
private suspend fun sendMultipleNotifications(
notificationData: MultipleNotificationsData,
student: Student
) {
val groupType = notificationData.type.group ?: return
val group = "${groupType}_${student.id}"
val groupId = student.id * 100 + notificationData.type.ordinal
notificationData.lines.forEach { item ->
val title = context.resources.getQuantityString(notificationData.titleStringRes, 1)
val notification = getDefaultNotificationBuilder(notificationData)
.setContentTitle(title)
.setContentText(item)
.setStyle(
NotificationCompat.BigTextStyle()
.setSummaryText(student.nickOrName)
.bigText(item)
)
.setGroup(group)
.build()
notificationManager.notify(Random.nextInt(Int.MAX_VALUE), notification)
saveNotification(title, item, notificationData, student)
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return
val summaryNotification = getDefaultNotificationBuilder(notificationData)
.setSmallIcon(notificationData.icon)
.setGroup(group)
.setStyle(NotificationCompat.InboxStyle().setSummaryText(student.nickOrName))
.setGroupSummary(true)
.build()
notificationManager.notify(groupId.toInt(), summaryNotification)
}
@SuppressLint("InlinedApi")
private fun getDefaultNotificationBuilder(notificationData: NotificationData): NotificationCompat.Builder {
val pendingIntentsFlags = if (appInfo.systemVersion >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
return NotificationCompat.Builder(context, notificationData.type.channel)
.setLargeIcon(context.getCompatBitmap(notificationData.icon, R.color.colorPrimary))
.setSmallIcon(R.drawable.ic_stat_all)
.setAutoCancel(true)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setColor(context.getCompatColor(R.color.colorPrimary))
.setContentIntent(
PendingIntent.getActivity(
context,
notificationData.startMenu.id,
MainActivity.getStartIntent(context, notificationData.startMenu, true),
pendingIntentsFlags
)
)
}
private suspend fun saveNotification(
title: String,
content: String,
notificationData: NotificationData,
student: Student
) {
val notificationEntity = Notification(
studentId = student.id,
title = title,
content = content,
type = notificationData.type,
date = LocalDateTime.now()
)
notificationRepository.saveNotification(notificationEntity)
}
}

View File

@ -1,102 +0,0 @@
package io.github.wulkanowy.services.sync.notifications
import android.app.PendingIntent
import android.content.Context
import android.os.Build
import androidx.annotation.PluralsRes
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.data.pojos.Notification
import io.github.wulkanowy.data.pojos.OneNotification
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.utils.getCompatBitmap
import io.github.wulkanowy.utils.getCompatColor
import io.github.wulkanowy.utils.nickOrName
import kotlin.random.Random
abstract class BaseNotification(
private val context: Context,
private val notificationManager: NotificationManagerCompat,
) {
protected fun sendNotification(notification: Notification, student: Student) =
when (notification) {
is OneNotification -> sendOneNotification(notification, student)
is MultipleNotifications -> sendMultipleNotifications(notification, student)
}
private fun sendOneNotification(notification: OneNotification, student: Student?) {
notificationManager.notify(
Random.nextInt(Int.MAX_VALUE),
getNotificationBuilder(notification).apply {
val content = context.getString(
notification.contentStringRes,
*notification.contentValues.toTypedArray()
)
setContentTitle(context.getString(notification.titleStringRes))
setContentText(content)
setStyle(
NotificationCompat.BigTextStyle()
.setSummaryText(student?.nickOrName)
.bigText(content)
)
}.build()
)
}
private fun sendMultipleNotifications(notification: MultipleNotifications, student: Student) {
val group = notification.type.group + student.id
val groupId = student.id * 100 + notification.type.ordinal
notification.lines.forEach { item ->
notificationManager.notify(
Random.nextInt(Int.MAX_VALUE),
getNotificationBuilder(notification).apply {
setContentTitle(getQuantityString(notification.titleStringRes, 1))
setContentText(item)
setStyle(
NotificationCompat.BigTextStyle()
.setSummaryText(student.nickOrName)
.bigText(item)
)
setGroup(group)
}.build()
)
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return
notificationManager.notify(
groupId.toInt(),
getNotificationBuilder(notification).apply {
setSmallIcon(notification.icon)
setGroup(group)
setStyle(NotificationCompat.InboxStyle().setSummaryText(student.nickOrName))
setGroupSummary(true)
}.build()
)
}
private fun getNotificationBuilder(notification: Notification) = NotificationCompat
.Builder(context, notification.type.channel)
.setLargeIcon(context.getCompatBitmap(notification.icon, R.color.colorPrimary))
.setSmallIcon(R.drawable.ic_stat_all)
.setAutoCancel(true)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setColor(context.getCompatColor(R.color.colorPrimary))
.setContentIntent(
PendingIntent.getActivity(
context, notification.startMenu.id,
MainActivity.getStartIntent(context, notification.startMenu, true),
PendingIntent.FLAG_UPDATE_CURRENT
)
)
private fun getQuantityString(@PluralsRes id: Int, value: Int): String {
return context.resources.getQuantityString(id, value, value)
}
}

View File

@ -1,29 +1,25 @@
package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Conference
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.data.pojos.MultipleNotificationsData
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.toFormattedString
import java.time.LocalDateTime
import javax.inject.Inject
class NewConferenceNotification @Inject constructor(
@ApplicationContext private val context: Context,
notificationManager: NotificationManagerCompat,
) : BaseNotification(context, notificationManager) {
private val appNotificationManager: AppNotificationManager
) {
fun notify(items: List<Conference>, student: Student) {
suspend fun notify(items: List<Conference>, student: Student) {
val today = LocalDateTime.now()
val lines = items.filter { !it.date.isBefore(today) }.map {
"${it.date.toFormattedString("dd.MM")} - ${it.title}: ${it.subject}"
}.ifEmpty { return }
val notification = MultipleNotifications(
val notification = MultipleNotificationsData(
type = NotificationType.NEW_CONFERENCE,
icon = R.drawable.ic_more_conferences,
titleStringRes = R.plurals.conference_notify_new_item_title,
@ -33,6 +29,6 @@ class NewConferenceNotification @Inject constructor(
lines = lines
)
sendNotification(notification, student)
appNotificationManager.sendNotification(notification, student)
}
}

View File

@ -1,29 +1,25 @@
package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.data.pojos.MultipleNotificationsData
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.toFormattedString
import java.time.LocalDate
import javax.inject.Inject
class NewExamNotification @Inject constructor(
@ApplicationContext private val context: Context,
notificationManager: NotificationManagerCompat,
) : BaseNotification(context, notificationManager) {
private val appNotificationManager: AppNotificationManager
) {
fun notify(items: List<Exam>, student: Student) {
suspend fun notify(items: List<Exam>, student: Student) {
val today = LocalDate.now()
val lines = items.filter { !it.date.isBefore(today) }.map {
"${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.description}"
}.ifEmpty { return }
val notification = MultipleNotifications(
val notification = MultipleNotificationsData(
type = NotificationType.NEW_EXAM,
icon = R.drawable.ic_main_exam,
titleStringRes = R.plurals.exam_notify_new_item_title,
@ -33,6 +29,6 @@ class NewExamNotification @Inject constructor(
lines = lines
)
sendNotification(notification, student)
appNotificationManager.sendNotification(notification, student)
}
}

View File

@ -1,23 +1,19 @@
package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
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.Student
import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.data.pojos.MultipleNotificationsData
import io.github.wulkanowy.ui.modules.main.MainView
import javax.inject.Inject
class NewGradeNotification @Inject constructor(
@ApplicationContext private val context: Context,
notificationManager: NotificationManagerCompat,
) : BaseNotification(context, notificationManager) {
private val appNotificationManager: AppNotificationManager
) {
fun notifyDetails(items: List<Grade>, student: Student) {
val notification = MultipleNotifications(
suspend fun notifyDetails(items: List<Grade>, student: Student) {
val notification = MultipleNotificationsData(
type = NotificationType.NEW_GRADE_DETAILS,
icon = R.drawable.ic_stat_grade,
titleStringRes = R.plurals.grade_new_items,
@ -29,11 +25,11 @@ class NewGradeNotification @Inject constructor(
}
)
sendNotification(notification, student)
appNotificationManager.sendNotification(notification, student)
}
fun notifyPredicted(items: List<GradeSummary>, student: Student) {
val notification = MultipleNotifications(
suspend fun notifyPredicted(items: List<GradeSummary>, student: Student) {
val notification = MultipleNotificationsData(
type = NotificationType.NEW_GRADE_PREDICTED,
icon = R.drawable.ic_stat_grade,
titleStringRes = R.plurals.grade_new_items_predicted,
@ -45,11 +41,11 @@ class NewGradeNotification @Inject constructor(
}
)
sendNotification(notification, student)
appNotificationManager.sendNotification(notification, student)
}
fun notifyFinal(items: List<GradeSummary>, student: Student) {
val notification = MultipleNotifications(
suspend fun notifyFinal(items: List<GradeSummary>, student: Student) {
val notification = MultipleNotificationsData(
type = NotificationType.NEW_GRADE_FINAL,
icon = R.drawable.ic_stat_grade,
titleStringRes = R.plurals.grade_new_items_final,
@ -61,6 +57,6 @@ class NewGradeNotification @Inject constructor(
}
)
sendNotification(notification, student)
appNotificationManager.sendNotification(notification, student)
}
}

View File

@ -1,29 +1,25 @@
package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Homework
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.data.pojos.MultipleNotificationsData
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.toFormattedString
import java.time.LocalDate
import javax.inject.Inject
class NewHomeworkNotification @Inject constructor(
@ApplicationContext private val context: Context,
notificationManager: NotificationManagerCompat,
) : BaseNotification(context, notificationManager) {
private val appNotificationManager: AppNotificationManager
) {
fun notify(items: List<Homework>, student: Student) {
suspend fun notify(items: List<Homework>, student: Student) {
val today = LocalDate.now()
val lines = items.filter { !it.date.isBefore(today) }.map {
"${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.content}"
}.ifEmpty { return }
val notification = MultipleNotifications(
val notification = MultipleNotificationsData(
type = NotificationType.NEW_HOMEWORK,
icon = R.drawable.ic_more_homework,
titleStringRes = R.plurals.homework_notify_new_item_title,
@ -33,6 +29,6 @@ class NewHomeworkNotification @Inject constructor(
lines = lines
)
sendNotification(notification, student)
appNotificationManager.sendNotification(notification, student)
}
}

View File

@ -1,30 +1,26 @@
package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.OneNotification
import io.github.wulkanowy.data.pojos.OneNotificationData
import io.github.wulkanowy.ui.modules.main.MainView
import javax.inject.Inject
class NewLuckyNumberNotification @Inject constructor(
@ApplicationContext private val context: Context,
notificationManager: NotificationManagerCompat,
) : BaseNotification(context, notificationManager) {
private val appNotificationManager: AppNotificationManager
) {
fun notify(item: LuckyNumber, student: Student) {
val notification = OneNotification(
type = NotificationType.NEW_LUCKY_NUMBER,
icon = R.drawable.ic_stat_luckynumber,
titleStringRes = R.string.lucky_number_notify_new_item_title,
contentStringRes = R.string.lucky_number_notify_new_item,
startMenu = MainView.Section.LUCKY_NUMBER,
contentValues = listOf(item.luckyNumber.toString())
)
suspend fun notify(item: LuckyNumber, student: Student) {
val notification = OneNotificationData(
type = NotificationType.NEW_LUCKY_NUMBER,
icon = R.drawable.ic_stat_luckynumber,
titleStringRes = R.string.lucky_number_notify_new_item_title,
contentStringRes = R.string.lucky_number_notify_new_item,
startMenu = MainView.Section.LUCKY_NUMBER,
contentValues = listOf(item.luckyNumber.toString())
)
sendNotification(notification, student)
appNotificationManager.sendNotification(notification, student)
}
}

View File

@ -1,22 +1,18 @@
package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.data.pojos.MultipleNotificationsData
import io.github.wulkanowy.ui.modules.main.MainView
import javax.inject.Inject
class NewMessageNotification @Inject constructor(
@ApplicationContext private val context: Context,
notificationManager: NotificationManagerCompat,
) : BaseNotification(context, notificationManager) {
private val appNotificationManager: AppNotificationManager
) {
fun notify(items: List<Message>, student: Student) {
val notification = MultipleNotifications(
suspend fun notify(items: List<Message>, student: Student) {
val notification = MultipleNotificationsData(
type = NotificationType.NEW_MESSAGE,
icon = R.drawable.ic_stat_message,
titleStringRes = R.plurals.message_new_items,
@ -28,6 +24,6 @@ class NewMessageNotification @Inject constructor(
}
)
sendNotification(notification, student)
appNotificationManager.sendNotification(notification, student)
}
}

View File

@ -1,23 +1,19 @@
package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.data.pojos.MultipleNotificationsData
import io.github.wulkanowy.sdk.scrapper.notes.NoteCategory
import io.github.wulkanowy.ui.modules.main.MainView
import javax.inject.Inject
class NewNoteNotification @Inject constructor(
@ApplicationContext private val context: Context,
notificationManager: NotificationManagerCompat,
) : BaseNotification(context, notificationManager) {
private val appNotificationManager: AppNotificationManager
) {
fun notify(items: List<Note>, student: Student) {
val notification = MultipleNotifications(
suspend fun notify(items: List<Note>, student: Student) {
val notification = MultipleNotificationsData(
type = NotificationType.NEW_NOTE,
icon = R.drawable.ic_stat_note,
titleStringRes = when (NoteCategory.getByValue(items.first().categoryType)) {
@ -41,6 +37,6 @@ class NewNoteNotification @Inject constructor(
}
)
sendNotification(notification, student)
appNotificationManager.sendNotification(notification, student)
}
}

View File

@ -1,33 +1,29 @@
package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.data.pojos.MultipleNotificationsData
import io.github.wulkanowy.ui.modules.main.MainView
import javax.inject.Inject
class NewSchoolAnnouncementNotification @Inject constructor(
@ApplicationContext private val context: Context,
notificationManager: NotificationManagerCompat,
) : BaseNotification(context, notificationManager) {
private val appNotificationManager: AppNotificationManager
) {
fun notify(items: List<SchoolAnnouncement>, student: Student) {
val notification = MultipleNotifications(
type = NotificationType.NEW_ANNOUNCEMENT,
icon = R.drawable.ic_all_about,
titleStringRes = R.plurals.school_announcement_notify_new_item_title,
contentStringRes = R.plurals.school_announcement_notify_new_items,
summaryStringRes = R.plurals.school_announcement_number_item,
startMenu = MainView.Section.SCHOOL_ANNOUNCEMENT,
lines = items.map {
"${it.subject}: ${it.content}"
}
suspend fun notify(items: List<SchoolAnnouncement>, student: Student) {
val notification = MultipleNotificationsData(
type = NotificationType.NEW_ANNOUNCEMENT,
icon = R.drawable.ic_all_about,
titleStringRes = R.plurals.school_announcement_notify_new_item_title,
contentStringRes = R.plurals.school_announcement_notify_new_items,
summaryStringRes = R.plurals.school_announcement_number_item,
startMenu = MainView.Section.SCHOOL_ANNOUNCEMENT,
lines = items.map {
"${it.subject}: ${it.content}"
}
)
sendNotification(notification, student)
appNotificationManager.sendNotification(notification, student)
}
}

View File

@ -8,8 +8,9 @@ import io.github.wulkanowy.services.sync.channels.NewHomeworkChannel
import io.github.wulkanowy.services.sync.channels.NewMessagesChannel
import io.github.wulkanowy.services.sync.channels.NewNotesChannel
import io.github.wulkanowy.services.sync.channels.NewSchoolAnnouncementsChannel
import io.github.wulkanowy.services.sync.channels.PushChannel
enum class NotificationType(val group: String, val channel: String) {
enum class NotificationType(val group: String?, val channel: String) {
NEW_CONFERENCE("new_conferences_group", NewConferencesChannel.CHANNEL_ID),
NEW_EXAM("new_exam_group", NewExamChannel.CHANNEL_ID),
NEW_GRADE_DETAILS("new_grade_details_group", NewGradesChannel.CHANNEL_ID),
@ -20,4 +21,5 @@ enum class NotificationType(val group: String, val channel: String) {
NEW_MESSAGE("new_message_group", NewMessagesChannel.CHANNEL_ID),
NEW_NOTE("new_notes_group", NewNotesChannel.CHANNEL_ID),
NEW_ANNOUNCEMENT("new_school_announcements_group", NewSchoolAnnouncementsChannel.CHANNEL_ID),
PUSH(null, PushChannel.CHANNEL_ID)
}

View File

@ -9,9 +9,12 @@ import io.github.wulkanowy.utils.waitForResult
import java.time.LocalDate.now
import javax.inject.Inject
class AttendanceWork @Inject constructor(private val attendanceRepository: AttendanceRepository) : Work {
class AttendanceWork @Inject constructor(
private val attendanceRepository: AttendanceRepository
) : Work {
override suspend fun doWork(student: Student, semester: Semester) {
attendanceRepository.getAttendance(student, semester, now().monday, now().sunday, true).waitForResult()
attendanceRepository.getAttendance(student, semester, now().monday, now().sunday, true)
.waitForResult()
}
}

View File

@ -121,10 +121,14 @@ class AccountDetailsFragment :
}
}
override fun popView() {
override fun popViewToMain() {
(requireActivity() as MainActivity).popView(2)
}
override fun popViewToAccounts() {
(requireActivity() as MainActivity).popView(1)
}
override fun recreateMainView() {
requireActivity().recreate()
}

View File

@ -119,7 +119,7 @@ class AccountDetailsPresenter @Inject constructor(
}
}
}.afterLoading {
view?.popView()
view?.popViewToMain()
}.launch("switch")
}
@ -152,11 +152,14 @@ class AccountDetailsPresenter @Inject constructor(
syncManager.stopSyncWorker()
openClearLoginView()
}
studentWithSemesters!!.student.isCurrent -> {
studentWithSemesters?.student?.isCurrent == true -> {
Timber.i("Logout result: Logout student and switch to another")
recreateMainView()
}
else -> Timber.i("Logout result: Logout student")
else -> {
Timber.i("Logout result: Logout student")
recreateMainView()
}
}
}
Status.ERROR -> {
@ -165,7 +168,11 @@ class AccountDetailsPresenter @Inject constructor(
}
}
}.afterLoading {
view?.popView()
if (studentWithSemesters?.student?.isCurrent == true) {
view?.popViewToMain()
} else {
view?.popViewToAccounts()
}
}.launch("logout")
}

View File

@ -15,7 +15,9 @@ interface AccountDetailsView : BaseView {
fun showLogoutConfirmDialog()
fun popView()
fun popViewToMain()
fun popViewToAccounts()
fun recreateMainView()

View File

@ -14,6 +14,8 @@ class ConferenceAdapter @Inject constructor() :
var items = emptyList<Conference>()
var onItemClickListener: (Conference) -> Unit = {}
override fun getItemCount() = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder(
@ -28,7 +30,10 @@ class ConferenceAdapter @Inject constructor() :
conferenceItemTitle.text = item.title
conferenceItemSubject.text = item.subject
conferenceItemContent.text = item.agenda
conferenceItemContent.visibility = if (item.agenda.isBlank()) View.GONE else View.VISIBLE
conferenceItemContent.visibility =
if (item.agenda.isBlank()) View.GONE else View.VISIBLE
root.setOnClickListener { onItemClickListener(item) }
}
}

View File

@ -0,0 +1,60 @@
package io.github.wulkanowy.ui.modules.conference
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.data.db.entities.Conference
import io.github.wulkanowy.databinding.DialogConferenceBinding
import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.toFormattedString
class ConferenceDialog : DialogFragment() {
private var binding: DialogConferenceBinding by lifecycleAwareVariable()
private lateinit var conference: Conference
companion object {
private const val ARGUMENT_KEY = "item"
fun newInstance(conference: Conference) = ConferenceDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, conference) }
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
arguments?.let {
conference = it.getSerializable(ARGUMENT_KEY) as Conference
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = DialogConferenceBinding.inflate(inflater).also { binding = it }.root
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(binding) {
conferenceDialogClose.setOnClickListener { dismiss() }
conferenceDialogSubjectValue.text = conference.subject
conferenceDialogDateValue.text = conference.date.toFormattedString("dd.MM.yyyy HH:mm")
conferenceDialogHeaderValue.text = conference.title
conferenceDialogAgendaValue.text = conference.agenda
conferenceDialogPresentValue.text = conference.presentOnConference
conferenceDialogPresentValue.isVisible = conference.presentOnConference.isNotBlank()
conferenceDialogPresentTitle.isVisible = conference.presentOnConference.isNotBlank()
conferenceDialogAgendaValue.isVisible = conference.agenda.isNotBlank()
conferenceDialogAgendaTitle.isVisible = conference.agenda.isNotBlank()
}
}
}

View File

@ -8,6 +8,7 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Conference
import io.github.wulkanowy.databinding.FragmentConferenceBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.getThemeAttrColor
@ -41,6 +42,8 @@ class ConferenceFragment : BaseFragment<FragmentConferenceBinding>(R.layout.frag
}
override fun initView() {
conferencesAdapter.onItemClickListener = presenter::onItemSelected
with(binding.conferenceRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = conferencesAdapter
@ -50,7 +53,11 @@ class ConferenceFragment : BaseFragment<FragmentConferenceBinding>(R.layout.frag
with(binding) {
conferenceSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
conferenceSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
conferenceSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
conferenceSwipe.setProgressBackgroundColorSchemeColor(
requireContext().getThemeAttrColor(
R.attr.colorSwipeRefresh
)
)
conferenceErrorRetry.setOnClickListener { presenter.onRetry() }
conferenceErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}
@ -98,6 +105,10 @@ class ConferenceFragment : BaseFragment<FragmentConferenceBinding>(R.layout.frag
binding.conferenceRecycler.visibility = if (show) View.VISIBLE else View.GONE
}
override fun openConferenceDialog(conference: Conference) {
(activity as? MainActivity)?.showDialogFragment(ConferenceDialog.newInstance(conference))
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.ui.modules.conference
import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.db.entities.Conference
import io.github.wulkanowy.data.repositories.ConferenceRepository
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
@ -43,6 +44,10 @@ class ConferencePresenter @Inject constructor(
loadData(true)
}
fun onItemSelected(conference: Conference) {
view?.openConferenceDialog(conference)
}
fun onDetailsClick() {
view?.showErrorDetailsDialog(lastError)
}

View File

@ -26,4 +26,6 @@ interface ConferenceView : BaseView {
fun enableSwipe(enable: Boolean)
fun showContent(show: Boolean)
fun openConferenceDialog(conference: Conference)
}

View File

@ -53,7 +53,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
var onAttendanceTileClickListener: () -> Unit = {}
var onLessonsTileClickListener: () -> Unit = {}
var onLessonsTileClickListener: (LocalDate) -> Unit = {}
var onHomeworkTileClickListener: () -> Unit = {}
@ -275,10 +275,12 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
val item = items[position] as DashboardItem.Lessons
val timetableFull = item.lessons
val binding = lessonsViewHolder.binding
var dateToNavigate = LocalDate.now()
fun updateLessonState() {
val currentDateTime = LocalDateTime.now()
val currentDate = LocalDate.now()
val tomorrowDate = currentDate.plusDays(1)
val currentTimetable = timetableFull?.lessons
.orEmpty()
@ -296,22 +298,27 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
when {
currentTimetable.isNotEmpty() -> {
dateToNavigate = currentDate
updateLessonView(item, currentTimetable, binding)
binding.dashboardLessonsItemTitleTomorrow.isVisible = false
}
tomorrowTimetable.isNotEmpty() -> {
dateToNavigate = tomorrowDate
updateLessonView(item, tomorrowTimetable, binding)
binding.dashboardLessonsItemTitleTomorrow.isVisible = true
}
currentDayHeader != null && currentDayHeader.content.isNotBlank() -> {
dateToNavigate = currentDate
updateLessonView(item, emptyList(), binding, currentDayHeader)
binding.dashboardLessonsItemTitleTomorrow.isVisible = false
}
tomorrowDayHeader != null && tomorrowDayHeader.content.isNotBlank() -> {
dateToNavigate = tomorrowDate
updateLessonView(item, emptyList(), binding, tomorrowDayHeader)
binding.dashboardLessonsItemTitleTomorrow.isVisible = true
}
else -> {
dateToNavigate = tomorrowDate
updateLessonView(item, emptyList(), binding)
binding.dashboardLessonsItemTitleTomorrow.isVisible =
!(item.isLoading && item.error == null)
@ -326,7 +333,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
Handler(Looper.getMainLooper()).post { updateLessonState() }
}
binding.root.setOnClickListener { onLessonsTileClickListener() }
binding.root.setOnClickListener { onLessonsTileClickListener(dateToNavigate) }
}
private fun updateLessonView(

View File

@ -24,6 +24,7 @@ import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment
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.notificationscenter.NotificationsCenterFragment
import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
import io.github.wulkanowy.utils.capitalise
@ -70,10 +71,7 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
override fun initView() {
val mainActivity = requireActivity() as MainActivity
val itemTouchHelper = ItemTouchHelper(
DashboardItemMoveCallback(
dashboardAdapter,
presenter::onDragAndDropEnd
)
DashboardItemMoveCallback(dashboardAdapter, presenter::onDragAndDropEnd)
)
dashboardAdapter.apply {
@ -87,7 +85,9 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
onAttendanceTileClickListener = {
mainActivity.pushView(AttendanceSummaryFragment.newInstance())
}
onLessonsTileClickListener = { mainActivity.pushView(TimetableFragment.newInstance()) }
onLessonsTileClickListener = {
mainActivity.pushView(TimetableFragment.newInstance(it))
}
onGradeTileClickListener = { mainActivity.pushView(GradeFragment.newInstance()) }
onHomeworkTileClickListener = { mainActivity.pushView(HomeworkFragment.newInstance()) }
onAnnouncementsTileClickListener = {
@ -121,6 +121,7 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.dashboard_menu_tiles -> presenter.onDashboardTileSettingsSelected()
R.id.dashboard_menu_notifaction_list -> presenter.onNotificationsCenterSelected()
else -> false
}
}
@ -183,6 +184,10 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
if (::presenter.isInitialized) presenter.onViewReselected()
}
override fun openNotificationsCenterView() {
(requireActivity() as MainActivity).pushView(NotificationsCenterFragment.newInstance())
}
override fun onDestroyView() {
dashboardAdapter.clearTimers()
presenter.onDetachView()

View File

@ -209,6 +209,11 @@ class DashboardPresenter @Inject constructor(
view?.showErrorDetailsDialog(lastError)
}
fun onNotificationsCenterSelected(): Boolean {
view?.openNotificationsCenterView()
return true
}
fun onDashboardTileSettingsSelected(): Boolean {
view?.showDashboardTileSettings(preferencesRepository.selectedDashboardTiles.toList())
return true
@ -492,31 +497,37 @@ class DashboardPresenter @Inject constructor(
end = LocalDate.now().plusDays(7),
forceRefresh = forceRefresh
)
}.onEach {
when (it.status) {
Status.LOADING -> {
Timber.i("Loading dashboard exams data started")
if (forceRefresh) return@onEach
updateData(
DashboardItem.Exams(it.data.orEmpty(), isLoading = true),
forceRefresh
)
}
.map { examResource ->
val sortedExams = examResource.data?.sortedBy { it.date }
if (!it.data.isNullOrEmpty()) {
firstLoadedItemList += DashboardItem.Type.EXAMS
examResource.copy(data = sortedExams)
}
.onEach {
when (it.status) {
Status.LOADING -> {
Timber.i("Loading dashboard exams data started")
if (forceRefresh) return@onEach
updateData(
DashboardItem.Exams(it.data.orEmpty(), isLoading = true),
forceRefresh
)
if (!it.data.isNullOrEmpty()) {
firstLoadedItemList += DashboardItem.Type.EXAMS
}
}
Status.SUCCESS -> {
Timber.i("Loading dashboard exams result: Success")
updateData(DashboardItem.Exams(it.data ?: emptyList()), forceRefresh)
}
Status.ERROR -> {
Timber.i("Loading dashboard exams result: An exception occurred")
errorHandler.dispatch(it.error!!)
updateData(DashboardItem.Exams(error = it.error), forceRefresh)
}
}
Status.SUCCESS -> {
Timber.i("Loading dashboard exams result: Success")
updateData(DashboardItem.Exams(it.data ?: emptyList()), forceRefresh)
}
Status.ERROR -> {
Timber.i("Loading dashboard exams result: An exception occurred")
errorHandler.dispatch(it.error!!)
updateData(DashboardItem.Exams(error = it.error), forceRefresh)
}
}
}.launch("dashboard_exams")
}.launch("dashboard_exams")
}
private fun loadConferences(student: Student, forceRefresh: Boolean) {

View File

@ -23,4 +23,6 @@ interface DashboardView : BaseView {
fun resetView()
fun popViewToRoot()
fun openNotificationsCenterView()
}

View File

@ -87,7 +87,7 @@ class NotificationDebugPresenter @Inject constructor(
}
}
private fun withStudent(block: (Student) -> Unit) {
private fun withStudent(block: suspend (Student) -> Unit) {
launch {
block(studentRepository.getCurrentStudent(false))
}

View File

@ -131,7 +131,9 @@ class GradeAverageProvider @Inject constructor(
val updatedFirstSemesterGrades =
firstSemesterSubject?.grades?.updateModifiers(student).orEmpty()
(updatedSecondSemesterGrades + updatedFirstSemesterGrades).calcAverage(isOptionalArithmeticAverage)
(updatedSecondSemesterGrades + updatedFirstSemesterGrades).calcAverage(
isOptionalArithmeticAverage
)
} else {
secondSemesterSubject.average
}
@ -147,7 +149,8 @@ class GradeAverageProvider @Inject constructor(
return if (!isAnyVulcanAverage || isGradeAverageForceCalc) {
val secondSemesterAverage =
secondSemesterSubject.grades.updateModifiers(student).calcAverage(isOptionalArithmeticAverage)
secondSemesterSubject.grades.updateModifiers(student)
.calcAverage(isOptionalArithmeticAverage)
val firstSemesterAverage = firstSemesterSubject?.grades?.updateModifiers(student)
?.calcAverage(isOptionalArithmeticAverage) ?: secondSemesterAverage
@ -213,7 +216,8 @@ class GradeAverageProvider @Inject constructor(
proposedPoints = "",
finalPoints = "",
pointsSum = "",
average = if (calcAverage) details.updateModifiers(student).calcAverage(isOptionalArithmeticAverage) else .0
average = if (calcAverage) details.updateModifiers(student)
.calcAverage(isOptionalArithmeticAverage) else .0
)
}
}

View File

@ -10,7 +10,7 @@ import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.ItemGradeSummaryBinding
import io.github.wulkanowy.databinding.ScrollableHeaderGradeSummaryBinding
import io.github.wulkanowy.utils.calcAverage
import io.github.wulkanowy.utils.calcFinalAverage
import java.util.Locale
import javax.inject.Inject
@ -25,6 +25,10 @@ class GradeSummaryAdapter @Inject constructor(
var items = emptyList<GradeSummary>()
var onCalculatedHelpClickListener: () -> Unit = {}
var onFinalHelpClickListener: () -> Unit = {}
override fun getItemCount() = items.size + if (items.isNotEmpty()) 1 else 0
override fun getItemViewType(position: Int) = when (position) {
@ -60,7 +64,7 @@ class GradeSummaryAdapter @Inject constructor(
val finalItemsCount = items.count { it.finalGrade.matches("[0-6][+-]?".toRegex()) }
val calculatedItemsCount = items.count { value -> value.average != 0.0 }
val allItemsCount = items.count { !it.subject.equals("zachowanie", true) }
val finalAverage = items.calcAverage(
val finalAverage = items.calcFinalAverage(
preferencesRepository.gradePlusModifier,
preferencesRepository.gradeMinusModifier
)
@ -83,6 +87,9 @@ class GradeSummaryAdapter @Inject constructor(
calculatedItemsCount,
allItemsCount
)
gradeSummaryCalculatedAverageHelp.setOnClickListener { onCalculatedHelpClickListener() }
gradeSummaryFinalAverageHelp.setOnClickListener { onFinalHelpClickListener() }
}
}

View File

@ -5,6 +5,7 @@ import android.view.View
import android.view.View.GONE
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
@ -48,6 +49,11 @@ class GradeSummaryFragment :
}
override fun initView() {
with(gradeSummaryAdapter) {
onCalculatedHelpClickListener = presenter::onCalculatedAverageHelpClick
onFinalHelpClickListener = presenter::onFinalAverageHelpClick
}
with(binding.gradeSummaryRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = gradeSummaryAdapter
@ -55,7 +61,11 @@ class GradeSummaryFragment :
with(binding) {
gradeSummarySwipe.setOnRefreshListener(presenter::onSwipeRefresh)
gradeSummarySwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
gradeSummarySwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
gradeSummarySwipe.setProgressBackgroundColorSchemeColor(
requireContext().getThemeAttrColor(
R.attr.colorSwipeRefresh
)
)
gradeSummaryErrorRetry.setOnClickListener { presenter.onRetry() }
gradeSummaryErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}
@ -107,6 +117,22 @@ class GradeSummaryFragment :
binding.gradeSummarySwipe.isRefreshing = show
}
override fun showCalculatedAverageHelpDialog() {
AlertDialog.Builder(requireContext())
.setTitle(R.string.grade_summary_calculated_average_help_dialog_title)
.setMessage(R.string.grade_summary_calculated_average_help_dialog_message)
.setPositiveButton(R.string.all_close) { _, _ -> }
.show()
}
override fun showFinalAverageHelpDialog() {
AlertDialog.Builder(requireContext())
.setTitle(R.string.grade_summary_final_average_help_dialog_title)
.setMessage(R.string.grade_summary_final_average_help_dialog_message)
.setPositiveButton(R.string.all_close) { _, _ -> }
.show()
}
override fun onParentLoadData(semesterId: Int, forceRefresh: Boolean) {
presenter.onParentViewLoadData(semesterId, forceRefresh)
}

View File

@ -135,6 +135,14 @@ class GradeSummaryPresenter @Inject constructor(
cancelJobs("load")
}
fun onCalculatedAverageHelpClick() {
view?.showCalculatedAverageHelpDialog()
}
fun onFinalAverageHelpClick() {
view?.showFinalAverageHelpDialog()
}
private fun createGradeSummaryItems(items: List<GradeSubject>): List<GradeSummary> {
return items
.filter { !checkEmpty(it) }

View File

@ -33,6 +33,10 @@ interface GradeSummaryView : BaseView {
fun showEmpty(show: Boolean)
fun showCalculatedAverageHelpDialog()
fun showFinalAverageHelpDialog()
fun notifyParentDataLoaded(semesterId: Int)
fun notifyParentRefresh()

View File

@ -103,9 +103,8 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
}
override fun notifyInitSymbolFragment(loginData: Triple<String, String, String>) {
(loginAdapter.getFragmentInstance(1) as? LoginSymbolFragment)?.onParentInitSymbolFragment(
loginData
)
(loginAdapter.getFragmentInstance(1) as? LoginSymbolFragment)
?.onParentInitSymbolFragment(loginData)
}
override fun notifyInitStudentSelectFragment(studentsWithSemesters: List<StudentWithSemesters>) {

View File

@ -13,7 +13,7 @@ import javax.inject.Inject
class LoginErrorHandler @Inject constructor(resources: Resources) : ErrorHandler(resources) {
var onBadCredentials: () -> Unit = {}
var onBadCredentials: (String?) -> Unit = {}
var onInvalidToken: (String) -> Unit = {}
@ -25,7 +25,7 @@ class LoginErrorHandler @Inject constructor(resources: Resources) : ErrorHandler
override fun proceed(error: Throwable) {
when (error) {
is BadCredentialsException -> onBadCredentials()
is BadCredentialsException -> onBadCredentials(error.message)
is SQLiteConstraintException -> onStudentDuplicate(resources.getString(R.string.login_duplicate_student))
is TokenDeadException -> onInvalidToken(resources.getString(R.string.login_expired_token))
is InvalidTokenException -> onInvalidToken(resources.getString(R.string.login_invalid_token))

View File

@ -51,10 +51,12 @@ class LoginAdvancedFragment :
private lateinit var hostSymbols: Array<String>
override val formHostValue: String
get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())).orEmpty()
get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString()))
.orEmpty()
override val formHostSymbol: String
get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())).orEmpty()
get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString()))
.orEmpty()
override val formPinValue: String
get() = binding.loginFormPin.text.toString().trim()
@ -92,39 +94,62 @@ class LoginAdvancedFragment :
loginFormSignIn.setOnClickListener { presenter.onSignInClick() }
loginTypeSwitch.setOnCheckedChangeListener { _, checkedId ->
presenter.onLoginModeSelected(when (checkedId) {
R.id.loginTypeApi -> Sdk.Mode.API
R.id.loginTypeScrapper -> Sdk.Mode.SCRAPPER
else -> Sdk.Mode.HYBRID
})
presenter.onLoginModeSelected(
when (checkedId) {
R.id.loginTypeApi -> Sdk.Mode.API
R.id.loginTypeScrapper -> Sdk.Mode.SCRAPPER
else -> Sdk.Mode.HYBRID
}
)
}
loginFormPin.setOnEditorDoneSignIn { loginFormSignIn.callOnClick() }
loginFormPass.setOnEditorDoneSignIn { loginFormSignIn.callOnClick() }
loginFormSymbol.setAdapter(ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, resources.getStringArray(R.array.symbols_values)))
loginFormSymbol.setAdapter(
ArrayAdapter(
requireContext(),
android.R.layout.simple_list_item_1,
resources.getStringArray(R.array.symbols_values)
)
)
}
with(binding.loginFormHost) {
setText(hostKeys.getOrNull(0).orEmpty())
setAdapter(LoginSymbolAdapter(context, R.layout.support_simple_spinner_dropdown_item, hostKeys))
setAdapter(
LoginSymbolAdapter(
context,
R.layout.support_simple_spinner_dropdown_item,
hostKeys
)
)
setOnClickListener { if (binding.loginFormContainer.visibility == GONE) dismissDropDown() }
}
}
override fun showMobileApiWarningMessage() {
binding.loginFormAdvancedWarningInfo.text = getString(R.string.login_advanced_warning_mobile_api)
binding.loginFormAdvancedWarningInfo.text =
getString(R.string.login_advanced_warning_mobile_api)
}
override fun showScraperWarningMessage() {
binding.loginFormAdvancedWarningInfo.text = getString(R.string.login_advanced_warning_scraper)
binding.loginFormAdvancedWarningInfo.text =
getString(R.string.login_advanced_warning_scraper)
}
override fun showHybridWarningMessage() {
binding.loginFormAdvancedWarningInfo.text = getString(R.string.login_advanced_warning_hybrid)
binding.loginFormAdvancedWarningInfo.text =
getString(R.string.login_advanced_warning_hybrid)
}
override fun setDefaultCredentials(username: String, pass: String, symbol: String, token: String, pin: String) {
override fun setDefaultCredentials(
username: String,
pass: String,
symbol: String,
token: String,
pin: String
) {
with(binding) {
loginFormUsername.setText(username)
loginFormPass.setText(pass)
@ -177,10 +202,10 @@ class LoginAdvancedFragment :
}
}
override fun setErrorPassIncorrect() {
override fun setErrorPassIncorrect(message: String?) {
with(binding.loginFormPassLayout) {
requestFocus()
error = getString(R.string.login_incorrect_password)
error = message ?: getString(R.string.login_incorrect_password)
}
}
@ -296,11 +321,13 @@ class LoginAdvancedFragment :
}
override fun notifyParentAccountLogged(studentsWithSemesters: List<StudentWithSemesters>) {
(activity as? LoginActivity)?.onFormFragmentAccountLogged(studentsWithSemesters, Triple(
binding.loginFormUsername.text.toString(),
binding.loginFormPass.text.toString(),
resources.getStringArray(R.array.hosts_values)[1]
))
(activity as? LoginActivity)?.onFormFragmentAccountLogged(
studentsWithSemesters, Triple(
binding.loginFormUsername.text.toString(),
binding.loginFormPass.text.toString(),
resources.getStringArray(R.array.hosts_values)[1]
)
)
}
override fun onResume() {

View File

@ -34,9 +34,9 @@ class LoginAdvancedPresenter @Inject constructor(
}
}
private fun onBadCredentials() {
private fun onBadCredentials(message: String?) {
view?.run {
setErrorPassIncorrect()
setErrorPassIncorrect(message)
showSoftKeyboard()
Timber.i("Entered wrong username or password")
}

View File

@ -49,7 +49,7 @@ interface LoginAdvancedView : BaseView {
fun setErrorPassInvalid(focus: Boolean)
fun setErrorPassIncorrect()
fun setErrorPassIncorrect(message: String?)
fun clearUsernameError()

View File

@ -5,6 +5,7 @@ import android.os.Bundle
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
@ -41,10 +42,12 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
get() = binding.loginFormPass.text.toString()
override val formHostValue: String
get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())).orEmpty()
get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString()))
.orEmpty()
override val formHostSymbol: String
get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())).orEmpty()
get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString()))
.orEmpty()
override val nicknameLabel: String
get() = getString(R.string.login_nickname_hint)
@ -88,7 +91,13 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
with(binding.loginFormHost) {
setText(hostKeys.getOrNull(0).orEmpty())
setAdapter(LoginSymbolAdapter(context, R.layout.support_simple_spinner_dropdown_item, hostKeys))
setAdapter(
LoginSymbolAdapter(
context,
R.layout.support_simple_spinner_dropdown_item,
hostKeys
)
)
setOnClickListener { if (binding.loginFormContainer.visibility == GONE) dismissDropDown() }
}
}
@ -142,24 +151,31 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
}
}
override fun setErrorPassIncorrect() {
with(binding.loginFormPassLayout) {
error = getString(R.string.login_incorrect_password)
override fun setErrorPassIncorrect(message: String?) {
val error = message ?: getString(R.string.login_incorrect_password_default)
with(binding) {
loginFormUsernameLayout.error = " "
loginFormPassLayout.error = " "
loginFormErrorBox.text = getString(R.string.login_incorrect_password, error)
loginFormErrorBox.isVisible = true
}
}
override fun setErrorEmailInvalid(domain: String) {
with(binding.loginFormUsernameLayout) {
error = getString(R.string.login_invalid_custom_email,domain)
error = getString(R.string.login_invalid_custom_email, domain)
}
}
override fun clearUsernameError() {
binding.loginFormUsernameLayout.error = null
binding.loginFormErrorBox.isVisible = false
}
override fun clearPassError() {
binding.loginFormPassLayout.error = null
binding.loginFormErrorBox.isVisible = false
}
override fun showSoftKeyboard() {
@ -183,12 +199,18 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
binding.loginFormVersion.text = "v${appInfo.versionName}"
}
override fun notifyParentAccountLogged(studentsWithSemesters: List<StudentWithSemesters>, loginData: Triple<String, String, String>) {
override fun notifyParentAccountLogged(
studentsWithSemesters: List<StudentWithSemesters>,
loginData: Triple<String, String, String>
) {
(activity as? LoginActivity)?.onFormFragmentAccountLogged(studentsWithSemesters, loginData)
}
override fun openPrivacyPolicyPage() {
context?.openInternetBrowser("https://wulkanowy.github.io/polityka-prywatnosci.html", ::showMessage)
context?.openInternetBrowser(
"https://wulkanowy.github.io/polityka-prywatnosci.html",
::showMessage
)
}
override fun showContact(show: Boolean) {
@ -210,7 +232,10 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
}
override fun openFaqPage() {
context?.openInternetBrowser("https://wulkanowy.github.io/czesto-zadawane-pytania/dlaczego-nie-moge-sie-zalogowac", ::showMessage)
context?.openInternetBrowser(
"https://wulkanowy.github.io/czesto-zadawane-pytania/dlaczego-nie-moge-sie-zalogowac",
::showMessage
)
}
override fun onResume() {
@ -223,7 +248,8 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
chooserTitle = requireContext().getString(R.string.login_email_intent_title),
email = "wulkanowyinc@gmail.com",
subject = requireContext().getString(R.string.login_email_subject),
body = requireContext().getString(R.string.login_email_text,
body = requireContext().getString(
R.string.login_email_text,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
appInfo.versionName,

View File

@ -30,7 +30,7 @@ class LoginFormPresenter @Inject constructor(
showVersion()
loginErrorHandler.onBadCredentials = {
setErrorPassIncorrect()
setErrorPassIncorrect(it)
showSoftKeyboard()
Timber.i("Entered wrong username or password")
}

View File

@ -37,7 +37,7 @@ interface LoginFormView : BaseView {
fun setErrorPassInvalid(focus: Boolean)
fun setErrorPassIncorrect()
fun setErrorPassIncorrect(message: String?)
fun setErrorEmailInvalid(domain: String)

View File

@ -79,12 +79,7 @@ class MessagePreviewAdapter @Inject constructor() :
val readText = when {
recipientCount > 1 -> {
context.resources.getQuantityString(
R.plurals.message_read_by,
message.readBy,
message.readBy,
recipientCount
)
context.getString(R.string.message_read_by, message.readBy, recipientCount)
}
message.readBy == 1 -> {
context.getString(R.string.message_read, context.getString(R.string.all_yes))

View File

@ -0,0 +1,62 @@
package io.github.wulkanowy.ui.modules.notificationscenter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Notification
import io.github.wulkanowy.databinding.ItemNotificationsCenterBinding
import io.github.wulkanowy.services.sync.notifications.NotificationType
import io.github.wulkanowy.utils.toFormattedString
import javax.inject.Inject
class NotificationsCenterAdapter @Inject constructor() :
ListAdapter<Notification, NotificationsCenterAdapter.ViewHolder>(DiffUtilCallback()) {
var onItemClickListener: (NotificationType) -> Unit = {}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemNotificationsCenterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
with(holder.binding) {
notificationsCenterItemTitle.text = item.title
notificationsCenterItemContent.text = item.content
notificationsCenterItemDate.text = item.date.toFormattedString("HH:mm, d MMM")
notificationsCenterItemIcon.setImageResource(item.type.toDrawableResId())
root.setOnClickListener { onItemClickListener(item.type) }
}
}
private fun NotificationType.toDrawableResId() = when (this) {
NotificationType.NEW_CONFERENCE -> R.drawable.ic_more_conferences
NotificationType.NEW_EXAM -> R.drawable.ic_main_exam
NotificationType.NEW_GRADE_DETAILS -> R.drawable.ic_stat_grade
NotificationType.NEW_GRADE_PREDICTED -> R.drawable.ic_stat_grade
NotificationType.NEW_GRADE_FINAL -> R.drawable.ic_stat_grade
NotificationType.NEW_HOMEWORK -> R.drawable.ic_more_homework
NotificationType.NEW_LUCKY_NUMBER -> R.drawable.ic_stat_luckynumber
NotificationType.NEW_MESSAGE -> R.drawable.ic_stat_message
NotificationType.NEW_NOTE -> R.drawable.ic_stat_note
NotificationType.NEW_ANNOUNCEMENT -> R.drawable.ic_all_about
NotificationType.PUSH -> R.drawable.ic_stat_all
}
class ViewHolder(val binding: ItemNotificationsCenterBinding) :
RecyclerView.ViewHolder(binding.root)
private class DiffUtilCallback : DiffUtil.ItemCallback<Notification>() {
override fun areContentsTheSame(oldItem: Notification, newItem: Notification) =
oldItem == newItem
override fun areItemsTheSame(oldItem: Notification, newItem: Notification) =
oldItem.id == newItem.id
}
}

View File

@ -0,0 +1,108 @@
package io.github.wulkanowy.ui.modules.notificationscenter
import android.os.Bundle
import android.view.View
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Notification
import io.github.wulkanowy.databinding.FragmentNotificationsCenterBinding
import io.github.wulkanowy.services.sync.notifications.NotificationType
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.conference.ConferenceFragment
import io.github.wulkanowy.ui.modules.exam.ExamFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.homework.HomeworkFragment
import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment
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.note.NoteFragment
import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment
import javax.inject.Inject
@AndroidEntryPoint
class NotificationsCenterFragment :
BaseFragment<FragmentNotificationsCenterBinding>(R.layout.fragment_notifications_center),
NotificationsCenterView, MainView.TitledView {
@Inject
lateinit var presenter: NotificationsCenterPresenter
@Inject
lateinit var notificationsCenterAdapter: NotificationsCenterAdapter
companion object {
fun newInstance() = NotificationsCenterFragment()
}
override val titleStringId: Int
get() = R.string.notifications_center_title
override val isViewEmpty: Boolean
get() = notificationsCenterAdapter.itemCount == 0
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentNotificationsCenterBinding.bind(view)
presenter.onAttachView(this)
}
override fun initView() {
notificationsCenterAdapter.onItemClickListener = { notificationType ->
notificationType.toDestinationFragment()
?.let { (requireActivity() as MainActivity).pushView(it) }
}
with(binding.notificationsCenterRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = notificationsCenterAdapter
}
}
override fun updateData(data: List<Notification>) {
notificationsCenterAdapter.submitList(data)
}
override fun showEmpty(show: Boolean) {
binding.notificationsCenterEmpty.isVisible = show
}
override fun showProgress(show: Boolean) {
binding.notificationsCenterProgress.isVisible = show
}
override fun showContent(show: Boolean) {
binding.notificationsCenterRecycler.isVisible = show
}
override fun showErrorView(show: Boolean) {
binding.notificationCenterError.isVisible = show
}
override fun setErrorDetails(message: String) {
binding.notificationCenterErrorMessage.text = message
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
private fun NotificationType.toDestinationFragment(): Fragment? = when (this) {
NotificationType.NEW_CONFERENCE -> ConferenceFragment.newInstance()
NotificationType.NEW_EXAM -> ExamFragment.newInstance()
NotificationType.NEW_GRADE_DETAILS -> GradeFragment.newInstance()
NotificationType.NEW_GRADE_PREDICTED -> GradeFragment.newInstance()
NotificationType.NEW_GRADE_FINAL -> GradeFragment.newInstance()
NotificationType.NEW_HOMEWORK -> HomeworkFragment.newInstance()
NotificationType.NEW_LUCKY_NUMBER -> LuckyNumberFragment.newInstance()
NotificationType.NEW_MESSAGE -> MessageFragment.newInstance()
NotificationType.NEW_NOTE -> NoteFragment.newInstance()
NotificationType.NEW_ANNOUNCEMENT -> SchoolAnnouncementFragment.newInstance()
NotificationType.PUSH -> null
}
}

View File

@ -0,0 +1,83 @@
package io.github.wulkanowy.ui.modules.notificationscenter
import io.github.wulkanowy.data.repositories.NotificationRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import javax.inject.Inject
class NotificationsCenterPresenter @Inject constructor(
private val notificationRepository: NotificationRepository,
errorHandler: ErrorHandler,
studentRepository: StudentRepository
) : BasePresenter<NotificationsCenterView>(errorHandler, studentRepository) {
private lateinit var lastError: Throwable
override fun onAttachView(view: NotificationsCenterView) {
super.onAttachView(view)
view.initView()
Timber.i("Notifications centre view was initialized")
errorHandler.showErrorMessage = ::showErrorViewOnError
loadData()
}
fun onRetry() {
view?.run {
showErrorView(false)
showProgress(true)
}
loadData()
}
fun onDetailsClick() {
view?.showErrorDetailsDialog(lastError)
}
private fun loadData() {
Timber.i("Loading notifications data started")
flow {
val studentId = studentRepository.getCurrentStudent(false).id
emitAll(notificationRepository.getNotifications(studentId))
}
.map { notificationList -> notificationList.sortedByDescending { it.date } }
.catch { Timber.i("Loading notifications result: An exception occurred") }
.onEach {
Timber.i("Loading notifications result: Success")
if (it.isEmpty()) {
view?.run {
showContent(false)
showProgress(false)
showEmpty(true)
}
} else {
view?.run {
showContent(true)
showProgress(false)
showEmpty(false)
updateData(it)
}
}
}
.launch()
}
private fun showErrorViewOnError(message: String, error: Throwable) {
view?.run {
if (isViewEmpty) {
lastError = error
setErrorDetails(message)
showErrorView(true)
showEmpty(false)
} else showError(message, error)
}
}
}

View File

@ -0,0 +1,23 @@
package io.github.wulkanowy.ui.modules.notificationscenter
import io.github.wulkanowy.data.db.entities.Notification
import io.github.wulkanowy.ui.base.BaseView
interface NotificationsCenterView : BaseView {
val isViewEmpty: Boolean
fun initView()
fun updateData(data: List<Notification>)
fun showProgress(show: Boolean)
fun showEmpty(show: Boolean)
fun showContent(show: Boolean)
fun showErrorView(show: Boolean)
fun setErrorDetails(message: String)
}

View File

@ -38,7 +38,7 @@ class SchoolAnnouncementDialog : DialogFragment() {
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = DialogSchoolAnnouncementBinding.inflate(inflater).apply { binding = this }.root
) = DialogSchoolAnnouncementBinding.inflate(inflater).also { binding = it }.root
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

View File

@ -10,9 +10,12 @@ import android.provider.Settings
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.app.NotificationManagerCompat
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
import androidx.recyclerview.widget.RecyclerView
import com.thelittlefireman.appkillermanager.AppKillerManager
import com.thelittlefireman.appkillermanager.exceptions.NoActionFoundException
@ -43,6 +46,21 @@ class NotificationsFragment : PreferenceFragmentCompat(),
override val titleStringId get() = R.string.pref_settings_notifications_title
override val isNotificationPermissionGranted: Boolean
get() {
val packageNameList =
NotificationManagerCompat.getEnabledListenerPackages(requireContext())
val appPackageName = requireContext().packageName
return appPackageName in packageNameList
}
private val notificationSettingsContract =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
presenter.onNotificationPermissionResult()
}
override fun initView(showDebugNotificationSwitch: Boolean) {
findPreference<Preference>(getString(R.string.pref_key_notification_debug))?.isVisible =
showDebugNotificationSwitch
@ -57,12 +75,11 @@ class NotificationsFragment : PreferenceFragmentCompat(),
}
}
findPreference<Preference>(getString(R.string.pref_key_notifications_system_settings))?.run {
setOnPreferenceClickListener {
findPreference<Preference>(getString(R.string.pref_key_notifications_system_settings))
?.setOnPreferenceClickListener {
presenter.onOpenSystemSettingsClicked()
true
}
}
}
override fun onCreateRecyclerView(
@ -157,6 +174,25 @@ class NotificationsFragment : PreferenceFragmentCompat(),
}
}
override fun openNotificationPermissionDialog() {
AlertDialog.Builder(requireContext())
.setTitle(getString(R.string.pref_notification_piggyback_popup_title))
.setMessage(getString(R.string.pref_notification_piggyback_popup_description))
.setPositiveButton(getString(R.string.pref_notification_piggyback_popup_positive)) { _, _ ->
notificationSettingsContract.launch(Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"))
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
setNotificationPiggybackPreferenceChecked(false)
}
.setOnDismissListener { setNotificationPiggybackPreferenceChecked(false) }
.show()
}
override fun setNotificationPiggybackPreferenceChecked(isChecked: Boolean) {
findPreference<SwitchPreferenceCompat>(getString(R.string.pref_key_notifications_piggyback))?.isChecked =
isChecked
}
override fun onResume() {
super.onResume()
preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(this)

View File

@ -31,6 +31,9 @@ class NotificationsPresenter @Inject constructor(
)
initView(appInfo.isDebug)
}
checkNotificationPiggybackState()
Timber.i("Settings notifications view was initialized")
}
@ -39,7 +42,7 @@ class NotificationsPresenter @Inject constructor(
preferencesRepository.apply {
when (key) {
isUpcomingLessonsNotificationsEnableKey -> {
isUpcomingLessonsNotificationsEnableKey, isUpcomingLessonsNotificationsPersistentKey -> {
if (!isUpcomingLessonsNotificationsEnable) {
timetableNotificationHelper.cancelNotification()
}
@ -47,6 +50,11 @@ class NotificationsPresenter @Inject constructor(
isDebugNotificationEnableKey -> {
chuckerCollector.showNotification = isDebugNotificationEnable
}
isNotificationPiggybackEnabledKey -> {
if (isNotificationPiggybackEnabled && view?.isNotificationPermissionGranted == false) {
view?.openNotificationPermissionDialog()
}
}
}
}
analytics.logEvent("setting_changed", "name" to key)
@ -59,4 +67,18 @@ class NotificationsPresenter @Inject constructor(
fun onOpenSystemSettingsClicked() {
view?.openSystemSettings()
}
fun onNotificationPermissionResult() {
view?.run {
setNotificationPiggybackPreferenceChecked(isNotificationPermissionGranted)
}
}
private fun checkNotificationPiggybackState() {
if (preferencesRepository.isNotificationPiggybackEnabled) {
view?.run {
setNotificationPiggybackPreferenceChecked(isNotificationPermissionGranted)
}
}
}
}

View File

@ -4,6 +4,8 @@ import io.github.wulkanowy.ui.base.BaseView
interface NotificationsView : BaseView {
val isNotificationPermissionGranted: Boolean
fun initView(showDebugNotificationSwitch: Boolean)
fun showFixSyncDialog()
@ -11,4 +13,8 @@ interface NotificationsView : BaseView {
fun openSystemSettings()
fun enableNotification(notificationKey: String, enable: Boolean)
fun openNotificationPermissionDialog()
fun setNotificationPiggybackPreferenceChecked(isChecked: Boolean)
}

View File

@ -1,12 +1,13 @@
package io.github.wulkanowy.ui.modules.timetable
import android.graphics.Paint
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
@ -151,8 +152,8 @@ class TimetableAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
if (lesson.isStudentPlan && showTimers) {
timers[position] = timer(period = 1000) {
if (ViewCompat.isAttachedToWindow(root)) {
root.post { updateTimeLeft(binding, lesson, position) }
Handler(Looper.getMainLooper()).post {
updateTimeLeft(binding, lesson, position)
}
}
} else {
@ -176,8 +177,8 @@ 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 until = lesson.until.plusMinutes(1)
val left = lesson.left?.plusMinutes(1)
val isJustFinished = lesson.isJustFinished
with(binding) {
@ -190,17 +191,10 @@ class TimetableAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
visibility = VISIBLE
text = context.getString(
R.string.timetable_time_until,
if (until.seconds <= 60) {
context.getString(
R.string.timetable_seconds,
until.seconds.toString(10)
)
} else {
context.getString(
R.string.timetable_minutes,
until.toMinutes().toString(10)
)
}
context.getString(
R.string.timetable_minutes,
until.toMinutes().toString(10)
)
)
}
}
@ -212,17 +206,10 @@ class TimetableAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
visibility = VISIBLE
text = context.getString(
R.string.timetable_time_left,
if (left.seconds < 60) {
context.getString(
R.string.timetable_seconds,
left.seconds.toString(10)
)
} else {
context.getString(
R.string.timetable_minutes,
left.toMinutes().toString(10)
)
}
context.getString(
R.string.timetable_minutes,
left.toMinutes().toString()
)
)
}
}
@ -360,7 +347,7 @@ class TimetableAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
private fun updateTeacherColor(teacherTextView: TextView, lesson: Timetable) {
teacherTextView.setTextColor(
teacherTextView.context.getThemeAttrColor(
if (lesson.teacherOld.isNotBlank() && lesson.teacherOld != lesson.teacher) R.attr.colorTimetableChange
if (lesson.teacherOld.isNotBlank()) R.attr.colorTimetableChange
else android.R.attr.textColorSecondary
)
)

View File

@ -13,7 +13,6 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.databinding.DialogTimetableBinding
import io.github.wulkanowy.utils.capitalise
import io.github.wulkanowy.utils.decapitalise
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.toFormattedString
@ -52,7 +51,7 @@ class TimetableDialog : DialogFragment() {
super.onViewCreated(view, savedInstanceState)
with(lesson) {
setInfo(info, teacher, canceled, changes)
setInfo(info, canceled, changes)
setSubject(subject, subjectOld)
setTeacher(teacher, teacherOld)
setGroup(group)
@ -80,7 +79,7 @@ class TimetableDialog : DialogFragment() {
}
@SuppressLint("DefaultLocale")
private fun setInfo(info: String, teacher: String, canceled: Boolean, changes: Boolean) {
private fun setInfo(info: String, canceled: Boolean, changes: Boolean) {
with(binding) {
when {
info.isNotBlank() -> {
@ -90,20 +89,26 @@ class TimetableDialog : DialogFragment() {
R.attr.colorPrimary
)
)
timetableDialogChangesValue.setTextColor(requireContext().getThemeAttrColor(R.attr.colorPrimary))
timetableDialogChangesValue.setTextColor(
requireContext().getThemeAttrColor(
R.attr.colorPrimary
)
)
} else {
timetableDialogChangesTitle.setTextColor(
requireContext().getThemeAttrColor(
R.attr.colorTimetableChange
)
)
timetableDialogChangesValue.setTextColor(requireContext().getThemeAttrColor(R.attr.colorTimetableChange))
timetableDialogChangesValue.setTextColor(
requireContext().getThemeAttrColor(
R.attr.colorTimetableChange
)
)
}
timetableDialogChangesValue.text = when {
canceled && !changes -> "Lekcja odwołana: $info"
changes && teacher.isNotBlank() -> "Zastępstwo: $teacher"
changes && teacher.isBlank() -> "Zastępstwo, ${info.decapitalise()}"
else -> info.capitalise()
}
}
@ -131,6 +136,15 @@ class TimetableDialog : DialogFragment() {
}
}
}
teacherOld.isNotBlank() && teacherOld == teacher -> {
timetableDialogTeacherValue.run {
visibility = GONE
}
timetableDialogTeacherNewValue.run {
visibility = VISIBLE
text = teacher
}
}
teacher.isNotBlank() -> timetableDialogTeacherValue.text = teacher
else -> {
timetableDialogTeacherTitle.visibility = GONE

View File

@ -44,7 +44,13 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
companion object {
private const val SAVED_DATE_KEY = "CURRENT_DATE"
fun newInstance() = TimetableFragment()
private const val ARGUMENT_DATE_KEY = "ARGUMENT_DATE"
fun newInstance(date: LocalDate? = null) = TimetableFragment().apply {
arguments = Bundle().apply {
date?.let { putLong(ARGUMENT_DATE_KEY, it.toEpochDay()) }
}
}
}
override val titleStringId get() = R.string.timetable_title
@ -62,7 +68,11 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
super.onViewCreated(view, savedInstanceState)
binding = FragmentTimetableBinding.bind(view)
messageContainer = binding.timetableRecycler
presenter.onAttachView(this, savedInstanceState?.getLong(SAVED_DATE_KEY))
val initDate = savedInstanceState?.getLong(SAVED_DATE_KEY)
?: arguments?.getLong(ARGUMENT_DATE_KEY)?.takeUnless { it == 0L }
presenter.onAttachView(this, initDate)
}
override fun initView() {

View File

@ -173,7 +173,7 @@ class TimetableWidgetFactory(
updateNotCanceledLessonNumberColor(this, lesson)
updateNotCanceledSubjectColor(this, lesson)
val teacherChange = lesson.teacherOld.isNotBlank() && lesson.teacher != lesson.teacherOld
val teacherChange = lesson.teacherOld.isNotBlank()
updateNotCanceledRoom(this, lesson, teacherChange)
updateNotCanceledTeacher(this, lesson, teacherChange)
}

View File

@ -3,7 +3,7 @@ package io.github.wulkanowy.utils
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.sdk.scrapper.grades.*
import io.github.wulkanowy.sdk.scrapper.grades.isGradeValid
fun List<Grade>.calcAverage(isOptionalArithmeticAverage: Boolean): Double {
val isArithmeticAverage = isOptionalArithmeticAverage && !any { it.weightValue != .0 }
@ -18,8 +18,7 @@ fun List<Grade>.calcAverage(isOptionalArithmeticAverage: Boolean): Double {
return if (denominator != 0.0) counter / denominator else 0.0
}
@JvmName("calcSummaryAverage")
fun List<GradeSummary>.calcAverage(plusModifier: Double, minusModifier: Double) = asSequence()
fun List<GradeSummary>.calcFinalAverage(plusModifier: Double, minusModifier: Double) = asSequence()
.mapNotNull {
if (it.finalGrade.matches("[0-6][+-]?".toRegex())) {
when {

View File

@ -33,7 +33,7 @@ class AutoRefreshHelper @Inject constructor(
private val sharedPref: SharedPrefProvider
) {
fun isShouldBeRefreshed(key: String): Boolean {
fun shouldBeRefreshed(key: String): Boolean {
val timestamp = sharedPref.getLong(key, 0).toLocalDateTime()
val servicesInterval = sharedPref.getString(context.getString(R.string.pref_key_services_interval), context.getString(R.string.pref_default_services_interval)).toLong()

View File

@ -1,8 +1,9 @@
Wersja 1.2.2
Wersja 1.3.0
- naprawiliśmy problem z widocznością zadań w aplikacji gdy widoczne są one na stronie www dziennika (nadal pozostaje błąd z zadaniami widocznymi tylko w oficjalnej aplikacji - czekamy na poprawkę po stronie VULCANa)
- odblokowaliśmy niedzielę w wyborze daty w planie lekcji i innych zakładkach
- przywróciliśmy odnośnik do szczęśliwego numerka w menu Więcej
- naprawiliśmy drobne błędy ze stabilnością i wyglądem
- naprawiliśmy logowanie na platformę Opolskiej eSzkoły
- dodaliśmy centrum powiadomień i opcję odbierania pushy z oficjalnej aplikacji (dla zaawansowanych)
- dodaliśmy objaśnienie do informacji o obliczonych średnich w podsumowaniu
- poprawiliśmy wyświetlanie zmian w planie lekcji
- dokonaliśmy też kilka innych zmian i kosmetycznych poprawek poprawiających komfort używania aplikacji
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/white" />
<corners android:radius="5dp" />
<solid android:color="#bbffffff" />
<corners android:radius="8dp" />
</shape>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/colorWidgetBackground" />
<corners android:radius="5dp" />
<solid android:color="#BB191919" />
<corners android:radius="8dp" />
</shape>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#000000">
<path
android:fillColor="#FF000000"
android:pathData="M11,18h2v-2h-2v2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM12,6c-2.21,0 -4,1.79 -4,4h2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2c0,2 -3,1.75 -3,5h2c0,-2.25 3,-2.5 3,-5 0,-2.21 -1.79,-4 -4,-4z"/>
</vector>

View File

@ -0,0 +1,211 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="24dp"
android:paddingEnd="8dp">
<View
android:layout_width="280dp"
android:layout_height="1dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/allDetailsHeader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:text="@string/all_details"
android:textSize="21sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/conferenceDialogHeaderTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginTop="28dp"
android:layout_marginEnd="24dp"
android:text="@string/all_title"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/allDetailsHeader" />
<TextView
android:id="@+id/conferenceDialogHeaderValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="24dp"
android:paddingStart="0dp"
android:paddingEnd="16dp"
android:text="@string/all_no_data"
android:textIsSelectable="true"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/conferenceDialogHeaderTitle" />
<TextView
android:id="@+id/conferenceDialogSubjectTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="24dp"
android:text="@string/all_subject"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/conferenceDialogHeaderValue" />
<TextView
android:id="@+id/conferenceDialogSubjectValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="24dp"
android:paddingStart="0dp"
android:paddingEnd="16dp"
android:text="@string/all_no_data"
android:textIsSelectable="true"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/conferenceDialogSubjectTitle" />
<TextView
android:id="@+id/conferenceDialogDateTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="24dp"
android:text="@string/all_date"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/conferenceDialogSubjectValue" />
<TextView
android:id="@+id/conferenceDialogDateValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="24dp"
android:paddingStart="0dp"
android:paddingEnd="16dp"
android:text="@string/all_no_data"
android:textIsSelectable="true"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/conferenceDialogDateTitle" />
<TextView
android:id="@+id/conferenceDialogPresentTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="24dp"
android:text="@string/conferences_present"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/conferenceDialogDateValue" />
<TextView
android:id="@+id/conferenceDialogPresentValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="24dp"
android:paddingStart="0dp"
android:paddingEnd="16dp"
android:text="@string/all_no_data"
android:textIsSelectable="true"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/conferenceDialogPresentTitle" />
<TextView
android:id="@+id/conferenceDialogAgendaTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="24dp"
android:text="@string/conference_agenda"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/conferenceDialogPresentValue" />
<TextView
android:id="@+id/conferenceDialogAgendaValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="24dp"
android:paddingStart="0dp"
android:paddingEnd="16dp"
android:text="@string/all_no_data"
android:textIsSelectable="true"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/conferenceDialogAgendaTitle"
tools:maxLines="5"
tools:text="@tools:sample/lorem/random" />
<com.google.android.material.button.MaterialButton
android:id="@+id/conferenceDialogClose"
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:layout_marginTop="36dp"
android:layout_marginEnd="0dp"
android:layout_marginBottom="8dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:minWidth="88dp"
android:text="@string/all_close"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/conferenceDialogAgendaValue" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -118,6 +118,7 @@
android:layout_marginEnd="24dp"
android:paddingStart="0dp"
android:paddingEnd="16dp"
tools:maxLines="5"
android:text="@string/all_no_data"
android:textIsSelectable="true"
android:textSize="16sp"

View File

@ -110,10 +110,8 @@
android:layout_width="match_parent"
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:gravity="center_horizontal"
android:text="@string/login_header_default"
android:textSize="16sp"
@ -126,6 +124,20 @@
app:layout_constraintVertical_chainStyle="packed"
app:layout_goneMarginTop="64dp" />
<TextView
android:id="@+id/loginFormErrorBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="32dp"
android:textAppearance="?attr/textAppearanceCaption"
android:textColor="@color/mtrl_error"
android:textSize="14sp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/loginFormHeader"
tools:text="Nazwa użytkownika lub hasło są niepoprawne albo hasło do konta wygasło"
tools:visibility="visible" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/loginFormUsernameLayout"
@ -134,15 +146,15 @@
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginLeft="24dp"
android:layout_marginTop="48dp"
android:layout_marginTop="28dp"
android:layout_marginEnd="24dp"
android:layout_marginRight="24dp"
android:hint="@string/login_nickname_hint"
app:errorEnabled="true"
app:layout_constraintBottom_toTopOf="@+id/loginFormPassLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginFormHeader">
app:layout_constraintTop_toBottomOf="@+id/loginFormErrorBox"
app:layout_goneMarginTop="48dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/loginFormUsername"
@ -170,7 +182,6 @@
android:hint="@string/login_password_hint"
app:errorEnabled="true"
app:errorIconDrawable="@null"
app:layout_constraintBottom_toTopOf="@+id/loginFormRecoverLink"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginFormUsernameLayout"
@ -200,7 +211,6 @@
android:textAppearance="?android:textAppearance"
app:backgroundTint="?android:windowBackground"
app:fontFamily="sans-serif-medium"
app:layout_constraintBottom_toTopOf="@id/loginFormHostLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginFormPassLayout"
tools:visibility="visible" />
@ -217,7 +227,6 @@
android:layout_marginRight="24dp"
android:hint="@string/login_host_hint"
android:orientation="vertical"
app:layout_constraintBottom_toTopOf="@+id/loginFormAdvancedButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginFormRecoverLink">
@ -262,14 +271,13 @@
android:id="@+id/loginFormPrivacyLink"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:gravity="start|center_vertical"
android:text="@string/login_privacy_policy"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
app:fontFamily="sans-serif-medium"
app:layout_constraintStart_toStartOf="@id/loginFormAdvancedButton"
app:layout_constraintTop_toBottomOf="@+id/loginFormAdvancedButton"
app:layout_constraintTop_toTopOf="@+id/loginFormVersion"
tools:visibility="visible" />
<TextView

View File

@ -13,6 +13,8 @@
android:id="@+id/messageTabRecycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="64dp"
tools:listitem="@layout/item_message" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@ -0,0 +1,106 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/notifications_center_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="16dp"
android:visibility="gone"
tools:itemCount="4"
tools:listitem="@layout/item_notifications_center"
tools:visibility="visible" />
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/notifications_center_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="gone" />
<LinearLayout
android:id="@+id/notifications_center_empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="10dp"
android:visibility="gone"
tools:ignore="UseCompoundDrawables">
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
app:srcCompat="@drawable/ic_settings_notifications"
app:tint="?colorOnBackground"
tools:ignore="contentDescription" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:text="@string/all_no_data"
android:textSize="20sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/notification_center_error"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="invisible"
tools:ignore="UseCompoundDrawables"
tools:visibility="invisible">
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
app:srcCompat="@drawable/ic_error"
app:tint="?colorOnBackground"
tools:ignore="contentDescription" />
<TextView
android:id="@+id/notification_center_error_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:padding="8dp"
android:text="@string/error_unknown"
android:textSize="20sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/notification_center_error_details"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:text="@string/all_details" />
<com.google.android.material.button.MaterialButton
android:id="@+id/notification_center_error_retry"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/all_retry" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

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