1
0

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

View File

@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier same "printed page" as the copyright notice for easier
identification within third-party archives. identification within third-party archives.
Copyright 2019 Wulkanowy Copyright 2021 Wulkanowy
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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" testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 30 targetSdkVersion 30
versionCode 95 versionCode 97
versionName "1.2.2" versionName "1.3.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
@ -96,10 +96,20 @@ android {
} }
} }
playConfigs {
play { enabled.set(true) }
}
buildFeatures { buildFeatures {
viewBinding true viewBinding true
} }
bundle {
language {
enableSplit = false
}
}
testOptions.unitTests { testOptions.unitTests {
includeAndroidResources = true includeAndroidResources = true
} }
@ -130,11 +140,10 @@ kapt {
} }
play { play {
serviceAccountEmail = System.getenv("PLAY_SERVICE_ACCOUNT_EMAIL") ?: "jan@fakelog.cf"
serviceAccountCredentials = file('key.p12')
defaultToAppBundles = false defaultToAppBundles = false
track = 'production' track = 'production'
updatePriority = 3 updatePriority = 3
enabled.set(false)
} }
huaweiPublish { huaweiPublish {
@ -157,7 +166,7 @@ ext {
} }
dependencies { 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' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
@ -174,7 +183,7 @@ dependencies {
implementation "androidx.recyclerview:recyclerview:1.2.1" implementation "androidx.recyclerview:recyclerview:1.2.1"
implementation "androidx.viewpager:viewpager:1.0.0" implementation "androidx.viewpager:viewpager:1.0.0"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.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 "androidx.coordinatorlayout:coordinatorlayout:1.1.0"
implementation "com.google.android.material:material:1.4.0" implementation "com.google.android.material:material:1.4.0"
implementation "com.github.wulkanowy:material-chips-input:2.3.1" implementation "com.github.wulkanowy:material-chips-input:2.3.1"
@ -211,11 +220,11 @@ dependencies {
implementation 'me.xdrop:fuzzywuzzy:1.3.1' implementation 'me.xdrop:fuzzywuzzy:1.3.1'
implementation 'com.fredporciuncula:flow-preferences:1.5.0' 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-analytics-ktx'
playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-messaging:'
playImplementation 'com.google.firebase:firebase-crashlytics:' 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' playImplementation 'com.google.android.play:core-ktx:1.8.1'
hmsImplementation 'com.huawei.hms:hianalytics:6.2.0.301' 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"> tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
<activity <activity
android:name=".ui.modules.splash.SplashActivity" android:name=".ui.modules.splash.SplashActivity"
android:exported="true"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:theme="@style/WulkanowyTheme.SplashScreen" android:theme="@style/WulkanowyTheme.SplashScreen"
tools:ignore="LockedOrientationActivity"> tools:ignore="LockedOrientationActivity">
@ -74,6 +75,7 @@
<activity <activity
android:name=".ui.modules.timetablewidget.TimetableWidgetConfigureActivity" android:name=".ui.modules.timetablewidget.TimetableWidgetConfigureActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true"
android:noHistory="true" android:noHistory="true"
android:theme="@style/WulkanowyTheme.WidgetAccountSwitcher"> android:theme="@style/WulkanowyTheme.WidgetAccountSwitcher">
<intent-filter> <intent-filter>
@ -83,6 +85,7 @@
<activity <activity
android:name=".ui.modules.luckynumberwidget.LuckyNumberWidgetConfigureActivity" android:name=".ui.modules.luckynumberwidget.LuckyNumberWidgetConfigureActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true"
android:noHistory="true" android:noHistory="true"
android:theme="@style/WulkanowyTheme.WidgetAccountSwitcher"> android:theme="@style/WulkanowyTheme.WidgetAccountSwitcher">
<intent-filter> <intent-filter>
@ -93,6 +96,22 @@
<service <service
android:name=".services.widgets.TimetableWidgetService" android:name=".services.widgets.TimetableWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" /> 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 <receiver
android:name=".ui.modules.timetablewidget.TimetableWidgetProvider" android:name=".ui.modules.timetablewidget.TimetableWidgetProvider"
@ -107,6 +126,7 @@
</receiver> </receiver>
<receiver <receiver
android:name=".ui.modules.luckynumberwidget.LuckyNumberWidgetProvider" android:name=".ui.modules.luckynumberwidget.LuckyNumberWidgetProvider"
android:exported="true"
android:label="@string/lucky_number_title"> android:label="@string/lucky_number_title">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <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.ChuckerCollector
import com.chuckerteam.chucker.api.ChuckerInterceptor import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.chuckerteam.chucker.api.RetentionManager import com.chuckerteam.chucker.api.RetentionManager
import com.squareup.moshi.Moshi
import com.fredporciuncula.flow.preferences.FlowSharedPreferences import com.fredporciuncula.flow.preferences.FlowSharedPreferences
import com.squareup.moshi.Moshi
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -202,4 +202,8 @@ internal class RepositoryModule {
@Singleton @Singleton
@Provides @Provides
fun provideSchoolAnnouncementDao(database: AppDatabase) = database.schoolAnnouncementDao 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.AttendanceSummaryDao
import io.github.wulkanowy.data.db.dao.CompletedLessonsDao import io.github.wulkanowy.data.db.dao.CompletedLessonsDao
import io.github.wulkanowy.data.db.dao.ConferenceDao 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.ExamDao
import io.github.wulkanowy.data.db.dao.GradeDao import io.github.wulkanowy.data.db.dao.GradeDao
import io.github.wulkanowy.data.db.dao.GradePartialStatisticsDao 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.MessagesDao
import io.github.wulkanowy.data.db.dao.MobileDeviceDao import io.github.wulkanowy.data.db.dao.MobileDeviceDao
import io.github.wulkanowy.data.db.dao.NoteDao 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.RecipientDao
import io.github.wulkanowy.data.db.dao.ReportingUnitDao 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.SchoolDao
import io.github.wulkanowy.data.db.dao.SemesterDao import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao 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.AttendanceSummary
import io.github.wulkanowy.data.db.entities.CompletedLesson import io.github.wulkanowy.data.db.entities.CompletedLesson
import io.github.wulkanowy.data.db.entities.Conference 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.Exam
import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradePartialStatistics 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.MessageAttachment
import io.github.wulkanowy.data.db.entities.MobileDevice import io.github.wulkanowy.data.db.entities.MobileDevice
import io.github.wulkanowy.data.db.entities.Note 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.Recipient
import io.github.wulkanowy.data.db.entities.ReportingUnit import io.github.wulkanowy.data.db.entities.ReportingUnit
import io.github.wulkanowy.data.db.entities.School 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.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentInfo 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.Migration38
import io.github.wulkanowy.data.db.migrations.Migration39 import io.github.wulkanowy.data.db.migrations.Migration39
import io.github.wulkanowy.data.db.migrations.Migration4 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.Migration5
import io.github.wulkanowy.data.db.migrations.Migration6 import io.github.wulkanowy.data.db.migrations.Migration6
import io.github.wulkanowy.data.db.migrations.Migration7 import io.github.wulkanowy.data.db.migrations.Migration7
@ -134,6 +137,7 @@ import javax.inject.Singleton
StudentInfo::class, StudentInfo::class,
TimetableHeader::class, TimetableHeader::class,
SchoolAnnouncement::class, SchoolAnnouncement::class,
Notification::class
], ],
version = AppDatabase.VERSION_SCHEMA, version = AppDatabase.VERSION_SCHEMA,
exportSchema = true exportSchema = true
@ -142,7 +146,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
companion object { companion object {
const val VERSION_SCHEMA = 39 const val VERSION_SCHEMA = 40
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(), Migration2(),
@ -183,6 +187,7 @@ abstract class AppDatabase : RoomDatabase() {
Migration37(), Migration37(),
Migration38(), Migration38(),
Migration39(), Migration39(),
Migration40()
) )
fun newInstance( fun newInstance(
@ -252,4 +257,6 @@ abstract class AppDatabase : RoomDatabase() {
abstract val timetableHeaderDao: TimetableHeaderDao abstract val timetableHeaderDao: TimetableHeaderDao
abstract val schoolAnnouncementDao: SchoolAnnouncementDao 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.services.sync.notifications.NotificationType
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
sealed interface Notification { sealed interface NotificationData {
val type: NotificationType val type: NotificationType
val startMenu: MainView.Section val startMenu: MainView.Section
val icon: Int val icon: Int
@ -14,7 +14,7 @@ sealed interface Notification {
val contentStringRes: Int val contentStringRes: Int
} }
data class MultipleNotifications( data class MultipleNotificationsData(
override val type: NotificationType, override val type: NotificationType,
override val startMenu: MainView.Section, override val startMenu: MainView.Section,
@DrawableRes override val icon: Int, @DrawableRes override val icon: Int,
@ -23,9 +23,9 @@ data class MultipleNotifications(
@PluralsRes val summaryStringRes: Int, @PluralsRes val summaryStringRes: Int,
val lines: List<String>, val lines: List<String>,
) : Notification ) : NotificationData
data class OneNotification( data class OneNotificationData(
override val type: NotificationType, override val type: NotificationType,
override val startMenu: MainView.Section, override val startMenu: MainView.Section,
@DrawableRes override val icon: Int, @DrawableRes override val icon: Int,
@ -33,4 +33,4 @@ data class OneNotification(
@StringRes override val contentStringRes: Int, @StringRes override val contentStringRes: Int,
val contentValues: List<String>, val contentValues: List<String>,
) : Notification ) : NotificationData

View File

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

View File

@ -28,10 +28,28 @@ class CompletedLessonsRepository @Inject constructor(
private val cacheKey = "completed" 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, mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) }, shouldFetch = {
query = { completedLessonsDb.loadAll(semester.studentId, semester.diaryId, start.monday, end.sunday) }, 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 = { fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
.getCompletedLessons(start.monday, end.sunday) .getCompletedLessons(start.monday, end.sunday)

View File

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

View File

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

View File

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

View File

@ -39,9 +39,19 @@ class GradeStatisticsRepository @Inject constructor(
private val semesterCacheKey = "grade_stats_semester" private val semesterCacheKey = "grade_stats_semester"
private val pointsCacheKey = "grade_stats_points" 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, 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) }, query = { gradePartialStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
fetch = { fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) 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, 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) }, query = { gradeSemesterStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
fetch = { fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
@ -94,10 +114,12 @@ class GradeStatisticsRepository @Inject constructor(
val itemsWithAverage = items.map { item -> val itemsWithAverage = items.map { item ->
item.copy().apply { item.copy().apply {
val denominator = item.amounts.sum() val denominator = item.amounts.sum()
average = if (denominator == 0) "" else (item.amounts.mapIndexed { gradeValue, amount -> average = if (denominator == 0) "" else {
(gradeValue + 1) * amount (item.amounts.mapIndexed { gradeValue, amount ->
}.sum().toDouble() / denominator).let { (gradeValue + 1) * amount
"%.2f".format(Locale.FRANCE, it) }.sum().toDouble() / denominator).let {
"%.2f".format(Locale.FRANCE, it)
}
} }
} }
} }
@ -109,7 +131,9 @@ class GradeStatisticsRepository @Inject constructor(
amounts = itemsWithAverage.map { it.amounts }.sumGradeAmounts(), amounts = itemsWithAverage.map { it.amounts }.sumGradeAmounts(),
studentGrade = 0 studentGrade = 0
).apply { ).apply {
average = itemsWithAverage.mapNotNull { it.average.replace(",", ".").toDoubleOrNull() }.average().let { average = itemsWithAverage.mapNotNull {
it.average.replace(",", ".").toDoubleOrNull()
}.average().let {
"%.2f".format(Locale.FRANCE, it) "%.2f".format(Locale.FRANCE, it)
} }
}).reversed() }).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, 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) }, query = { gradePointsStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
fetch = { fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)

View File

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

View File

@ -23,11 +23,17 @@ class LuckyNumberRepository @Inject constructor(
private val saveFetchResultMutex = Mutex() 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, mutex = saveFetchResultMutex,
shouldFetch = { it == null || forceRefresh }, shouldFetch = { it == null || forceRefresh },
query = { luckyNumberDb.load(student.studentId, now()) }, 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 -> saveFetchResult = { old, new ->
if (new != old) { if (new != old) {
old?.let { luckyNumberDb.deleteAll(listOfNotNull(it)) } old?.let { luckyNumberDb.deleteAll(listOfNotNull(it)) }
@ -41,9 +47,11 @@ class LuckyNumberRepository @Inject constructor(
fun getLuckyNumberHistory(student: Student, start: LocalDate, end: LocalDate) = fun getLuckyNumberHistory(student: Student, start: LocalDate, end: LocalDate) =
luckyNumberDb.getAll(student.studentId, start, end) luckyNumberDb.getAll(student.studentId, start, end)
suspend fun getNotNotifiedLuckyNumber(student: Student) = luckyNumberDb.load(student.studentId, now()).map { suspend fun getNotNotifiedLuckyNumber(student: Student) =
if (it?.isNotified == false) it else null luckyNumberDb.load(student.studentId, now()).map {
}.first() 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") @Suppress("UNUSED_PARAMETER")
fun getMessages( fun getMessages(
student: Student, semester: Semester, student: Student,
folder: MessageFolder, forceRefresh: Boolean, notify: Boolean = false semester: Semester,
folder: MessageFolder,
forceRefresh: Boolean,
notify: Boolean = false,
): Flow<Resource<List<Message>>> = networkBoundResource( ): Flow<Resource<List<Message>>> = networkBoundResource(
mutex = saveFetchResultMutex, mutex = saveFetchResultMutex,
shouldFetch = { shouldFetch = {
it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed( val isExpired = refreshHelper.shouldBeRefreshed(
getRefreshKey(cacheKey, student, folder) key = getRefreshKey(cacheKey, student, folder)
) )
it.isEmpty() || forceRefresh || isExpired
}, },
query = { messagesDb.loadAll(student.id.toInt(), folder.id) }, query = { messagesDb.loadAll(student.id.toInt(), folder.id) },
fetch = { fetch = {
@ -77,7 +81,8 @@ class MessageRepository @Inject constructor(
) )
private fun getMessagesWithReadByChange( private fun getMessagesWithReadByChange(
old: List<Message>, new: List<Message>, old: List<Message>,
new: List<Message>,
setNotified: Boolean setNotified: Boolean
): List<Message> { ): List<Message> {
val oldMeta = old.map { Triple(it, it.readBy, it.unreadBy) } val oldMeta = old.map { Triple(it, it.readBy, it.unreadBy) }
@ -96,7 +101,9 @@ class MessageRepository @Inject constructor(
} }
fun getMessage( fun getMessage(
student: Student, message: Message, markAsRead: Boolean = false student: Student,
message: Message,
markAsRead: Boolean = false,
): Flow<Resource<MessageWithAttachment?>> = networkBoundResource( ): Flow<Resource<MessageWithAttachment?>> = networkBoundResource(
shouldFetch = { shouldFetch = {
checkNotNull(it, { "This message no longer exist!" }) checkNotNull(it, { "This message no longer exist!" })
@ -135,8 +142,10 @@ class MessageRepository @Inject constructor(
} }
suspend fun sendMessage( suspend fun sendMessage(
student: Student, subject: String, content: String, student: Student,
recipients: List<Recipient> subject: String,
content: String,
recipients: List<Recipient>,
): SentMessage = sdk.init(student).sendMessage( ): SentMessage = sdk.init(student).sendMessage(
subject = subject, subject = subject,
content = content, content = content,

View File

@ -28,9 +28,16 @@ class MobileDeviceRepository @Inject constructor(
private val cacheKey = "devices" private val cacheKey = "devices"
fun getDevices(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource( fun getDevices(
student: Student,
semester: Semester,
forceRefresh: Boolean,
) = networkBoundResource(
mutex = saveFetchResultMutex, 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) }, query = { mobileDb.loadAll(student.userLoginId.takeIf { it != 0 } ?: student.studentId) },
fetch = { fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) 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.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -28,9 +27,19 @@ class NoteRepository @Inject constructor(
private val cacheKey = "note" 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, 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) }, query = { noteDb.loadAll(student.studentId) },
fetch = { fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) 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.dashboard.DashboardItem
import io.github.wulkanowy.ui.modules.grade.GradeAverageMode import io.github.wulkanowy.ui.modules.grade.GradeAverageMode
import io.github.wulkanowy.ui.modules.grade.GradeSortingMode 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.toLocalDateTime
import io.github.wulkanowy.utils.toTimestamp import io.github.wulkanowy.utils.toTimestamp
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -108,6 +107,22 @@ class PreferencesRepository @Inject constructor(
R.bool.pref_default_notification_upcoming_lessons_enable 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 isDebugNotificationEnableKey = context.getString(R.string.pref_key_notification_debug)
val isDebugNotificationEnable: Boolean val isDebugNotificationEnable: Boolean
get() = getBoolean(isDebugNotificationEnableKey, R.bool.pref_default_notification_debug) get() = getBoolean(isDebugNotificationEnableKey, R.bool.pref_default_notification_debug)
@ -176,10 +191,8 @@ class PreferencesRepository @Inject constructor(
) )
var lasSyncDate: LocalDateTime var lasSyncDate: LocalDateTime
get() = getLong( get() = getLong(R.string.pref_key_last_sync_date, R.string.pref_default_last_sync_date)
R.string.pref_key_last_sync_date, .toLocalDateTime()
R.string.pref_default_last_sync_date
).toLocalDateTime()
set(value) = sharedPref.edit().putLong("last_sync_date", value.toTimestamp()).apply() set(value) = sharedPref.edit().putLong("last_sync_date", value.toTimestamp()).apply()
var dashboardItemsPosition: Map<DashboardItem.Type, Int>? 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() set(value) = sharedPref.edit().putInt(PREF_KEY_IN_APP_REVIEW_COUNT, value).apply()
var inAppReviewDate: LocalDate? var inAppReviewDate: LocalDate?
get() = sharedPref.getLong(PREF_KEY_IN_APP_REVIEW_DATE, 0).takeIf { it != 0L }?.toLocalDate() get() = sharedPref.getLong(PREF_KEY_IN_APP_REVIEW_DATE, 0).takeIf { it != 0L }
set(value) = sharedPref.edit().putLong(PREF_KEY_IN_APP_REVIEW_DATE, value!!.toTimestamp()).apply() ?.toLocalDate()
set(value) = sharedPref.edit().putLong(PREF_KEY_IN_APP_REVIEW_DATE, value!!.toTimestamp())
.apply()
var isAppReviewDone: Boolean var isAppReviewDone: Boolean
get() = sharedPref.getBoolean(PREF_KEY_IN_APP_REVIEW_DONE, false) 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.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.sdk.Sdk 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.init
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import javax.inject.Inject import javax.inject.Inject
@ -15,26 +17,34 @@ import javax.inject.Singleton
@Singleton @Singleton
class RecipientRepository @Inject constructor( class RecipientRepository @Inject constructor(
private val recipientDb: RecipientDao, 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) { suspend fun refreshRecipients(student: Student, unit: ReportingUnit, role: Int) {
val new = sdk.init(student).getRecipients(unit.unitId, role).mapToEntities(unit.studentId) val new = sdk.init(student).getRecipients(unit.unitId, role).mapToEntities(unit.studentId)
val old = recipientDb.loadAll(unit.studentId, unit.unitId, role) val old = recipientDb.loadAll(unit.studentId, unit.unitId, role)
recipientDb.deleteAll(old uniqueSubtract new) recipientDb.deleteAll(old uniqueSubtract new)
recipientDb.insertAll(new uniqueSubtract old) recipientDb.insertAll(new uniqueSubtract old)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
} }
suspend fun getRecipients(student: Student, unit: ReportingUnit, role: Int): List<Recipient> { suspend fun getRecipients(student: Student, unit: ReportingUnit, role: Int): List<Recipient> {
return recipientDb.loadAll(unit.studentId, unit.unitId, role).ifEmpty { val cached = recipientDb.loadAll(unit.studentId, unit.unitId, role)
refreshRecipients(student, unit, role)
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student))
return if (cached.isEmpty() || isExpired) {
refreshRecipients(student, unit, role)
recipientDb.loadAll(unit.studentId, unit.unitId, role) recipientDb.loadAll(unit.studentId, unit.unitId, role)
} } else cached
} }
suspend fun getMessageRecipients(student: Student, message: Message): List<Recipient> { 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) return sdk.getPasswordResetCaptchaCode(host, symbol)
} }
suspend fun sendRecoverRequest(url: String, symbol: String, email: String, reCaptchaResponse: String): String { suspend fun sendRecoverRequest(
return sdk.sendPasswordResetRequest(url, symbol, email, reCaptchaResponse) 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.dao.SchoolAnnouncementDao
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement 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.Student
import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.sdk.Sdk 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.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -30,17 +28,15 @@ class SchoolAnnouncementRepository @Inject constructor(
fun getSchoolAnnouncements( fun getSchoolAnnouncements(
student: Student, student: Student,
forceRefresh: Boolean, forceRefresh: Boolean, notify: Boolean = false
notify: Boolean = false
) = networkBoundResource( ) = networkBoundResource(
mutex = saveFetchResultMutex, mutex = saveFetchResultMutex,
shouldFetch = { shouldFetch = {
it.isEmpty() || forceRefresh val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student))
|| refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, student)) it.isEmpty() || forceRefresh || isExpired
}, },
query = { query = {
schoolAnnouncementDb.loadAll( schoolAnnouncementDb.loadAll(student.studentId)
student.studentId)
}, },
fetch = { fetch = {
sdk.init(student) sdk.init(student)
@ -57,9 +53,11 @@ class SchoolAnnouncementRepository @Inject constructor(
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
} }
) )
fun getSchoolAnnouncementFromDatabase(student: Student): Flow<List<SchoolAnnouncement>> { fun getSchoolAnnouncementFromDatabase(student: Student): Flow<List<SchoolAnnouncement>> {
return schoolAnnouncementDb.loadAll(student.studentId) 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.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntity import io.github.wulkanowy.data.mappers.mapToEntity
import io.github.wulkanowy.sdk.Sdk 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.init
import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.networkBoundResource
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
@ -14,29 +16,41 @@ import javax.inject.Singleton
@Singleton @Singleton
class SchoolRepository @Inject constructor( class SchoolRepository @Inject constructor(
private val schoolDb: SchoolDao, private val schoolDb: SchoolDao,
private val sdk: Sdk private val sdk: Sdk,
private val refreshHelper: AutoRefreshHelper,
) { ) {
private val saveFetchResultMutex = Mutex() private val saveFetchResultMutex = Mutex()
fun getSchoolInfo(student: Student, semester: Semester, forceRefresh: Boolean) = private val cacheKey = "school_info"
networkBoundResource(
mutex = saveFetchResultMutex, fun getSchoolInfo(
shouldFetch = { it == null || forceRefresh }, student: Student,
query = { schoolDb.load(semester.studentId, semester.classId) }, semester: Semester,
fetch = { forceRefresh: Boolean,
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear).getSchool() ) = networkBoundResource(
.mapToEntity(semester) mutex = saveFetchResultMutex,
}, shouldFetch = {
saveFetchResult = { old, new -> val isExpired = refreshHelper.shouldBeRefreshed(
if (old != null && new != old) { key = getRefreshKey(cacheKey, student)
with(schoolDb) { )
deleteAll(listOf(old)) it == null || forceRefresh || isExpired
insertAll(listOf(new)) },
} query = { schoolDb.load(semester.studentId, semester.classId) },
} else if (old == null) { fetch = {
schoolDb.insertAll(listOf(new)) 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() private val saveFetchResultMutex = Mutex()
fun getStudentInfo(student: Student, semester: Semester, forceRefresh: Boolean) = fun getStudentInfo(
networkBoundResource( student: Student,
mutex = saveFetchResultMutex, semester: Semester,
shouldFetch = { it == null || forceRefresh }, forceRefresh: Boolean,
query = { studentInfoDao.loadStudentInfo(student.studentId) }, ) = networkBoundResource(
fetch = { mutex = saveFetchResultMutex,
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) shouldFetch = { it == null || forceRefresh },
.getStudentInfo().mapToEntity(semester) query = { studentInfoDao.loadStudentInfo(student.studentId) },
}, fetch = {
saveFetchResult = { old, new -> sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
if (old != null && new != old) { .getStudentInfo().mapToEntity(semester)
with(studentInfoDao) { },
deleteAll(listOf(old)) saveFetchResult = { old, new ->
insertAll(listOf(new)) if (old != null && new != old) {
} with(studentInfoDao) {
} else if (old == null) { deleteAll(listOf(old))
studentInfoDao.insertAll(listOf(new)) 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.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.sdk.Sdk 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.init
import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
@ -15,14 +17,24 @@ import javax.inject.Singleton
@Singleton @Singleton
class SubjectRepository @Inject constructor( class SubjectRepository @Inject constructor(
private val subjectDao: SubjectDao, private val subjectDao: SubjectDao,
private val sdk: Sdk private val sdk: Sdk,
private val refreshHelper: AutoRefreshHelper,
) { ) {
private val saveFetchResultMutex = Mutex() 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, 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) }, query = { subjectDao.loadAll(semester.diaryId, semester.studentId) },
fetch = { fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
@ -31,6 +43,8 @@ class SubjectRepository @Inject constructor(
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
subjectDao.deleteAll(old uniqueSubtract new) subjectDao.deleteAll(old uniqueSubtract new)
subjectDao.insertAll(new uniqueSubtract old) 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.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.sdk.Sdk 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.init
import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
@ -15,14 +17,24 @@ import javax.inject.Singleton
@Singleton @Singleton
class TeacherRepository @Inject constructor( class TeacherRepository @Inject constructor(
private val teacherDb: TeacherDao, private val teacherDb: TeacherDao,
private val sdk: Sdk private val sdk: Sdk,
private val refreshHelper: AutoRefreshHelper,
) { ) {
private val saveFetchResultMutex = Mutex() 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, 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) }, query = { teacherDb.loadAll(semester.studentId, semester.classId) },
fetch = { fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
@ -32,6 +44,8 @@ class TeacherRepository @Inject constructor(
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
teacherDb.deleteAll(old uniqueSubtract new) teacherDb.deleteAll(old uniqueSubtract new)
teacherDb.insertAll(new uniqueSubtract old) teacherDb.insertAll(new uniqueSubtract old)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
} }
) )
} }

View File

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

View File

@ -11,6 +11,7 @@ import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.services.HiltBroadcastReceiver import io.github.wulkanowy.services.HiltBroadcastReceiver
import io.github.wulkanowy.services.sync.channels.UpcomingLessonsChannel.Companion.CHANNEL_ID import io.github.wulkanowy.services.sync.channels.UpcomingLessonsChannel.Companion.CHANNEL_ID
@ -32,6 +33,9 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() {
@Inject @Inject
lateinit var studentRepository: StudentRepository lateinit var studentRepository: StudentRepository
@Inject
lateinit var preferencesRepository: PreferencesRepository
companion object { companion object {
const val NOTIFICATION_TYPE_CURRENT = 1 const val NOTIFICATION_TYPE_CURRENT = 1
const val NOTIFICATION_TYPE_UPCOMING = 2 const val NOTIFICATION_TYPE_UPCOMING = 2
@ -68,6 +72,7 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() {
private fun prepareNotification(context: Context, intent: Intent) { private fun prepareNotification(context: Context, intent: Intent) {
val type = intent.getIntExtra(LESSON_TYPE, 0) val type = intent.getIntExtra(LESSON_TYPE, 0)
val notificationId = intent.getIntExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id) val notificationId = intent.getIntExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id)
val isPersistent = preferencesRepository.isUpcomingLessonsNotificationsPersistent
if (type == NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION) { if (type == NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION) {
return NotificationManagerCompat.from(context).cancel(notificationId) 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") 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, 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("()")), context.getString(
nextSubject?.let { context.getString(R.string.timetable_later, "($nextRoom) $nextSubject".removePrefix("()")) } 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?) { private fun showNotification(
NotificationManagerCompat.from(context).notify(notificationId, NotificationCompat.Builder(context, CHANNEL_ID) context: Context,
.setContentTitle(title) notificationId: Int,
.setContentText(next) isPersistent: Boolean,
.setAutoCancel(false) studentName: String?,
.setOngoing(true) countDown: Long,
.setWhen(countDown) timeout: Long,
.apply { title: String,
if (Build.VERSION.SDK_INT >= N) setUsesChronometer(true) next: String?
} ) {
.setTimeoutAfter(timeout) NotificationManagerCompat.from(context)
.setSmallIcon(R.drawable.ic_stat_timetable) .notify(notificationId, NotificationCompat.Builder(context, CHANNEL_ID)
.setColor(context.getCompatColor(R.color.colorPrimary)) .setContentTitle(title)
.setStyle(NotificationCompat.InboxStyle().also { .setContentText(next)
it.setSummaryText(studentName) .setAutoCancel(false)
it.addLine(next) .setWhen(countDown)
}) .setOngoing(isPersistent)
.setContentIntent(PendingIntent.getActivity(context, MainView.Section.TIMETABLE.id, .apply {
MainActivity.getStartIntent(context, MainView.Section.TIMETABLE, true), FLAG_UPDATE_CURRENT)) if (Build.VERSION.SDK_INT >= N) setUsesChronometer(true)
.build() }
) .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 io.github.wulkanowy.utils.toTimestamp
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.LocalDateTime.now import java.time.LocalDateTime.now
import javax.inject.Inject import javax.inject.Inject
@ -57,10 +58,13 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
lessons.sortedBy { it.start }.forEachIndexed { index, lesson -> lessons.sortedBy { it.start }.forEachIndexed { index, lesson ->
val upcomingTime = getUpcomingLessonTime(index, lessons, lesson) val upcomingTime = getUpcomingLessonTime(index, lessons, lesson)
cancelScheduledTo( cancelScheduledTo(
upcomingTime..lesson.start, range = upcomingTime..lesson.start,
getRequestCode(upcomingTime, studentId) 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") 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) 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) { withContext(dispatchersProvider.backgroundThread) {
lessons.groupBy { it.date } lessons.groupBy { it.date }
.map { it.value.sortedBy { lesson -> lesson.start } } .map { it.value.sortedBy { lesson -> lesson.start } }
@ -96,26 +105,26 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
if (lesson.start > now()) { if (lesson.start > now()) {
scheduleBroadcast( scheduleBroadcast(
intent, intent = intent,
student.studentId, studentId = student.studentId,
NOTIFICATION_TYPE_UPCOMING, notificationType = NOTIFICATION_TYPE_UPCOMING,
getUpcomingLessonTime(index, active, lesson) time = getUpcomingLessonTime(index, active, lesson)
) )
} }
if (lesson.end > now()) { if (lesson.end > now()) {
scheduleBroadcast( scheduleBroadcast(
intent, intent = intent,
student.studentId, studentId = student.studentId,
NOTIFICATION_TYPE_CURRENT, notificationType = NOTIFICATION_TYPE_CURRENT,
lesson.start time = lesson.start
) )
if (active.lastIndex == index) { if (active.lastIndex == index) {
scheduleBroadcast( scheduleBroadcast(
intent, intent = intent,
student.studentId, studentId = student.studentId,
NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION, notificationType = NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION,
lesson.end time = lesson.end
) )
} }
} }
@ -143,17 +152,21 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
notificationType: Int, notificationType: Int,
time: LocalDateTime time: LocalDateTime
) { ) {
AlarmManagerCompat.setExactAndAllowWhileIdle( try {
alarmManager, RTC_WAKEUP, time.toTimestamp(), AlarmManagerCompat.setExactAndAllowWhileIdle(
PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also { alarmManager, RTC_WAKEUP, time.toTimestamp(),
it.putExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id) PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also {
it.putExtra(LESSON_TYPE, notificationType) it.putExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id)
}, FLAG_UPDATE_CURRENT) it.putExtra(LESSON_TYPE, notificationType)
) }, FLAG_UPDATE_CURRENT)
Timber.d( )
"TimetableNotification scheduled: type: $notificationType, subject: ${ Timber.d(
intent.getStringExtra(LESSON_TITLE) "TimetableNotification scheduled: type: $notificationType, subject: ${
}, start: $time, student: $studentId" 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) { fun startPeriodicSyncWorker(restart: Boolean = false) {
if (preferencesRepository.isServiceEnabled && !now().isHolidays) { if (preferencesRepository.isServiceEnabled && !now().isHolidays) {
workManager.enqueueUniquePeriodicWork(SyncWorker::class.java.simpleName, if (restart) REPLACE else KEEP, val serviceInterval = preferencesRepository.servicesInterval
PeriodicWorkRequestBuilder<SyncWorker>(preferencesRepository.servicesInterval, MINUTES)
workManager.enqueueUniquePeriodicWork(
SyncWorker::class.java.simpleName, if (restart) REPLACE else KEEP,
PeriodicWorkRequestBuilder<SyncWorker>(serviceInterval, MINUTES)
.setInitialDelay(10, MINUTES) .setInitialDelay(10, MINUTES)
.setBackoffCriteria(EXPONENTIAL, 30, MINUTES) .setBackoffCriteria(EXPONENTIAL, 30, MINUTES)
.setConstraints(Constraints.Builder() .setConstraints(
.setRequiredNetworkType(if (preferencesRepository.isServicesOnlyWifi) UNMETERED else CONNECTED) Constraints.Builder()
.build()) .setRequiredNetworkType(if (preferencesRepository.isServicesOnlyWifi) UNMETERED else CONNECTED)
.build()) .build()
)
.build()
)
} }
} }
@ -77,7 +83,11 @@ class SyncManager @Inject constructor(
) )
.build() .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() 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 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.R
import io.github.wulkanowy.data.db.entities.Conference import io.github.wulkanowy.data.db.entities.Conference
import io.github.wulkanowy.data.db.entities.Student 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.ui.modules.main.MainView
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
import java.time.LocalDateTime import java.time.LocalDateTime
import javax.inject.Inject import javax.inject.Inject
class NewConferenceNotification @Inject constructor( class NewConferenceNotification @Inject constructor(
@ApplicationContext private val context: Context, private val appNotificationManager: AppNotificationManager
notificationManager: NotificationManagerCompat, ) {
) : BaseNotification(context, notificationManager) {
fun notify(items: List<Conference>, student: Student) { suspend fun notify(items: List<Conference>, student: Student) {
val today = LocalDateTime.now() val today = LocalDateTime.now()
val lines = items.filter { !it.date.isBefore(today) }.map { val lines = items.filter { !it.date.isBefore(today) }.map {
"${it.date.toFormattedString("dd.MM")} - ${it.title}: ${it.subject}" "${it.date.toFormattedString("dd.MM")} - ${it.title}: ${it.subject}"
}.ifEmpty { return } }.ifEmpty { return }
val notification = MultipleNotifications( val notification = MultipleNotificationsData(
type = NotificationType.NEW_CONFERENCE, type = NotificationType.NEW_CONFERENCE,
icon = R.drawable.ic_more_conferences, icon = R.drawable.ic_more_conferences,
titleStringRes = R.plurals.conference_notify_new_item_title, titleStringRes = R.plurals.conference_notify_new_item_title,
@ -33,6 +29,6 @@ class NewConferenceNotification @Inject constructor(
lines = lines lines = lines
) )
sendNotification(notification, student) appNotificationManager.sendNotification(notification, student)
} }
} }

View File

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

View File

@ -1,23 +1,19 @@
package io.github.wulkanowy.services.sync.notifications 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.R
import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Student 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.ui.modules.main.MainView
import javax.inject.Inject import javax.inject.Inject
class NewGradeNotification @Inject constructor( class NewGradeNotification @Inject constructor(
@ApplicationContext private val context: Context, private val appNotificationManager: AppNotificationManager
notificationManager: NotificationManagerCompat, ) {
) : BaseNotification(context, notificationManager) {
fun notifyDetails(items: List<Grade>, student: Student) { suspend fun notifyDetails(items: List<Grade>, student: Student) {
val notification = MultipleNotifications( val notification = MultipleNotificationsData(
type = NotificationType.NEW_GRADE_DETAILS, type = NotificationType.NEW_GRADE_DETAILS,
icon = R.drawable.ic_stat_grade, icon = R.drawable.ic_stat_grade,
titleStringRes = R.plurals.grade_new_items, 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) { suspend fun notifyPredicted(items: List<GradeSummary>, student: Student) {
val notification = MultipleNotifications( val notification = MultipleNotificationsData(
type = NotificationType.NEW_GRADE_PREDICTED, type = NotificationType.NEW_GRADE_PREDICTED,
icon = R.drawable.ic_stat_grade, icon = R.drawable.ic_stat_grade,
titleStringRes = R.plurals.grade_new_items_predicted, 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) { suspend fun notifyFinal(items: List<GradeSummary>, student: Student) {
val notification = MultipleNotifications( val notification = MultipleNotificationsData(
type = NotificationType.NEW_GRADE_FINAL, type = NotificationType.NEW_GRADE_FINAL,
icon = R.drawable.ic_stat_grade, icon = R.drawable.ic_stat_grade,
titleStringRes = R.plurals.grade_new_items_final, 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 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.R
import io.github.wulkanowy.data.db.entities.Homework import io.github.wulkanowy.data.db.entities.Homework
import io.github.wulkanowy.data.db.entities.Student 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.ui.modules.main.MainView
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
import java.time.LocalDate import java.time.LocalDate
import javax.inject.Inject import javax.inject.Inject
class NewHomeworkNotification @Inject constructor( class NewHomeworkNotification @Inject constructor(
@ApplicationContext private val context: Context, private val appNotificationManager: AppNotificationManager
notificationManager: NotificationManagerCompat, ) {
) : BaseNotification(context, notificationManager) {
fun notify(items: List<Homework>, student: Student) { suspend fun notify(items: List<Homework>, student: Student) {
val today = LocalDate.now() val today = LocalDate.now()
val lines = items.filter { !it.date.isBefore(today) }.map { val lines = items.filter { !it.date.isBefore(today) }.map {
"${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.content}" "${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.content}"
}.ifEmpty { return } }.ifEmpty { return }
val notification = MultipleNotifications( val notification = MultipleNotificationsData(
type = NotificationType.NEW_HOMEWORK, type = NotificationType.NEW_HOMEWORK,
icon = R.drawable.ic_more_homework, icon = R.drawable.ic_more_homework,
titleStringRes = R.plurals.homework_notify_new_item_title, titleStringRes = R.plurals.homework_notify_new_item_title,
@ -33,6 +29,6 @@ class NewHomeworkNotification @Inject constructor(
lines = lines lines = lines
) )
sendNotification(notification, student) appNotificationManager.sendNotification(notification, student)
} }
} }

View File

@ -1,30 +1,26 @@
package io.github.wulkanowy.services.sync.notifications 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.R
import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Student 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 io.github.wulkanowy.ui.modules.main.MainView
import javax.inject.Inject import javax.inject.Inject
class NewLuckyNumberNotification @Inject constructor( class NewLuckyNumberNotification @Inject constructor(
@ApplicationContext private val context: Context, private val appNotificationManager: AppNotificationManager
notificationManager: NotificationManagerCompat, ) {
) : BaseNotification(context, notificationManager) {
fun notify(item: LuckyNumber, student: Student) { suspend fun notify(item: LuckyNumber, student: Student) {
val notification = OneNotification( val notification = OneNotificationData(
type = NotificationType.NEW_LUCKY_NUMBER, type = NotificationType.NEW_LUCKY_NUMBER,
icon = R.drawable.ic_stat_luckynumber, icon = R.drawable.ic_stat_luckynumber,
titleStringRes = R.string.lucky_number_notify_new_item_title, titleStringRes = R.string.lucky_number_notify_new_item_title,
contentStringRes = R.string.lucky_number_notify_new_item, contentStringRes = R.string.lucky_number_notify_new_item,
startMenu = MainView.Section.LUCKY_NUMBER, startMenu = MainView.Section.LUCKY_NUMBER,
contentValues = listOf(item.luckyNumber.toString()) 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 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.R
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.Student 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.ui.modules.main.MainView
import javax.inject.Inject import javax.inject.Inject
class NewMessageNotification @Inject constructor( class NewMessageNotification @Inject constructor(
@ApplicationContext private val context: Context, private val appNotificationManager: AppNotificationManager
notificationManager: NotificationManagerCompat, ) {
) : BaseNotification(context, notificationManager) {
fun notify(items: List<Message>, student: Student) { suspend fun notify(items: List<Message>, student: Student) {
val notification = MultipleNotifications( val notification = MultipleNotificationsData(
type = NotificationType.NEW_MESSAGE, type = NotificationType.NEW_MESSAGE,
icon = R.drawable.ic_stat_message, icon = R.drawable.ic_stat_message,
titleStringRes = R.plurals.message_new_items, 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 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.R
import io.github.wulkanowy.data.db.entities.Note import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.db.entities.Student 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.sdk.scrapper.notes.NoteCategory
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import javax.inject.Inject import javax.inject.Inject
class NewNoteNotification @Inject constructor( class NewNoteNotification @Inject constructor(
@ApplicationContext private val context: Context, private val appNotificationManager: AppNotificationManager
notificationManager: NotificationManagerCompat, ) {
) : BaseNotification(context, notificationManager) {
fun notify(items: List<Note>, student: Student) { suspend fun notify(items: List<Note>, student: Student) {
val notification = MultipleNotifications( val notification = MultipleNotificationsData(
type = NotificationType.NEW_NOTE, type = NotificationType.NEW_NOTE,
icon = R.drawable.ic_stat_note, icon = R.drawable.ic_stat_note,
titleStringRes = when (NoteCategory.getByValue(items.first().categoryType)) { 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 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.R
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.data.db.entities.Student 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.ui.modules.main.MainView
import javax.inject.Inject import javax.inject.Inject
class NewSchoolAnnouncementNotification @Inject constructor( class NewSchoolAnnouncementNotification @Inject constructor(
@ApplicationContext private val context: Context, private val appNotificationManager: AppNotificationManager
notificationManager: NotificationManagerCompat, ) {
) : BaseNotification(context, notificationManager) {
fun notify(items: List<SchoolAnnouncement>, student: Student) { suspend fun notify(items: List<SchoolAnnouncement>, student: Student) {
val notification = MultipleNotifications( val notification = MultipleNotificationsData(
type = NotificationType.NEW_ANNOUNCEMENT, type = NotificationType.NEW_ANNOUNCEMENT,
icon = R.drawable.ic_all_about, icon = R.drawable.ic_all_about,
titleStringRes = R.plurals.school_announcement_notify_new_item_title, titleStringRes = R.plurals.school_announcement_notify_new_item_title,
contentStringRes = R.plurals.school_announcement_notify_new_items, contentStringRes = R.plurals.school_announcement_notify_new_items,
summaryStringRes = R.plurals.school_announcement_number_item, summaryStringRes = R.plurals.school_announcement_number_item,
startMenu = MainView.Section.SCHOOL_ANNOUNCEMENT, startMenu = MainView.Section.SCHOOL_ANNOUNCEMENT,
lines = items.map { lines = items.map {
"${it.subject}: ${it.content}" "${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.NewMessagesChannel
import io.github.wulkanowy.services.sync.channels.NewNotesChannel import io.github.wulkanowy.services.sync.channels.NewNotesChannel
import io.github.wulkanowy.services.sync.channels.NewSchoolAnnouncementsChannel 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_CONFERENCE("new_conferences_group", NewConferencesChannel.CHANNEL_ID),
NEW_EXAM("new_exam_group", NewExamChannel.CHANNEL_ID), NEW_EXAM("new_exam_group", NewExamChannel.CHANNEL_ID),
NEW_GRADE_DETAILS("new_grade_details_group", NewGradesChannel.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_MESSAGE("new_message_group", NewMessagesChannel.CHANNEL_ID),
NEW_NOTE("new_notes_group", NewNotesChannel.CHANNEL_ID), NEW_NOTE("new_notes_group", NewNotesChannel.CHANNEL_ID),
NEW_ANNOUNCEMENT("new_school_announcements_group", NewSchoolAnnouncementsChannel.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 java.time.LocalDate.now
import javax.inject.Inject 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) { 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) (requireActivity() as MainActivity).popView(2)
} }
override fun popViewToAccounts() {
(requireActivity() as MainActivity).popView(1)
}
override fun recreateMainView() { override fun recreateMainView() {
requireActivity().recreate() requireActivity().recreate()
} }

View File

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

View File

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

View File

@ -14,6 +14,8 @@ class ConferenceAdapter @Inject constructor() :
var items = emptyList<Conference>() var items = emptyList<Conference>()
var onItemClickListener: (Conference) -> Unit = {}
override fun getItemCount() = items.size override fun getItemCount() = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder(
@ -28,7 +30,10 @@ class ConferenceAdapter @Inject constructor() :
conferenceItemTitle.text = item.title conferenceItemTitle.text = item.title
conferenceItemSubject.text = item.subject conferenceItemSubject.text = item.subject
conferenceItemContent.text = item.agenda 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.data.db.entities.Conference
import io.github.wulkanowy.databinding.FragmentConferenceBinding import io.github.wulkanowy.databinding.FragmentConferenceBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.widgets.DividerItemDecoration import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.getThemeAttrColor
@ -41,6 +42,8 @@ class ConferenceFragment : BaseFragment<FragmentConferenceBinding>(R.layout.frag
} }
override fun initView() { override fun initView() {
conferencesAdapter.onItemClickListener = presenter::onItemSelected
with(binding.conferenceRecycler) { with(binding.conferenceRecycler) {
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
adapter = conferencesAdapter adapter = conferencesAdapter
@ -50,7 +53,11 @@ class ConferenceFragment : BaseFragment<FragmentConferenceBinding>(R.layout.frag
with(binding) { with(binding) {
conferenceSwipe.setOnRefreshListener(presenter::onSwipeRefresh) conferenceSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
conferenceSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary)) 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() } conferenceErrorRetry.setOnClickListener { presenter.onRetry() }
conferenceErrorDetails.setOnClickListener { presenter.onDetailsClick() } 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 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() { override fun onDestroyView() {
presenter.onDetachView() presenter.onDetachView()
super.onDestroyView() super.onDestroyView()

View File

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

View File

@ -26,4 +26,6 @@ interface ConferenceView : BaseView {
fun enableSwipe(enable: Boolean) fun enableSwipe(enable: Boolean)
fun showContent(show: 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 onAttendanceTileClickListener: () -> Unit = {}
var onLessonsTileClickListener: () -> Unit = {} var onLessonsTileClickListener: (LocalDate) -> Unit = {}
var onHomeworkTileClickListener: () -> Unit = {} var onHomeworkTileClickListener: () -> Unit = {}
@ -275,10 +275,12 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
val item = items[position] as DashboardItem.Lessons val item = items[position] as DashboardItem.Lessons
val timetableFull = item.lessons val timetableFull = item.lessons
val binding = lessonsViewHolder.binding val binding = lessonsViewHolder.binding
var dateToNavigate = LocalDate.now()
fun updateLessonState() { fun updateLessonState() {
val currentDateTime = LocalDateTime.now() val currentDateTime = LocalDateTime.now()
val currentDate = LocalDate.now() val currentDate = LocalDate.now()
val tomorrowDate = currentDate.plusDays(1)
val currentTimetable = timetableFull?.lessons val currentTimetable = timetableFull?.lessons
.orEmpty() .orEmpty()
@ -296,22 +298,27 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
when { when {
currentTimetable.isNotEmpty() -> { currentTimetable.isNotEmpty() -> {
dateToNavigate = currentDate
updateLessonView(item, currentTimetable, binding) updateLessonView(item, currentTimetable, binding)
binding.dashboardLessonsItemTitleTomorrow.isVisible = false binding.dashboardLessonsItemTitleTomorrow.isVisible = false
} }
tomorrowTimetable.isNotEmpty() -> { tomorrowTimetable.isNotEmpty() -> {
dateToNavigate = tomorrowDate
updateLessonView(item, tomorrowTimetable, binding) updateLessonView(item, tomorrowTimetable, binding)
binding.dashboardLessonsItemTitleTomorrow.isVisible = true binding.dashboardLessonsItemTitleTomorrow.isVisible = true
} }
currentDayHeader != null && currentDayHeader.content.isNotBlank() -> { currentDayHeader != null && currentDayHeader.content.isNotBlank() -> {
dateToNavigate = currentDate
updateLessonView(item, emptyList(), binding, currentDayHeader) updateLessonView(item, emptyList(), binding, currentDayHeader)
binding.dashboardLessonsItemTitleTomorrow.isVisible = false binding.dashboardLessonsItemTitleTomorrow.isVisible = false
} }
tomorrowDayHeader != null && tomorrowDayHeader.content.isNotBlank() -> { tomorrowDayHeader != null && tomorrowDayHeader.content.isNotBlank() -> {
dateToNavigate = tomorrowDate
updateLessonView(item, emptyList(), binding, tomorrowDayHeader) updateLessonView(item, emptyList(), binding, tomorrowDayHeader)
binding.dashboardLessonsItemTitleTomorrow.isVisible = true binding.dashboardLessonsItemTitleTomorrow.isVisible = true
} }
else -> { else -> {
dateToNavigate = tomorrowDate
updateLessonView(item, emptyList(), binding) updateLessonView(item, emptyList(), binding)
binding.dashboardLessonsItemTitleTomorrow.isVisible = binding.dashboardLessonsItemTitleTomorrow.isVisible =
!(item.isLoading && item.error == null) !(item.isLoading && item.error == null)
@ -326,7 +333,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
Handler(Looper.getMainLooper()).post { updateLessonState() } Handler(Looper.getMainLooper()).post { updateLessonState() }
} }
binding.root.setOnClickListener { onLessonsTileClickListener() } binding.root.setOnClickListener { onLessonsTileClickListener(dateToNavigate) }
} }
private fun updateLessonView( 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.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.message.MessageFragment 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.schoolannouncement.SchoolAnnouncementFragment
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
import io.github.wulkanowy.utils.capitalise import io.github.wulkanowy.utils.capitalise
@ -70,10 +71,7 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
override fun initView() { override fun initView() {
val mainActivity = requireActivity() as MainActivity val mainActivity = requireActivity() as MainActivity
val itemTouchHelper = ItemTouchHelper( val itemTouchHelper = ItemTouchHelper(
DashboardItemMoveCallback( DashboardItemMoveCallback(dashboardAdapter, presenter::onDragAndDropEnd)
dashboardAdapter,
presenter::onDragAndDropEnd
)
) )
dashboardAdapter.apply { dashboardAdapter.apply {
@ -87,7 +85,9 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
onAttendanceTileClickListener = { onAttendanceTileClickListener = {
mainActivity.pushView(AttendanceSummaryFragment.newInstance()) mainActivity.pushView(AttendanceSummaryFragment.newInstance())
} }
onLessonsTileClickListener = { mainActivity.pushView(TimetableFragment.newInstance()) } onLessonsTileClickListener = {
mainActivity.pushView(TimetableFragment.newInstance(it))
}
onGradeTileClickListener = { mainActivity.pushView(GradeFragment.newInstance()) } onGradeTileClickListener = { mainActivity.pushView(GradeFragment.newInstance()) }
onHomeworkTileClickListener = { mainActivity.pushView(HomeworkFragment.newInstance()) } onHomeworkTileClickListener = { mainActivity.pushView(HomeworkFragment.newInstance()) }
onAnnouncementsTileClickListener = { onAnnouncementsTileClickListener = {
@ -121,6 +121,7 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) { return when (item.itemId) {
R.id.dashboard_menu_tiles -> presenter.onDashboardTileSettingsSelected() R.id.dashboard_menu_tiles -> presenter.onDashboardTileSettingsSelected()
R.id.dashboard_menu_notifaction_list -> presenter.onNotificationsCenterSelected()
else -> false else -> false
} }
} }
@ -183,6 +184,10 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
if (::presenter.isInitialized) presenter.onViewReselected() if (::presenter.isInitialized) presenter.onViewReselected()
} }
override fun openNotificationsCenterView() {
(requireActivity() as MainActivity).pushView(NotificationsCenterFragment.newInstance())
}
override fun onDestroyView() { override fun onDestroyView() {
dashboardAdapter.clearTimers() dashboardAdapter.clearTimers()
presenter.onDetachView() presenter.onDetachView()

View File

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

View File

@ -23,4 +23,6 @@ interface DashboardView : BaseView {
fun resetView() fun resetView()
fun popViewToRoot() 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 { launch {
block(studentRepository.getCurrentStudent(false)) block(studentRepository.getCurrentStudent(false))
} }

View File

@ -131,7 +131,9 @@ class GradeAverageProvider @Inject constructor(
val updatedFirstSemesterGrades = val updatedFirstSemesterGrades =
firstSemesterSubject?.grades?.updateModifiers(student).orEmpty() firstSemesterSubject?.grades?.updateModifiers(student).orEmpty()
(updatedSecondSemesterGrades + updatedFirstSemesterGrades).calcAverage(isOptionalArithmeticAverage) (updatedSecondSemesterGrades + updatedFirstSemesterGrades).calcAverage(
isOptionalArithmeticAverage
)
} else { } else {
secondSemesterSubject.average secondSemesterSubject.average
} }
@ -147,7 +149,8 @@ class GradeAverageProvider @Inject constructor(
return if (!isAnyVulcanAverage || isGradeAverageForceCalc) { return if (!isAnyVulcanAverage || isGradeAverageForceCalc) {
val secondSemesterAverage = val secondSemesterAverage =
secondSemesterSubject.grades.updateModifiers(student).calcAverage(isOptionalArithmeticAverage) secondSemesterSubject.grades.updateModifiers(student)
.calcAverage(isOptionalArithmeticAverage)
val firstSemesterAverage = firstSemesterSubject?.grades?.updateModifiers(student) val firstSemesterAverage = firstSemesterSubject?.grades?.updateModifiers(student)
?.calcAverage(isOptionalArithmeticAverage) ?: secondSemesterAverage ?.calcAverage(isOptionalArithmeticAverage) ?: secondSemesterAverage
@ -213,7 +216,8 @@ class GradeAverageProvider @Inject constructor(
proposedPoints = "", proposedPoints = "",
finalPoints = "", finalPoints = "",
pointsSum = "", 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.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.ItemGradeSummaryBinding import io.github.wulkanowy.databinding.ItemGradeSummaryBinding
import io.github.wulkanowy.databinding.ScrollableHeaderGradeSummaryBinding import io.github.wulkanowy.databinding.ScrollableHeaderGradeSummaryBinding
import io.github.wulkanowy.utils.calcAverage import io.github.wulkanowy.utils.calcFinalAverage
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@ -25,6 +25,10 @@ class GradeSummaryAdapter @Inject constructor(
var items = emptyList<GradeSummary>() var items = emptyList<GradeSummary>()
var onCalculatedHelpClickListener: () -> Unit = {}
var onFinalHelpClickListener: () -> Unit = {}
override fun getItemCount() = items.size + if (items.isNotEmpty()) 1 else 0 override fun getItemCount() = items.size + if (items.isNotEmpty()) 1 else 0
override fun getItemViewType(position: Int) = when (position) { 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 finalItemsCount = items.count { it.finalGrade.matches("[0-6][+-]?".toRegex()) }
val calculatedItemsCount = items.count { value -> value.average != 0.0 } val calculatedItemsCount = items.count { value -> value.average != 0.0 }
val allItemsCount = items.count { !it.subject.equals("zachowanie", true) } val allItemsCount = items.count { !it.subject.equals("zachowanie", true) }
val finalAverage = items.calcAverage( val finalAverage = items.calcFinalAverage(
preferencesRepository.gradePlusModifier, preferencesRepository.gradePlusModifier,
preferencesRepository.gradeMinusModifier preferencesRepository.gradeMinusModifier
) )
@ -83,6 +87,9 @@ class GradeSummaryAdapter @Inject constructor(
calculatedItemsCount, calculatedItemsCount,
allItemsCount 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.GONE
import android.view.View.INVISIBLE import android.view.View.INVISIBLE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
@ -48,6 +49,11 @@ class GradeSummaryFragment :
} }
override fun initView() { override fun initView() {
with(gradeSummaryAdapter) {
onCalculatedHelpClickListener = presenter::onCalculatedAverageHelpClick
onFinalHelpClickListener = presenter::onFinalAverageHelpClick
}
with(binding.gradeSummaryRecycler) { with(binding.gradeSummaryRecycler) {
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
adapter = gradeSummaryAdapter adapter = gradeSummaryAdapter
@ -55,7 +61,11 @@ class GradeSummaryFragment :
with(binding) { with(binding) {
gradeSummarySwipe.setOnRefreshListener(presenter::onSwipeRefresh) gradeSummarySwipe.setOnRefreshListener(presenter::onSwipeRefresh)
gradeSummarySwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary)) 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() } gradeSummaryErrorRetry.setOnClickListener { presenter.onRetry() }
gradeSummaryErrorDetails.setOnClickListener { presenter.onDetailsClick() } gradeSummaryErrorDetails.setOnClickListener { presenter.onDetailsClick() }
} }
@ -107,6 +117,22 @@ class GradeSummaryFragment :
binding.gradeSummarySwipe.isRefreshing = show 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) { override fun onParentLoadData(semesterId: Int, forceRefresh: Boolean) {
presenter.onParentViewLoadData(semesterId, forceRefresh) presenter.onParentViewLoadData(semesterId, forceRefresh)
} }

View File

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

View File

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

View File

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

View File

@ -13,7 +13,7 @@ import javax.inject.Inject
class LoginErrorHandler @Inject constructor(resources: Resources) : ErrorHandler(resources) { class LoginErrorHandler @Inject constructor(resources: Resources) : ErrorHandler(resources) {
var onBadCredentials: () -> Unit = {} var onBadCredentials: (String?) -> Unit = {}
var onInvalidToken: (String) -> Unit = {} var onInvalidToken: (String) -> Unit = {}
@ -25,7 +25,7 @@ class LoginErrorHandler @Inject constructor(resources: Resources) : ErrorHandler
override fun proceed(error: Throwable) { override fun proceed(error: Throwable) {
when (error) { when (error) {
is BadCredentialsException -> onBadCredentials() is BadCredentialsException -> onBadCredentials(error.message)
is SQLiteConstraintException -> onStudentDuplicate(resources.getString(R.string.login_duplicate_student)) is SQLiteConstraintException -> onStudentDuplicate(resources.getString(R.string.login_duplicate_student))
is TokenDeadException -> onInvalidToken(resources.getString(R.string.login_expired_token)) is TokenDeadException -> onInvalidToken(resources.getString(R.string.login_expired_token))
is InvalidTokenException -> onInvalidToken(resources.getString(R.string.login_invalid_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> private lateinit var hostSymbols: Array<String>
override val formHostValue: 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 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 override val formPinValue: String
get() = binding.loginFormPin.text.toString().trim() get() = binding.loginFormPin.text.toString().trim()
@ -92,39 +94,62 @@ class LoginAdvancedFragment :
loginFormSignIn.setOnClickListener { presenter.onSignInClick() } loginFormSignIn.setOnClickListener { presenter.onSignInClick() }
loginTypeSwitch.setOnCheckedChangeListener { _, checkedId -> loginTypeSwitch.setOnCheckedChangeListener { _, checkedId ->
presenter.onLoginModeSelected(when (checkedId) { presenter.onLoginModeSelected(
R.id.loginTypeApi -> Sdk.Mode.API when (checkedId) {
R.id.loginTypeScrapper -> Sdk.Mode.SCRAPPER R.id.loginTypeApi -> Sdk.Mode.API
else -> Sdk.Mode.HYBRID R.id.loginTypeScrapper -> Sdk.Mode.SCRAPPER
}) else -> Sdk.Mode.HYBRID
}
)
} }
loginFormPin.setOnEditorDoneSignIn { loginFormSignIn.callOnClick() } loginFormPin.setOnEditorDoneSignIn { loginFormSignIn.callOnClick() }
loginFormPass.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) { with(binding.loginFormHost) {
setText(hostKeys.getOrNull(0).orEmpty()) 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() } setOnClickListener { if (binding.loginFormContainer.visibility == GONE) dismissDropDown() }
} }
} }
override fun showMobileApiWarningMessage() { 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() { 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() { 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) { with(binding) {
loginFormUsername.setText(username) loginFormUsername.setText(username)
loginFormPass.setText(pass) loginFormPass.setText(pass)
@ -177,10 +202,10 @@ class LoginAdvancedFragment :
} }
} }
override fun setErrorPassIncorrect() { override fun setErrorPassIncorrect(message: String?) {
with(binding.loginFormPassLayout) { with(binding.loginFormPassLayout) {
requestFocus() 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>) { override fun notifyParentAccountLogged(studentsWithSemesters: List<StudentWithSemesters>) {
(activity as? LoginActivity)?.onFormFragmentAccountLogged(studentsWithSemesters, Triple( (activity as? LoginActivity)?.onFormFragmentAccountLogged(
binding.loginFormUsername.text.toString(), studentsWithSemesters, Triple(
binding.loginFormPass.text.toString(), binding.loginFormUsername.text.toString(),
resources.getStringArray(R.array.hosts_values)[1] binding.loginFormPass.text.toString(),
)) resources.getStringArray(R.array.hosts_values)[1]
)
)
} }
override fun onResume() { override fun onResume() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -79,12 +79,7 @@ class MessagePreviewAdapter @Inject constructor() :
val readText = when { val readText = when {
recipientCount > 1 -> { recipientCount > 1 -> {
context.resources.getQuantityString( context.getString(R.string.message_read_by, message.readBy, recipientCount)
R.plurals.message_read_by,
message.readBy,
message.readBy,
recipientCount
)
} }
message.readBy == 1 -> { message.readBy == 1 -> {
context.getString(R.string.message_read, context.getString(R.string.all_yes)) 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, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
) = DialogSchoolAnnouncementBinding.inflate(inflater).apply { binding = this }.root ) = DialogSchoolAnnouncementBinding.inflate(inflater).also { binding = it }.root
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)

View File

@ -10,9 +10,12 @@ import android.provider.Settings
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.app.NotificationManagerCompat
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.thelittlefireman.appkillermanager.AppKillerManager import com.thelittlefireman.appkillermanager.AppKillerManager
import com.thelittlefireman.appkillermanager.exceptions.NoActionFoundException 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 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) { override fun initView(showDebugNotificationSwitch: Boolean) {
findPreference<Preference>(getString(R.string.pref_key_notification_debug))?.isVisible = findPreference<Preference>(getString(R.string.pref_key_notification_debug))?.isVisible =
showDebugNotificationSwitch showDebugNotificationSwitch
@ -57,12 +75,11 @@ class NotificationsFragment : PreferenceFragmentCompat(),
} }
} }
findPreference<Preference>(getString(R.string.pref_key_notifications_system_settings))?.run { findPreference<Preference>(getString(R.string.pref_key_notifications_system_settings))
setOnPreferenceClickListener { ?.setOnPreferenceClickListener {
presenter.onOpenSystemSettingsClicked() presenter.onOpenSystemSettingsClicked()
true true
} }
}
} }
override fun onCreateRecyclerView( 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() { override fun onResume() {
super.onResume() super.onResume()
preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(this) preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(this)

View File

@ -31,6 +31,9 @@ class NotificationsPresenter @Inject constructor(
) )
initView(appInfo.isDebug) initView(appInfo.isDebug)
} }
checkNotificationPiggybackState()
Timber.i("Settings notifications view was initialized") Timber.i("Settings notifications view was initialized")
} }
@ -39,7 +42,7 @@ class NotificationsPresenter @Inject constructor(
preferencesRepository.apply { preferencesRepository.apply {
when (key) { when (key) {
isUpcomingLessonsNotificationsEnableKey -> { isUpcomingLessonsNotificationsEnableKey, isUpcomingLessonsNotificationsPersistentKey -> {
if (!isUpcomingLessonsNotificationsEnable) { if (!isUpcomingLessonsNotificationsEnable) {
timetableNotificationHelper.cancelNotification() timetableNotificationHelper.cancelNotification()
} }
@ -47,6 +50,11 @@ class NotificationsPresenter @Inject constructor(
isDebugNotificationEnableKey -> { isDebugNotificationEnableKey -> {
chuckerCollector.showNotification = isDebugNotificationEnable chuckerCollector.showNotification = isDebugNotificationEnable
} }
isNotificationPiggybackEnabledKey -> {
if (isNotificationPiggybackEnabled && view?.isNotificationPermissionGranted == false) {
view?.openNotificationPermissionDialog()
}
}
} }
} }
analytics.logEvent("setting_changed", "name" to key) analytics.logEvent("setting_changed", "name" to key)
@ -59,4 +67,18 @@ class NotificationsPresenter @Inject constructor(
fun onOpenSystemSettingsClicked() { fun onOpenSystemSettingsClicked() {
view?.openSystemSettings() 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 { interface NotificationsView : BaseView {
val isNotificationPermissionGranted: Boolean
fun initView(showDebugNotificationSwitch: Boolean) fun initView(showDebugNotificationSwitch: Boolean)
fun showFixSyncDialog() fun showFixSyncDialog()
@ -11,4 +13,8 @@ interface NotificationsView : BaseView {
fun openSystemSettings() fun openSystemSettings()
fun enableNotification(notificationKey: String, enable: Boolean) 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 package io.github.wulkanowy.ui.modules.timetable
import android.graphics.Paint import android.graphics.Paint
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View.GONE import android.view.View.GONE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R import io.github.wulkanowy.R
@ -151,8 +152,8 @@ class TimetableAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
if (lesson.isStudentPlan && showTimers) { if (lesson.isStudentPlan && showTimers) {
timers[position] = timer(period = 1000) { timers[position] = timer(period = 1000) {
if (ViewCompat.isAttachedToWindow(root)) { Handler(Looper.getMainLooper()).post {
root.post { updateTimeLeft(binding, lesson, position) } updateTimeLeft(binding, lesson, position)
} }
} }
} else { } else {
@ -176,8 +177,8 @@ class TimetableAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
private fun updateTimeLeft(binding: ItemTimetableBinding, lesson: Timetable, position: Int) { private fun updateTimeLeft(binding: ItemTimetableBinding, lesson: Timetable, position: Int) {
val isShowTimeUntil = lesson.isShowTimeUntil(getPreviousLesson(position)) val isShowTimeUntil = lesson.isShowTimeUntil(getPreviousLesson(position))
val until = lesson.until val until = lesson.until.plusMinutes(1)
val left = lesson.left val left = lesson.left?.plusMinutes(1)
val isJustFinished = lesson.isJustFinished val isJustFinished = lesson.isJustFinished
with(binding) { with(binding) {
@ -190,17 +191,10 @@ class TimetableAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
visibility = VISIBLE visibility = VISIBLE
text = context.getString( text = context.getString(
R.string.timetable_time_until, R.string.timetable_time_until,
if (until.seconds <= 60) { context.getString(
context.getString( R.string.timetable_minutes,
R.string.timetable_seconds, until.toMinutes().toString(10)
until.seconds.toString(10) )
)
} else {
context.getString(
R.string.timetable_minutes,
until.toMinutes().toString(10)
)
}
) )
} }
} }
@ -212,17 +206,10 @@ class TimetableAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
visibility = VISIBLE visibility = VISIBLE
text = context.getString( text = context.getString(
R.string.timetable_time_left, R.string.timetable_time_left,
if (left.seconds < 60) { context.getString(
context.getString( R.string.timetable_minutes,
R.string.timetable_seconds, left.toMinutes().toString()
left.seconds.toString(10) )
)
} else {
context.getString(
R.string.timetable_minutes,
left.toMinutes().toString(10)
)
}
) )
} }
} }
@ -360,7 +347,7 @@ class TimetableAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
private fun updateTeacherColor(teacherTextView: TextView, lesson: Timetable) { private fun updateTeacherColor(teacherTextView: TextView, lesson: Timetable) {
teacherTextView.setTextColor( teacherTextView.setTextColor(
teacherTextView.context.getThemeAttrColor( 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 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.data.db.entities.Timetable
import io.github.wulkanowy.databinding.DialogTimetableBinding import io.github.wulkanowy.databinding.DialogTimetableBinding
import io.github.wulkanowy.utils.capitalise import io.github.wulkanowy.utils.capitalise
import io.github.wulkanowy.utils.decapitalise
import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.lifecycleAwareVariable import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
@ -52,7 +51,7 @@ class TimetableDialog : DialogFragment() {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
with(lesson) { with(lesson) {
setInfo(info, teacher, canceled, changes) setInfo(info, canceled, changes)
setSubject(subject, subjectOld) setSubject(subject, subjectOld)
setTeacher(teacher, teacherOld) setTeacher(teacher, teacherOld)
setGroup(group) setGroup(group)
@ -80,7 +79,7 @@ class TimetableDialog : DialogFragment() {
} }
@SuppressLint("DefaultLocale") @SuppressLint("DefaultLocale")
private fun setInfo(info: String, teacher: String, canceled: Boolean, changes: Boolean) { private fun setInfo(info: String, canceled: Boolean, changes: Boolean) {
with(binding) { with(binding) {
when { when {
info.isNotBlank() -> { info.isNotBlank() -> {
@ -90,20 +89,26 @@ class TimetableDialog : DialogFragment() {
R.attr.colorPrimary R.attr.colorPrimary
) )
) )
timetableDialogChangesValue.setTextColor(requireContext().getThemeAttrColor(R.attr.colorPrimary)) timetableDialogChangesValue.setTextColor(
requireContext().getThemeAttrColor(
R.attr.colorPrimary
)
)
} else { } else {
timetableDialogChangesTitle.setTextColor( timetableDialogChangesTitle.setTextColor(
requireContext().getThemeAttrColor( requireContext().getThemeAttrColor(
R.attr.colorTimetableChange R.attr.colorTimetableChange
) )
) )
timetableDialogChangesValue.setTextColor(requireContext().getThemeAttrColor(R.attr.colorTimetableChange)) timetableDialogChangesValue.setTextColor(
requireContext().getThemeAttrColor(
R.attr.colorTimetableChange
)
)
} }
timetableDialogChangesValue.text = when { timetableDialogChangesValue.text = when {
canceled && !changes -> "Lekcja odwołana: $info" canceled && !changes -> "Lekcja odwołana: $info"
changes && teacher.isNotBlank() -> "Zastępstwo: $teacher"
changes && teacher.isBlank() -> "Zastępstwo, ${info.decapitalise()}"
else -> info.capitalise() 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 teacher.isNotBlank() -> timetableDialogTeacherValue.text = teacher
else -> { else -> {
timetableDialogTeacherTitle.visibility = GONE timetableDialogTeacherTitle.visibility = GONE

View File

@ -44,7 +44,13 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
companion object { companion object {
private const val SAVED_DATE_KEY = "CURRENT_DATE" 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 override val titleStringId get() = R.string.timetable_title
@ -62,7 +68,11 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding = FragmentTimetableBinding.bind(view) binding = FragmentTimetableBinding.bind(view)
messageContainer = binding.timetableRecycler 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() { override fun initView() {

View File

@ -173,7 +173,7 @@ class TimetableWidgetFactory(
updateNotCanceledLessonNumberColor(this, lesson) updateNotCanceledLessonNumberColor(this, lesson)
updateNotCanceledSubjectColor(this, lesson) updateNotCanceledSubjectColor(this, lesson)
val teacherChange = lesson.teacherOld.isNotBlank() && lesson.teacher != lesson.teacherOld val teacherChange = lesson.teacherOld.isNotBlank()
updateNotCanceledRoom(this, lesson, teacherChange) updateNotCanceledRoom(this, lesson, teacherChange)
updateNotCanceledTeacher(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.R
import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary 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 { fun List<Grade>.calcAverage(isOptionalArithmeticAverage: Boolean): Double {
val isArithmeticAverage = isOptionalArithmeticAverage && !any { it.weightValue != .0 } 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 return if (denominator != 0.0) counter / denominator else 0.0
} }
@JvmName("calcSummaryAverage") fun List<GradeSummary>.calcFinalAverage(plusModifier: Double, minusModifier: Double) = asSequence()
fun List<GradeSummary>.calcAverage(plusModifier: Double, minusModifier: Double) = asSequence()
.mapNotNull { .mapNotNull {
if (it.finalGrade.matches("[0-6][+-]?".toRegex())) { if (it.finalGrade.matches("[0-6][+-]?".toRegex())) {
when { when {

View File

@ -33,7 +33,7 @@ class AutoRefreshHelper @Inject constructor(
private val sharedPref: SharedPrefProvider private val sharedPref: SharedPrefProvider
) { ) {
fun isShouldBeRefreshed(key: String): Boolean { fun shouldBeRefreshed(key: String): Boolean {
val timestamp = sharedPref.getLong(key, 0).toLocalDateTime() 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() 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) - naprawiliśmy logowanie na platformę Opolskiej eSzkoły
- odblokowaliśmy niedzielę w wyborze daty w planie lekcji i innych zakładkach - dodaliśmy centrum powiadomień i opcję odbierania pushy z oficjalnej aplikacji (dla zaawansowanych)
- przywróciliśmy odnośnik do szczęśliwego numerka w menu Więcej - dodaliśmy objaśnienie do informacji o obliczonych średnich w podsumowaniu
- naprawiliśmy drobne błędy ze stabilnością i wyglądem - 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 Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases

View File

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

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<solid android:color="@color/colorWidgetBackground" /> <solid android:color="#BB191919" />
<corners android:radius="5dp" /> <corners android:radius="8dp" />
</shape> </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:layout_marginEnd="24dp"
android:paddingStart="0dp" android:paddingStart="0dp"
android:paddingEnd="16dp" android:paddingEnd="16dp"
tools:maxLines="5"
android:text="@string/all_no_data" android:text="@string/all_no_data"
android:textIsSelectable="true" android:textIsSelectable="true"
android:textSize="16sp" android:textSize="16sp"

View File

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

View File

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