1
0
mirror of https://github.com/wulkanowy/wulkanowy.git synced 2024-09-20 11:29:08 -05:00

Merge branch 'release/1.3.0'

This commit is contained in:
Mikołaj Pich 2021-09-28 23:26:18 +02:00
commit 689012131f
121 changed files with 4187 additions and 790 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 96 versionCode 97
versionName "1.2.3" 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.3" 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"
@ -215,7 +224,7 @@ dependencies {
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,13 +114,15 @@ 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 {
(item.amounts.mapIndexed { gradeValue, amount ->
(gradeValue + 1) * amount (gradeValue + 1) * amount
}.sum().toDouble() / denominator).let { }.sum().toDouble() / denominator).let {
"%.2f".format(Locale.FRANCE, it) "%.2f".format(Locale.FRANCE, it)
} }
} }
} }
}
when (subjectName) { when (subjectName) {
"Wszystkie" -> (itemsWithAverage.reversed() + GradeSemesterStatistics( "Wszystkie" -> (itemsWithAverage.reversed() + GradeSemesterStatistics(
studentId = semester.studentId, studentId = semester.studentId,
@ -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) =
luckyNumberDb.load(student.studentId, now()).map {
if (it?.isNotified == false) it else null if (it?.isNotified == false) it else null
}.first() }.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,15 +16,26 @@ 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(
fun getSchoolInfo(
student: Student,
semester: Semester,
forceRefresh: Boolean,
) = networkBoundResource(
mutex = saveFetchResultMutex, mutex = saveFetchResultMutex,
shouldFetch = { it == null || forceRefresh }, shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(
key = getRefreshKey(cacheKey, student)
)
it == null || forceRefresh || isExpired
},
query = { schoolDb.load(semester.studentId, semester.classId) }, query = { schoolDb.load(semester.studentId, semester.classId) },
fetch = { fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear).getSchool() sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear).getSchool()
@ -37,6 +50,7 @@ class SchoolRepository @Inject constructor(
} else if (old == null) { } else if (old == null) {
schoolDb.insertAll(listOf(new)) schoolDb.insertAll(listOf(new))
} }
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
} }
) )
} }

View File

@ -19,8 +19,11 @@ 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,
semester: Semester,
forceRefresh: Boolean,
) = networkBoundResource(
mutex = saveFetchResultMutex, mutex = saveFetchResultMutex,
shouldFetch = { it == null || forceRefresh }, shouldFetch = { it == null || forceRefresh },
query = { studentInfoDao.loadStudentInfo(student.studentId) }, query = { studentInfoDao.loadStudentInfo(student.studentId) },

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,20 +92,38 @@ 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,
notificationId: Int,
isPersistent: Boolean,
studentName: String?,
countDown: Long,
timeout: Long,
title: String,
next: String?
) {
NotificationManagerCompat.from(context)
.notify(notificationId, NotificationCompat.Builder(context, CHANNEL_ID)
.setContentTitle(title) .setContentTitle(title)
.setContentText(next) .setContentText(next)
.setAutoCancel(false) .setAutoCancel(false)
.setOngoing(true)
.setWhen(countDown) .setWhen(countDown)
.setOngoing(isPersistent)
.apply { .apply {
if (Build.VERSION.SDK_INT >= N) setUsesChronometer(true) if (Build.VERSION.SDK_INT >= N) setUsesChronometer(true)
} }
@ -111,8 +134,14 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() {
it.setSummaryText(studentName) it.setSummaryText(studentName)
it.addLine(next) it.addLine(next)
}) })
.setContentIntent(PendingIntent.getActivity(context, MainView.Section.TIMETABLE.id, .setContentIntent(
MainActivity.getStartIntent(context, MainView.Section.TIMETABLE, true), FLAG_UPDATE_CURRENT)) PendingIntent.getActivity(
context,
MainView.Section.TIMETABLE.id,
MainActivity.getStartIntent(context, MainView.Section.TIMETABLE, true),
FLAG_UPDATE_CURRENT
)
)
.build() .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,6 +152,7 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
notificationType: Int, notificationType: Int,
time: LocalDateTime time: LocalDateTime
) { ) {
try {
AlarmManagerCompat.setExactAndAllowWhileIdle( AlarmManagerCompat.setExactAndAllowWhileIdle(
alarmManager, RTC_WAKEUP, time.toTimestamp(), alarmManager, RTC_WAKEUP, time.toTimestamp(),
PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also { PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also {
@ -155,5 +165,8 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
intent.getStringExtra(LESSON_TITLE) intent.getStringExtra(LESSON_TITLE)
}, start: $time, student: $studentId" }, 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(
Constraints.Builder()
.setRequiredNetworkType(if (preferencesRepository.isServicesOnlyWifi) UNMETERED else CONNECTED) .setRequiredNetworkType(if (preferencesRepository.isServicesOnlyWifi) UNMETERED else CONNECTED)
.build()) .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,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.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,
@ -25,6 +21,6 @@ class NewLuckyNumberNotification @Inject constructor(
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,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.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,
@ -28,6 +24,6 @@ class NewSchoolAnnouncementNotification @Inject constructor(
} }
) )
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

@ -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
@ -120,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
} }
} }
@ -182,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

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(
when (checkedId) {
R.id.loginTypeApi -> Sdk.Mode.API R.id.loginTypeApi -> Sdk.Mode.API
R.id.loginTypeScrapper -> Sdk.Mode.SCRAPPER R.id.loginTypeScrapper -> Sdk.Mode.SCRAPPER
else -> Sdk.Mode.HYBRID 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(
studentsWithSemesters, Triple(
binding.loginFormUsername.text.toString(), binding.loginFormUsername.text.toString(),
binding.loginFormPass.text.toString(), binding.loginFormPass.text.toString(),
resources.getStringArray(R.array.hosts_values)[1] 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,9 +151,14 @@ 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
} }
} }
@ -156,10 +170,12 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
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

@ -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,13 +75,12 @@ 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(
inflater: LayoutInflater?, inflater: LayoutInflater?,
@ -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(
R.string.timetable_seconds,
until.seconds.toString(10)
)
} else {
context.getString( context.getString(
R.string.timetable_minutes, R.string.timetable_minutes,
until.toMinutes().toString(10) 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(
R.string.timetable_seconds,
left.seconds.toString(10)
)
} else {
context.getString( context.getString(
R.string.timetable_minutes, R.string.timetable_minutes,
left.toMinutes().toString(10) left.toMinutes().toString()
) )
}
) )
} }
} }
@ -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

@ -89,14 +89,22 @@ 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 {
@ -128,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

@ -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.3 Wersja 1.3.0
- naprawiliśmy pomieszane imiona nauczycieli z salami w planie lekcji - naprawiliśmy logowanie na platformę Opolskiej eSzkoły
- dodaliśmy brakujące okienka ze szczegółami na ekranie zebrań - dodaliśmy centrum powiadomień i opcję odbierania pushy z oficjalnej aplikacji (dla zaawansowanych)
- klikając w kafelek z lekcjami na jutro aplikacja teraz przekierowuje na ekran z planem na jutro - dodaliśmy objaśnienie do informacji o obliczonych średnich w podsumowaniu
- naprawiliśmy błąd przy wylogowywaniu innego niż bieżący uczeń - 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

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

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView 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="wrap_content"
android:layout_marginHorizontal="8dp"
android:layout_marginTop="12dp"
app:cardElevation="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/notifications_center_item_date"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:textColor="?android:textColorSecondary"
android:textSize="14sp"
app:layout_constraintEnd_toStartOf="@id/notifications_center_item_icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/date/ddmmyy" />
<TextView
android:id="@+id/notifications_center_item_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="16dp"
android:textSize="15sp"
app:layout_constraintEnd_toStartOf="@id/notifications_center_item_icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/notifications_center_item_date"
tools:text="@tools:sample/lorem" />
<TextView
android:id="@+id/notifications_center_item_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:ellipsize="end"
android:maxLines="5"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/notifications_center_item_icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/notifications_center_item_title"
tools:text="@tools:sample/lorem/random" />
<ImageView
android:id="@+id/notifications_center_item_icon"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?colorPrimary"
tools:ignore="ContentDescription"
tools:src="@tools:sample/avatars" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout 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" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -12,7 +13,7 @@
android:layout_width="150dp" android:layout_width="150dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="20dp" android:layout_marginTop="20dp"
android:layout_marginEnd="5dp" android:layout_marginEnd="15dp"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
@ -24,14 +25,31 @@
android:text="@string/grade_summary_calculated_average" android:text="@string/grade_summary_calculated_average"
android:textSize="16sp" /> android:textSize="16sp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="horizontal">
<TextView <TextView
android:id="@+id/gradeSummaryScrollableHeaderCalculated" android:id="@+id/gradeSummaryScrollableHeaderCalculated"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_horizontal" android:gravity="center"
android:textSize="21sp" android:textSize="21sp"
tools:text="6,00" /> tools:text="6,00" />
<ImageButton
android:id="@+id/gradeSummaryCalculatedAverageHelp"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginLeft="8dp"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@+string/grade_summary_calculated_average_help_dialog_title"
app:srcCompat="@drawable/ic_help"
app:tint="?colorOnBackground" />
</LinearLayout>
<TextView <TextView
android:id="@+id/gradeSummaryScrollableHeaderCalculatedSubjectCount" android:id="@+id/gradeSummaryScrollableHeaderCalculatedSubjectCount"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -57,6 +75,12 @@
android:text="@string/grade_summary_final_average" android:text="@string/grade_summary_final_average"
android:textSize="16sp" /> android:textSize="16sp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="horizontal">
<TextView <TextView
android:id="@+id/gradeSummaryScrollableHeaderFinal" android:id="@+id/gradeSummaryScrollableHeaderFinal"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -65,6 +89,17 @@
android:textSize="21sp" android:textSize="21sp"
tools:text="6,00" /> tools:text="6,00" />
<ImageButton
android:id="@+id/gradeSummaryFinalAverageHelp"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginLeft="8dp"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@+string/grade_summary_calculated_average_help_dialog_title"
app:srcCompat="@drawable/ic_help"
app:tint="?colorOnBackground" />
</LinearLayout>
<TextView <TextView
android:id="@+id/gradeSummaryScrollableHeaderFinalSubjectCount" android:id="@+id/gradeSummaryScrollableHeaderFinalSubjectCount"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -1,10 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/dashboard_menu_notifaction_list"
android:icon="@drawable/ic_settings_notifications"
android:orderInCategory="1"
android:title="@string/notifications_center_title"
app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="ifRoom" />
<item <item
android:id="@+id/dashboard_menu_tiles" android:id="@+id/dashboard_menu_tiles"
android:icon="@drawable/ic_more_settings" android:icon="@drawable/ic_more_settings"
android:orderInCategory="1" android:orderInCategory="2"
android:title="@string/pref_dashboard_appearance_tiles_title" android:title="@string/pref_dashboard_appearance_tiles_title"
app:iconTint="@color/material_on_surface_emphasis_medium" app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />

View File

@ -5,7 +5,7 @@
<item <item
android:id="@+id/mainMenuAccount" android:id="@+id/mainMenuAccount"
android:orderInCategory="2" android:orderInCategory="10"
app:showAsAction="always" app:showAsAction="always"
tools:ignore="MenuTitle" /> tools:ignore="MenuTitle" />
</menu> </menu>

View File

@ -41,8 +41,8 @@
<item>Barvy známek v deníku</item> <item>Barvy známek v deníku</item>
</string-array> </string-array>
<string-array name="grade_average_mode_entries"> <string-array name="grade_average_mode_entries">
<item>Průměrná známka od druhého semestru</item> <item>Průměr známek pouze z vybraného semestru</item>
<item>Průměr známek z obou semestrů</item> <item>Průměr z průměrů z obou semestrů</item>
<item>Průměr známek z celého roku</item> <item>Průměr známek z celého roku</item>
</string-array> </string-array>
<string-array name="dashboard_tile_entries"> <string-array name="dashboard_tile_entries">

View File

@ -12,7 +12,7 @@
<string name="about_title">O aplikaci</string> <string name="about_title">O aplikaci</string>
<string name="logviewer_title">Prohlížeč protokolů</string> <string name="logviewer_title">Prohlížeč protokolů</string>
<string name="debug_title">Ladění</string> <string name="debug_title">Ladění</string>
<string name="notification_debug_title">Ladění oznáme</string> <string name="notification_debug_title">Ladění upozorně</string>
<string name="contributors_title">Tvůrci</string> <string name="contributors_title">Tvůrci</string>
<string name="license_title">Licence</string> <string name="license_title">Licence</string>
<string name="message_title">Zprávy</string> <string name="message_title">Zprávy</string>
@ -24,6 +24,7 @@
<string name="account_details_title">Podrobnosti účtu</string> <string name="account_details_title">Podrobnosti účtu</string>
<string name="student_info_title">Informace o žáku</string> <string name="student_info_title">Informace o žáku</string>
<string name="dashboard_title">Domů</string> <string name="dashboard_title">Domů</string>
<string name="notifications_center_title">Centrum upozornění</string>
<!--Subtitles--> <!--Subtitles-->
<string name="grade_subtitle">Semestr %1$d, %2$d/%3$d</string> <string name="grade_subtitle">Semestr %1$d, %2$d/%3$d</string>
<!--Login--> <!--Login-->
@ -42,7 +43,8 @@
<string name="login_symbol_hint">Symbol</string> <string name="login_symbol_hint">Symbol</string>
<string name="login_sign_in">Přihlásit</string> <string name="login_sign_in">Přihlásit</string>
<string name="login_invalid_password">Toto heslo je příliš krátké</string> <string name="login_invalid_password">Toto heslo je příliš krátké</string>
<string name="login_incorrect_password">Přihlašovací údaje jsou nesprávné. Ujistěte se, že je v poli níže vybrána správná variace deníku UONET+</string> <string name="login_incorrect_password_default">Přihlašovací údaje jsou nesprávné</string>
<string name="login_incorrect_password">%1$s. Zkontrolujte, zda je níže vybrána správná variace deníku UONET+</string>
<string name="login_invalid_pin">Neplatný PIN</string> <string name="login_invalid_pin">Neplatný PIN</string>
<string name="login_invalid_token">Neplatný token</string> <string name="login_invalid_token">Neplatný token</string>
<string name="login_expired_token">Token vypršel</string> <string name="login_expired_token">Token vypršel</string>
@ -90,6 +92,10 @@
<string name="grade_summary_final_grade">Konečná známka</string> <string name="grade_summary_final_grade">Konečná známka</string>
<string name="grade_summary_predicted_grade">Předpokládaná známka</string> <string name="grade_summary_predicted_grade">Předpokládaná známka</string>
<string name="grade_summary_calculated_average">Vypočítaný průměr</string> <string name="grade_summary_calculated_average">Vypočítaný průměr</string>
<string name="grade_summary_calculated_average_help_dialog_title">Jak funguje vypočítaný průměr?</string>
<string name="grade_summary_calculated_average_help_dialog_message">Vypočítaný průměr je aritmetický průměr vypočítaný z průměrů předmětů. Umožňuje vám to znát přibližný konečný průměr. Vypočítává se způsobem zvoleným uživatelem v nastavení aplikaci. Doporučuje se vybrat příslušnou možnost. Důvodem je rozdílný výpočet školních průměrů. Pokud vaše škola navíc uvádí průměr předmětů na stránce deníku Vulcan, aplikace si je stáhne a tyto průměry nepočítá. To lze změnit vynucením výpočtu průměru v nastavení aplikaci.\n\n<b>Průměr známek pouze z vybraného semestru</b>:\n1. Výpočet váženého průměru pro každý předmět v daném semestru\n2. Sčítání vypočítaných průměrů\n3. Výpočet aritmetického průměru součtených průměrů\n\n<b>Průměr průměrů z obou semestrů</b>:\n1. Výpočet váženého průměru pro každý předmět v semestru 1 a 2\n2. Výpočet aritmetického průměru vypočítaných průměrů za semestry 1 a 2 pro každý předmět.\n3. Sčítání vypočítaných průměrů\n4. Výpočet aritmetického průměru součtených průměrů\n\n<b>Průměr známek z celého roku:</b>\n1. Výpočet váženého průměru za rok pro každý předmět. Konečný průměr v 1. semestru je nepodstatný.\n3. Sčítání vypočítaných průměrů\n4. Výpočet aritmetického průměru součtených průměrů</string>
<string name="grade_summary_final_average_help_dialog_title">Jak funguje konečný průměr?</string>
<string name="grade_summary_final_average_help_dialog_message">Konečný průměr je aritmetický průměr vypočítaný ze všech aktuálně dostupných konečných známek v daném semestru.\n\nSchéma výpočtu se skládá z následujících kroků:\n1. Sčítání konečných známek zadaných učiteli\n2. Děleno počtem předmětů, pro které už byly vydány známky</string>
<string name="grade_summary_final_average">Konečný průměr</string> <string name="grade_summary_final_average">Konečný průměr</string>
<string name="grade_summary_from_subjects">z %1$d z %2$d předmětů</string> <string name="grade_summary_from_subjects">z %1$d z %2$d předmětů</string>
<string name="grade_menu_summary">Shrnutí</string> <string name="grade_menu_summary">Shrnutí</string>
@ -238,12 +244,7 @@
<string name="message_chip_only_unread">Pouze nepřečtené</string> <string name="message_chip_only_unread">Pouze nepřečtené</string>
<string name="message_chip_only_with_attachments">Pouze s přílohami</string> <string name="message_chip_only_with_attachments">Pouze s přílohami</string>
<string name="message_read">Přečtena: %s</string> <string name="message_read">Přečtena: %s</string>
<plurals name="message_read_by"> <string name="message_read_by">Přečtena přes: %1$d z %2$d osob</string>
<item quantity="one">Přečtena přes: %1$d z %2$d osob</item>
<item quantity="few">Přečtena přes: %1$d z %2$d osob</item>
<item quantity="many">Přečtena přes: %1$d z %2$d osob</item>
<item quantity="other">Přečtena přes: %1$d z %2$d osob</item>
</plurals>
<plurals name="message_number_item"> <plurals name="message_number_item">
<item quantity="one">%d zpráva</item> <item quantity="one">%d zpráva</item>
<item quantity="few">%d zprávy</item> <item quantity="few">%d zprávy</item>
@ -402,7 +403,7 @@
<item quantity="many">Máte %1$d nových setkání</item> <item quantity="many">Máte %1$d nových setkání</item>
<item quantity="other">Máte %1$d nových setkání</item> <item quantity="other">Máte %1$d nových setkání</item>
</plurals> </plurals>
<string name="conferences_present">Present at conference</string> <string name="conferences_present">Přítomnost na setkání</string>
<string name="conference_agenda">Agenda</string> <string name="conference_agenda">Agenda</string>
<!--Director information--> <!--Director information-->
<string name="school_announcement_title">Školní oznámení</string> <string name="school_announcement_title">Školní oznámení</string>
@ -592,7 +593,7 @@
<string name="all_yes">Ano</string> <string name="all_yes">Ano</string>
<string name="all_no">Ne</string> <string name="all_no">Ne</string>
<string name="all_save">Uložit</string> <string name="all_save">Uložit</string>
<string name="all_title">Title</string> <string name="all_title">Titul</string>
<!--Timetable Widget--> <!--Timetable Widget-->
<string name="widget_timetable_no_items">Žádné lekce</string> <string name="widget_timetable_no_items">Žádné lekce</string>
<string name="widget_timetable_theme_title">Vybrat motiv</string> <string name="widget_timetable_theme_title">Vybrat motiv</string>
@ -602,7 +603,7 @@
<!--Preferences--> <!--Preferences-->
<string name="pref_view_header">Vzhled a chování aplikací</string> <string name="pref_view_header">Vzhled a chování aplikací</string>
<string name="pref_view_list">Výchozí zobrazení</string> <string name="pref_view_list">Výchozí zobrazení</string>
<string name="pref_view_grade_average_mode">Výpočet koncoročního průměru</string> <string name="pref_view_grade_average_mode">Možnosti vypočítaného průměru</string>
<string name="pref_view_grade_average_force_calc">Vynutit průměrný výpočet podle aplikace</string> <string name="pref_view_grade_average_force_calc">Vynutit průměrný výpočet podle aplikace</string>
<string name="pref_view_present">Zobrazit přítomnost</string> <string name="pref_view_present">Zobrazit přítomnost</string>
<string name="pref_view_app_theme">Motiv</string> <string name="pref_view_app_theme">Motiv</string>
@ -617,12 +618,18 @@
<string name="pref_notify_header">Upozornění</string> <string name="pref_notify_header">Upozornění</string>
<string name="pref_notify_switch">Zobrazit upozornění</string> <string name="pref_notify_switch">Zobrazit upozornění</string>
<string name="pref_notify_upcoming_lessons_switch">Zobrazit upozornění o nadcházející lekci</string> <string name="pref_notify_upcoming_lessons_switch">Zobrazit upozornění o nadcházející lekci</string>
<string name="pref_notify_upcoming_lessons_persistent_switch">Nastavit upozornění o nadcházející lekci jako trvalé</string>
<string name="pref_notify_upcoming_lessons_persistent_summary">Vypnout, když upozornění není ve vašem hodinkách/náramku viditelné</string>
<string name="pref_notify_open_system_settings">Otevřít systémová nastavení upozornění</string> <string name="pref_notify_open_system_settings">Otevřít systémová nastavení upozornění</string>
<string name="pref_notify_fix_sync_issues">Opravte problémy se synchronizací a upozorněním</string> <string name="pref_notify_fix_sync_issues">Opravte problémy se synchronizací a upozorněním</string>
<string name="pref_notify_fix_sync_issues_message">Vaše zařízení může mít problémy se synchronizací dat as upozorněními.\n\nChcete-li je opravit, přidejte Wulkanového do funkce Autostart a vypněte optimalizaci/úsporu baterie v nastavení systému telefonu.</string> <string name="pref_notify_fix_sync_issues_message">Vaše zařízení může mít problémy se synchronizací dat as upozorněními.\n\nChcete-li je opravit, přidejte Wulkanového do funkce Autostart a vypněte optimalizaci/úsporu baterie v nastavení systému telefonu.</string>
<string name="pref_notify_fix_sync_issues_settings_button">Přejít do nastavení</string> <string name="pref_notify_fix_sync_issues_settings_button">Přejít do nastavení</string>
<string name="pref_notify_debug_switch">Zobrazit upozornění o ladění</string> <string name="pref_notify_debug_switch">Zobrazit upozornění o ladění</string>
<string name="pref_notify_disabled_summary">Synchronizace je vypnutá</string> <string name="pref_notify_disabled_summary">Synchronizace je vypnutá</string>
<string name="pref_notify_notifications_piggyback">Zachytit upozornění oficiální aplikací</string>
<string name="pref_notification_piggyback_popup_title">Zachytit upozornění</string>
<string name="pref_notification_piggyback_popup_description">S touto funkcí můžete získat náhradu push upozornění jako v oficiální aplikaci. Vše, co musíte udělat, je povolit Wulkanowému číst všechna vaše upozornění v nastaveních systému.\n\nJak to funguje?\nKdyž obdržíte oznámení v Deníčku VULCAN, Wulkanowy bude o tom informován (k tomu je to dodatečné povolení) a spustí synchronizaci, aby mohl zaslat vlastní upozornění.\n\nPOUZE PRO POKROČILÉ UŽIVATELE</string>
<string name="pref_notification_piggyback_popup_positive">Přejít do nastavení</string>
<string name="pref_services_header">Synchronizace</string> <string name="pref_services_header">Synchronizace</string>
<string name="pref_services_switch">Automatická aktualizace</string> <string name="pref_services_switch">Automatická aktualizace</string>
<string name="pref_services_suspended">Pozastaveno na dovolené</string> <string name="pref_services_suspended">Pozastaveno na dovolené</string>

View File

@ -41,8 +41,8 @@
<item>Farben der Bewertungen im Logbuch</item> <item>Farben der Bewertungen im Logbuch</item>
</string-array> </string-array>
<string-array name="grade_average_mode_entries"> <string-array name="grade_average_mode_entries">
<item>Durchschnittsnote für das 2. Semester</item>
<item>Durchschnitt der Noten aus beiden Semestern</item> <item>Durchschnitt der Noten aus beiden Semestern</item>
<item>Durchschnittswert der Durchschnittswerte beider Semester</item>
<item>Durchschnitt der Noten aus dem ganzen Jahr</item> <item>Durchschnitt der Noten aus dem ganzen Jahr</item>
</string-array> </string-array>
<string-array name="dashboard_tile_entries"> <string-array name="dashboard_tile_entries">

View File

@ -24,6 +24,7 @@
<string name="account_details_title">Kontodetails</string> <string name="account_details_title">Kontodetails</string>
<string name="student_info_title">Schülerinfo</string> <string name="student_info_title">Schülerinfo</string>
<string name="dashboard_title">Übersicht</string> <string name="dashboard_title">Übersicht</string>
<string name="notifications_center_title">Benachrichtigungszentrum</string>
<!--Subtitles--> <!--Subtitles-->
<string name="grade_subtitle">Semester %1$d, %2$d/%3$d</string> <string name="grade_subtitle">Semester %1$d, %2$d/%3$d</string>
<!--Login--> <!--Login-->
@ -42,7 +43,8 @@
<string name="login_symbol_hint">Symbol</string> <string name="login_symbol_hint">Symbol</string>
<string name="login_sign_in">Anmelden</string> <string name="login_sign_in">Anmelden</string>
<string name="login_invalid_password">Passwort ist zu kurz</string> <string name="login_invalid_password">Passwort ist zu kurz</string>
<string name="login_incorrect_password">Anmeldedaten sind falsch. Stellen Sie sicher, dass die richtige UONET+ Registervariation im unteren Feld ausgewählt ist</string> <string name="login_incorrect_password_default">Anmeldedaten sind falsch</string>
<string name="login_incorrect_password">%1$s. Stellen Sie sicher, dass die korrekte UONET+ Registervariation unten ausgewählt ist</string>
<string name="login_invalid_pin">Ungültige PIN</string> <string name="login_invalid_pin">Ungültige PIN</string>
<string name="login_invalid_token">Ungültige token</string> <string name="login_invalid_token">Ungültige token</string>
<string name="login_expired_token">Token ist nicht mehr gültig</string> <string name="login_expired_token">Token ist nicht mehr gültig</string>
@ -90,6 +92,10 @@
<string name="grade_summary_final_grade">Finaler Note</string> <string name="grade_summary_final_grade">Finaler Note</string>
<string name="grade_summary_predicted_grade">Vorhergesagte Note</string> <string name="grade_summary_predicted_grade">Vorhergesagte Note</string>
<string name="grade_summary_calculated_average">Berechnender Durchschnitt</string> <string name="grade_summary_calculated_average">Berechnender Durchschnitt</string>
<string name="grade_summary_calculated_average_help_dialog_title">Wie funktioniert der berechnete Durchschnitt?</string>
<string name="grade_summary_calculated_average_help_dialog_message">The Calculated Average is the arithmetic average calculated from the subjects averages. It allows you to know the approximate final average. It is calculated in a way selected by the user in the application settings. It is recommended that you choose the appropriate option. This is because the calculation of school averages differs. Additionally, if your school reports the average of the subjects on the Vulcan page, the application downloads them and does not calculate these averages. This can be changed by forcing the calculation of the average in the application settings.\n\n<b>Average of grades only from selected semester</b>:\n1. Calculating the weighted average for each subject in a given semester\n2.Adding calculated averages\n3. Calculation of the arithmetic average of the summed averages\n\n<b>Average of averages from both semesters</b>:\n1.Calculating the weighted average for each subject in semester 1 and 2\n2. Calculating the arithmetic average of the calculated averages for semesters 1 and 2 for each subject.\n3. Adding calculated averages\n4. Calculation of the arithmetic average of the summed averages\n\n<b>Average of grades from the whole year:</b>\n1. Calculating weighted average over the year for each subject. The final average in the 1st semester is irrelevant.\n3. Adding calculated averages\n4. Calculating the arithmetic average of summed averages</string>
<string name="grade_summary_final_average_help_dialog_title">Wie funktioniert der endgültige Durchschnitt?</string>
<string name="grade_summary_final_average_help_dialog_message">The Final Average is the arithmetic average calculated from all currently available final grades in the given semester.\n\nThe calculation scheme consists of the following steps:\n1. Summing up the final grades given by teachers\n2. Divide by the number of subjects that have already been graded</string>
<string name="grade_summary_final_average">Finaler Durchschnitt</string> <string name="grade_summary_final_average">Finaler Durchschnitt</string>
<string name="grade_summary_from_subjects">aus %1$d von %2$d Schulfächern</string> <string name="grade_summary_from_subjects">aus %1$d von %2$d Schulfächern</string>
<string name="grade_menu_summary">Zusammenfassung</string> <string name="grade_menu_summary">Zusammenfassung</string>
@ -155,7 +161,7 @@
<!--Additional lessons--> <!--Additional lessons-->
<string name="additional_lessons_title">Zusätzliche Lektionen</string> <string name="additional_lessons_title">Zusätzliche Lektionen</string>
<string name="additional_lessons_button">Zusätzliche Lektionen anzeigen</string> <string name="additional_lessons_button">Zusätzliche Lektionen anzeigen</string>
<string name="additional_lessons_no_items">Keine Infos zu zusätzlichen Lektionen</string> <string name="additional_lessons_no_items">Keine Informationen über zusätzlichen Lektionen</string>
<!--Attendance--> <!--Attendance-->
<string name="attendance_summary_button">Übersicht über die Schulbesuch</string> <string name="attendance_summary_button">Übersicht über die Schulbesuch</string>
<string name="attendance_absence_school">Aus schulischen Gründen abwesend</string> <string name="attendance_absence_school">Aus schulischen Gründen abwesend</string>
@ -185,8 +191,8 @@
<item quantity="other">Neue prüfungen</item> <item quantity="other">Neue prüfungen</item>
</plurals> </plurals>
<plurals name="exam_notify_new_item_content"> <plurals name="exam_notify_new_item_content">
<item quantity="one">Du hast %d neue Prüfung erhalten</item> <item quantity="one">Du hast %d neue Prüfung</item>
<item quantity="other">Sie haben %d neue Prüfungen erhalten</item> <item quantity="other">Du hast %d neue Prüfungen</item>
</plurals> </plurals>
<plurals name="exam_number_item"> <plurals name="exam_number_item">
<item quantity="one">%d prüfung</item> <item quantity="one">%d prüfung</item>
@ -212,16 +218,13 @@
<string name="message_subject">Thema</string> <string name="message_subject">Thema</string>
<string name="message_content">Inhalt</string> <string name="message_content">Inhalt</string>
<string name="message_send_successful">Nachricht erfolgreich gesendet</string> <string name="message_send_successful">Nachricht erfolgreich gesendet</string>
<string name="message_not_exists">Nachricht existiert nicht</string> <string name="message_not_exists">Nachricht nicht vorhanden</string>
<string name="message_required_recipients">Sie müssen mindestens 1 Empfänger auswählen.</string> <string name="message_required_recipients">Sie müssen mindestens 1 Empfänger auswählen.</string>
<string name="message_content_min_length">Der Inhalt der Nachricht muss mindestens 3 Zeichen lang sein.</string> <string name="message_content_min_length">Der Inhalt der Nachricht muss mindestens 3 Zeichen lang sein.</string>
<string name="message_chip_only_unread">Nur ungelesen</string> <string name="message_chip_only_unread">Nur ungelesen</string>
<string name="message_chip_only_with_attachments">Nur mit Anhängen</string> <string name="message_chip_only_with_attachments">Nur mit Anhängen</string>
<string name="message_read">Lesen: %s</string> <string name="message_read">Lesen: %s</string>
<plurals name="message_read_by"> <string name="message_read_by">Lesen von: %1$d von %2$d Personen</string>
<item quantity="one">Lesen von: %1$d von %2$d Personen</item>
<item quantity="other">Lesen von: %1$d von %2$d Personen</item>
</plurals>
<plurals name="message_number_item"> <plurals name="message_number_item">
<item quantity="one">%d nachricht</item> <item quantity="one">%d nachricht</item>
<item quantity="other">%d nachrichten</item> <item quantity="other">%d nachrichten</item>
@ -344,7 +347,7 @@
<item quantity="one">Sie haben %1$d neue konferenz</item> <item quantity="one">Sie haben %1$d neue konferenz</item>
<item quantity="other">Sie haben %1$d neue konferenzen</item> <item quantity="other">Sie haben %1$d neue konferenzen</item>
</plurals> </plurals>
<string name="conferences_present">Present at conference</string> <string name="conferences_present">Teilnahme an einem Meeting</string>
<string name="conference_agenda">Agenda</string> <string name="conference_agenda">Agenda</string>
<!--Director information--> <!--Director information-->
<string name="school_announcement_title">Schulankündigungen</string> <string name="school_announcement_title">Schulankündigungen</string>
@ -514,7 +517,7 @@
<string name="all_yes">Ja</string> <string name="all_yes">Ja</string>
<string name="all_no">Nein</string> <string name="all_no">Nein</string>
<string name="all_save">Speichern</string> <string name="all_save">Speichern</string>
<string name="all_title">Title</string> <string name="all_title">Titel</string>
<!--Timetable Widget--> <!--Timetable Widget-->
<string name="widget_timetable_no_items">Keine Lektionen</string> <string name="widget_timetable_no_items">Keine Lektionen</string>
<string name="widget_timetable_theme_title">Thema wählen</string> <string name="widget_timetable_theme_title">Thema wählen</string>
@ -524,7 +527,7 @@
<!--Preferences--> <!--Preferences-->
<string name="pref_view_header">Aussehen &amp; Verhalten</string> <string name="pref_view_header">Aussehen &amp; Verhalten</string>
<string name="pref_view_list">Standard Ansicht</string> <string name="pref_view_list">Standard Ansicht</string>
<string name="pref_view_grade_average_mode">Berechnung des Jahresenddurchschnitts</string> <string name="pref_view_grade_average_mode">Berechnete Durchschnittsoptionen</string>
<string name="pref_view_grade_average_force_calc">Mittelwertberechnung durch App erzwingen</string> <string name="pref_view_grade_average_force_calc">Mittelwertberechnung durch App erzwingen</string>
<string name="pref_view_present">Anwesendheit zeigen</string> <string name="pref_view_present">Anwesendheit zeigen</string>
<string name="pref_view_app_theme">Thema</string> <string name="pref_view_app_theme">Thema</string>
@ -539,12 +542,18 @@
<string name="pref_notify_header">Benachrichtigungen</string> <string name="pref_notify_header">Benachrichtigungen</string>
<string name="pref_notify_switch">Benachrichtigungen anzeigen</string> <string name="pref_notify_switch">Benachrichtigungen anzeigen</string>
<string name="pref_notify_upcoming_lessons_switch">Benachrichtigungen über bevorstehende Lektionen anzeigen</string> <string name="pref_notify_upcoming_lessons_switch">Benachrichtigungen über bevorstehende Lektionen anzeigen</string>
<string name="pref_notify_upcoming_lessons_persistent_switch">Festlegen einer Benachrichtigung über die bevorstehende Lektion dauerhaft</string>
<string name="pref_notify_upcoming_lessons_persistent_summary">Deaktivieren wenn die Benachrichtigung nicht in deiner Uhr/Band angezeigt wird</string>
<string name="pref_notify_open_system_settings">Systembenachrichtigungseinstellungen öffnen</string> <string name="pref_notify_open_system_settings">Systembenachrichtigungseinstellungen öffnen</string>
<string name="pref_notify_fix_sync_issues">Synchronisierungs- und Benachrichtigungsprobleme reparieren</string> <string name="pref_notify_fix_sync_issues">Synchronisierungs- und Benachrichtigungsprobleme reparieren</string>
<string name="pref_notify_fix_sync_issues_message">Ihr Gerät hat möglicherweise Probleme mit der Datensynchronisierung und Benachrichtigungen.\n\nUm diese zu reparieren, fügen Sie Wulkanowy zum Autostart hinzu und deaktivieren Sie die Batterieoptimierung in den Systemeinstellungen des Geräts.</string> <string name="pref_notify_fix_sync_issues_message">Ihr Gerät hat möglicherweise Probleme mit der Datensynchronisierung und Benachrichtigungen.\n\nUm diese zu reparieren, fügen Sie Wulkanowy zum Autostart hinzu und deaktivieren Sie die Batterieoptimierung in den Systemeinstellungen des Geräts.</string>
<string name="pref_notify_fix_sync_issues_settings_button">Gehe zu den Einstellungen</string> <string name="pref_notify_fix_sync_issues_settings_button">Gehe zu den Einstellungen</string>
<string name="pref_notify_debug_switch">Debug-Benachrichtigungen anzeigen</string> <string name="pref_notify_debug_switch">Debug-Benachrichtigungen anzeigen</string>
<string name="pref_notify_disabled_summary">Synchronisierung ist deaktiviert</string> <string name="pref_notify_disabled_summary">Synchronisierung ist deaktiviert</string>
<string name="pref_notify_notifications_piggyback">Offizielle App-Benachrichtigungen erfassen</string>
<string name="pref_notification_piggyback_popup_title">Benachrichtigungen erfassen</string>
<string name="pref_notification_piggyback_popup_description">With this feature you can gain a substitute of push notifications like in the official app. All you need to do is allow Wulkanowy to receive all notifications in your system settings.\n\nHow it works?\nWhen you get a notification in Dziennik VULCAN, Wulkanowy will be notified (that\'s what these extra permissions are for) and will trigger a sync so that can send its own notification.\n\nFOR ADVANCED USERS ONLY</string>
<string name="pref_notification_piggyback_popup_positive">Gehe zu Einstellungen</string>
<string name="pref_services_header">Synchronisierung</string> <string name="pref_services_header">Synchronisierung</string>
<string name="pref_services_switch">Automatische Aktualisierung</string> <string name="pref_services_switch">Automatische Aktualisierung</string>
<string name="pref_services_suspended">An Feiertagen suspendiert</string> <string name="pref_services_suspended">An Feiertagen suspendiert</string>

View File

@ -41,8 +41,8 @@
<item>Kolory ocen w dzienniku</item> <item>Kolory ocen w dzienniku</item>
</string-array> </string-array>
<string-array name="grade_average_mode_entries"> <string-array name="grade_average_mode_entries">
<item>Średnia ocen z drugiego semestru</item> <item>Średnia ocen tylko z wybranego semestru</item>
<item>Średnia średnich z obu semestrów</item> <item>Średnia ze średnich z obu semestrów</item>
<item>Średnia wszystkich ocen z całego roku</item> <item>Średnia wszystkich ocen z całego roku</item>
</string-array> </string-array>
<string-array name="dashboard_tile_entries"> <string-array name="dashboard_tile_entries">

View File

@ -24,6 +24,7 @@
<string name="account_details_title">Szczegóły konta</string> <string name="account_details_title">Szczegóły konta</string>
<string name="student_info_title">Informacje o uczniu</string> <string name="student_info_title">Informacje o uczniu</string>
<string name="dashboard_title">Start</string> <string name="dashboard_title">Start</string>
<string name="notifications_center_title">Centrum powiadomień</string>
<!--Subtitles--> <!--Subtitles-->
<string name="grade_subtitle">Semestr %1$d, %2$d/%3$d</string> <string name="grade_subtitle">Semestr %1$d, %2$d/%3$d</string>
<!--Login--> <!--Login-->
@ -42,7 +43,8 @@
<string name="login_symbol_hint">Symbol</string> <string name="login_symbol_hint">Symbol</string>
<string name="login_sign_in">Zaloguj</string> <string name="login_sign_in">Zaloguj</string>
<string name="login_invalid_password">To hasło jest za krótkie</string> <string name="login_invalid_password">To hasło jest za krótkie</string>
<string name="login_incorrect_password">Dane logowania są niepoprawne. Upewnij się, że została wybrana odpowiednia odmiana dziennika UONET+ w polu poniżej</string> <string name="login_incorrect_password_default">Dane logowania są niepoprawne</string>
<string name="login_incorrect_password">%1$s. Upewnij się, że wybrano poprawną odmianę dziennika UONET+ poniżej</string>
<string name="login_invalid_pin">Nieprawidłowy PIN</string> <string name="login_invalid_pin">Nieprawidłowy PIN</string>
<string name="login_invalid_token">Nieprawidłowy token</string> <string name="login_invalid_token">Nieprawidłowy token</string>
<string name="login_expired_token">Token stracił ważność</string> <string name="login_expired_token">Token stracił ważność</string>
@ -90,6 +92,10 @@
<string name="grade_summary_final_grade">Ocena końcowa</string> <string name="grade_summary_final_grade">Ocena końcowa</string>
<string name="grade_summary_predicted_grade">Przewidywana ocena</string> <string name="grade_summary_predicted_grade">Przewidywana ocena</string>
<string name="grade_summary_calculated_average">Obliczona średnia</string> <string name="grade_summary_calculated_average">Obliczona średnia</string>
<string name="grade_summary_calculated_average_help_dialog_title">Jak działa obliczona średnia?</string>
<string name="grade_summary_calculated_average_help_dialog_message">Obliczona średnia jest średnią arytmetyczną obliczoną ze średnich przedmiotów. Pozwala ona na poznanie przybliżonej średniej końcowej. Jest obliczana w sposób wybrany przez użytkownika w ustawieniach aplikacji. Zaleca się wybranie odpowiedniej opcji. Dzieje się tak dlatego, że obliczanie średnich w szkołach różni się. Dodatkowo, jeśli twoja szkoła ma włączone średnie przedmiotów na stronie dziennika Vulcan, aplikacja pobiera je i ich nie oblicza. Można to zmienić, wymuszając obliczanie średniej w ustawieniach aplikacji.\n\n<b>Średnia ocen tylko z wybranego semestru</b>:\n1. Obliczanie średniej arytmetycznej każdego przedmiotu w danym semestrze\n2. Zsumowanie obliczonych średnich\n3. Obliczanie średniej arytmetycznej zsumowanych średnich\n\n<b>Średnia ze średnich z obu semestrów</b>:\n1.Obliczanie średniej arytmetycznej każdego przedmiotu w semestrze 1 i 2\n2. Obliczanie średniej arytmetycznej obliczonych średnich w semetrze 1 i 2 każdego przedmiotu.\n3. Zsumowanie obliczonych średnich\n4. Obliczanie średniej arytmetycznej zsumowanych średnich\n\n<b>Średnia wszystkich ocen z całego roku:</b>\n1. Obliczanie średniej arytmetycznej z każdego przedmiotu w ciągu całego roku. Końcowa ocena w 1 semestrze jest bez znaczenia.\n3. Zsumowanie obliczonych średnich\n4. Obliczanie średniej arytmetycznej z zsumowanych średnich</string>
<string name="grade_summary_final_average_help_dialog_title">Jak działa końcowa średnia?</string>
<string name="grade_summary_final_average_help_dialog_message">Średnią końcową jest średnia arytmetyczna obliczona na podstawie wszystkich obecnie dostępnych ocen końcowych w danym semestrze.\n\nSchemat obliczeń składa się z następujących kroków:\n1. Sumowanie końcowych ocen wpisanych przez nauczycieli\n2. Dzielenie przez liczbę przedmiotów, z których oceny zostały już wystawione</string>
<string name="grade_summary_final_average">Końcowa średnia</string> <string name="grade_summary_final_average">Końcowa średnia</string>
<string name="grade_summary_from_subjects">z %1$d na %2$d przedmiotów</string> <string name="grade_summary_from_subjects">z %1$d na %2$d przedmiotów</string>
<string name="grade_menu_summary">Podsumowanie</string> <string name="grade_menu_summary">Podsumowanie</string>
@ -238,12 +244,7 @@
<string name="message_chip_only_unread">Tylko nieprzeczytane</string> <string name="message_chip_only_unread">Tylko nieprzeczytane</string>
<string name="message_chip_only_with_attachments">Tylko z załącznikami</string> <string name="message_chip_only_with_attachments">Tylko z załącznikami</string>
<string name="message_read">Przeczytana: %s</string> <string name="message_read">Przeczytana: %s</string>
<plurals name="message_read_by"> <string name="message_read_by">Przeczytana przez: %1$d z %2$d osób</string>
<item quantity="one">Przeczytana przez: %1$d z %2$d osób</item>
<item quantity="few">Przeczytana przez: %1$d z %2$d osób</item>
<item quantity="many">Przeczytana przez: %1$d z %2$d osób</item>
<item quantity="other">Przeczytana przez: %1$d z %2$d osób</item>
</plurals>
<plurals name="message_number_item"> <plurals name="message_number_item">
<item quantity="one">%d wiadomość</item> <item quantity="one">%d wiadomość</item>
<item quantity="few">%d wiadomości</item> <item quantity="few">%d wiadomości</item>
@ -602,7 +603,7 @@
<!--Preferences--> <!--Preferences-->
<string name="pref_view_header">Wygląd i zachowanie aplikacji</string> <string name="pref_view_header">Wygląd i zachowanie aplikacji</string>
<string name="pref_view_list">Domyślny widok</string> <string name="pref_view_list">Domyślny widok</string>
<string name="pref_view_grade_average_mode">Obliczanie średniej końcoworocznej</string> <string name="pref_view_grade_average_mode">Opcje obliczonej średniej</string>
<string name="pref_view_grade_average_force_calc">Wymuś obliczanie średniej przez aplikację</string> <string name="pref_view_grade_average_force_calc">Wymuś obliczanie średniej przez aplikację</string>
<string name="pref_view_present">Pokazuj obecność</string> <string name="pref_view_present">Pokazuj obecność</string>
<string name="pref_view_app_theme">Motyw</string> <string name="pref_view_app_theme">Motyw</string>
@ -617,12 +618,18 @@
<string name="pref_notify_header">Powiadomienia</string> <string name="pref_notify_header">Powiadomienia</string>
<string name="pref_notify_switch">Pokazuj powiadomienia</string> <string name="pref_notify_switch">Pokazuj powiadomienia</string>
<string name="pref_notify_upcoming_lessons_switch">Pokazuj powiadomienia o nadchodzących lekcjach</string> <string name="pref_notify_upcoming_lessons_switch">Pokazuj powiadomienia o nadchodzących lekcjach</string>
<string name="pref_notify_upcoming_lessons_persistent_switch">Ustaw powiadomienie o nadchodzącej lekcji jako trwałe</string>
<string name="pref_notify_upcoming_lessons_persistent_summary">Wyłącz, gdy powiadomienie nie jest widoczne na zegarku/opasce</string>
<string name="pref_notify_open_system_settings">Otwórz systemowe ustawienia powiadomień</string> <string name="pref_notify_open_system_settings">Otwórz systemowe ustawienia powiadomień</string>
<string name="pref_notify_fix_sync_issues">Napraw problemy z synchronizacją i powiadomieniami</string> <string name="pref_notify_fix_sync_issues">Napraw problemy z synchronizacją i powiadomieniami</string>
<string name="pref_notify_fix_sync_issues_message">Na twoim urządzeniu mogą występować problemy z synchronizacją danych i powiadomieniami.\n\nBy je naprawić, dodaj Wulkanowego do autostartu i wyłącz optymalizację/oszczędzanie baterii w ustawieniach systemowych telefonu.</string> <string name="pref_notify_fix_sync_issues_message">Na twoim urządzeniu mogą występować problemy z synchronizacją danych i powiadomieniami.\n\nBy je naprawić, dodaj Wulkanowego do autostartu i wyłącz optymalizację/oszczędzanie baterii w ustawieniach systemowych telefonu.</string>
<string name="pref_notify_fix_sync_issues_settings_button">Przejdź do ustawień</string> <string name="pref_notify_fix_sync_issues_settings_button">Przejdź do ustawień</string>
<string name="pref_notify_debug_switch">Pokazuj powiadomienia debugowania</string> <string name="pref_notify_debug_switch">Pokazuj powiadomienia debugowania</string>
<string name="pref_notify_disabled_summary">Synchronizacja jest wyłączona</string> <string name="pref_notify_disabled_summary">Synchronizacja jest wyłączona</string>
<string name="pref_notify_notifications_piggyback">Przechwytywanie powiadomień oficjalnej aplikacji</string>
<string name="pref_notification_piggyback_popup_title">Przechwytywanie powiadomień</string>
<string name="pref_notification_piggyback_popup_description">Dzięki tej funkcji możesz uzyskać namiastkę powiadomień push, takich jak w oficjalnej aplikacji. Wszystko, co musisz zrobić, to zezwolić Wulkanowemu na odczytywanie wszystkich powiadomień w ustawieniach systemowych.\n\nJak to działa?\nKiedy otrzymasz powiadomienie w Dzienniczku VULCAN, Wulkanowy zostanie o tym powiadomiony (do tego jest to dodatkowe uprawnienie) i uruchomi synchronizację, aby mógł wysłać własne powiadomienie.\n\nWYŁĄCZNIE DLA ZAAWANSOWANYCH UŻYTKOWNIKÓW</string>
<string name="pref_notification_piggyback_popup_positive">Przejdź do ustawień</string>
<string name="pref_services_header">Synchronizacja</string> <string name="pref_services_header">Synchronizacja</string>
<string name="pref_services_switch">Automatyczna aktualizacja</string> <string name="pref_services_switch">Automatyczna aktualizacja</string>
<string name="pref_services_suspended">Zawieszona na wakacjach</string> <string name="pref_services_suspended">Zawieszona na wakacjach</string>

View File

@ -41,8 +41,8 @@
<item>Цвета оценок в дневнике</item> <item>Цвета оценок в дневнике</item>
</string-array> </string-array>
<string-array name="grade_average_mode_entries"> <string-array name="grade_average_mode_entries">
<item>Средняя оценка со 2 семестра</item> <item>Средние оценки только с выбранного семестра</item>
<item>Средняя оценка с двух семестров</item> <item>Средние значения для обоих семестров</item>
<item>Средняя оценок со всего года</item> <item>Средняя оценок со всего года</item>
</string-array> </string-array>
<string-array name="dashboard_tile_entries"> <string-array name="dashboard_tile_entries">

View File

@ -24,6 +24,7 @@
<string name="account_details_title">Данные аккаунта</string> <string name="account_details_title">Данные аккаунта</string>
<string name="student_info_title">Информация о студенте</string> <string name="student_info_title">Информация о студенте</string>
<string name="dashboard_title">Панель</string> <string name="dashboard_title">Панель</string>
<string name="notifications_center_title">Центр уведомлений</string>
<!--Subtitles--> <!--Subtitles-->
<string name="grade_subtitle">%1$d семестр, %2$d/%3$d</string> <string name="grade_subtitle">%1$d семестр, %2$d/%3$d</string>
<!--Login--> <!--Login-->
@ -42,7 +43,8 @@
<string name="login_symbol_hint">Symbol</string> <string name="login_symbol_hint">Symbol</string>
<string name="login_sign_in">Войти</string> <string name="login_sign_in">Войти</string>
<string name="login_invalid_password">Слишком короткий пароль</string> <string name="login_invalid_password">Слишком короткий пароль</string>
<string name="login_incorrect_password">Данные для входа неверны. Убедитесь, что в поле ниже выбран правильный вариант регистра UONET+</string> <string name="login_incorrect_password_default">Данные для входа указаны неверно</string>
<string name="login_incorrect_password">%1$s. Убедитесь, что ниже выбран правильный UONET+ вариант регистра</string>
<string name="login_invalid_pin">Неправильный PIN</string> <string name="login_invalid_pin">Неправильный PIN</string>
<string name="login_invalid_token">Неверный token</string> <string name="login_invalid_token">Неверный token</string>
<string name="login_expired_token">Token просрочен</string> <string name="login_expired_token">Token просрочен</string>
@ -90,6 +92,10 @@
<string name="grade_summary_final_grade">Итоговая оценка</string> <string name="grade_summary_final_grade">Итоговая оценка</string>
<string name="grade_summary_predicted_grade">Ожидаемая оценка</string> <string name="grade_summary_predicted_grade">Ожидаемая оценка</string>
<string name="grade_summary_calculated_average">Рассчитанная средняя оценка</string> <string name="grade_summary_calculated_average">Рассчитанная средняя оценка</string>
<string name="grade_summary_calculated_average_help_dialog_title">Как рассчитывается средняя работа?</string>
<string name="grade_summary_calculated_average_help_dialog_message">Расчетное среднее - это среднее арифметическое, рассчитанное на основе средних значений испытуемых. Это позволяет узнать приблизительное итоговое среднее значение. Он рассчитывается способом, выбранным пользователем в настройках приложения. Рекомендуется выбрать подходящий вариант. Это потому, что расчет средних показателей школы отличается. Кроме того, если ваша школа сообщает среднее значение по предметам на странице Vulcan, приложение загружает их и не вычисляет эти средние значения. Это можно изменить, принудительно вычисляя среднее значение в настройках приложения.\n\n<b>Среднее значение только за выбранный семестр </b>:\n1. Вычисление средневзвешенного значения по каждому предмету за семестр\n2.Добавление вычисленных средних\n3. Вычисление среднего арифметического суммарных средних\n\n<b>Среднее из средних значений за оба семестра</b>:\n1.Расчет средневзвешенного значения для каждого предмета в семестрах 1 и 2\n2. Вычисление среднего арифметического рассчитанных средних значений для семестров 1 и 2 по каждому предмету.\n3. Добавление вычисленных средних\n4. Расчет среднего арифметического суммированных средних\n\n<b>Среднее значение оценок за весь год: </b>\n1. Расчет средневзвешенного значения за год по каждому предмету. Итоговое среднее значение за 1 семестр не имеет значения.\n3. Добавление вычисленных средних\n4. Расчет среднего арифметического</string>
<string name="grade_summary_final_average_help_dialog_title">Как работает окончательный средний показатель?</string>
<string name="grade_summary_final_average_help_dialog_message">Среднее арифметическое - это среднее арифметическое, рассчитанное по всем имеющимся на данный момент итоговым классам данного семестра.\n\nСхема расчета состоит из следующих шагов:\n1. Суммирование итоговых классов преподавателей\n2. Деление по количеству уже оцененных предметов</string>
<string name="grade_summary_final_average">Итоговая средняя оценка</string> <string name="grade_summary_final_average">Итоговая средняя оценка</string>
<string name="grade_summary_from_subjects">от %1$d из %2$d субъектов</string> <string name="grade_summary_from_subjects">от %1$d из %2$d субъектов</string>
<string name="grade_menu_summary">Итоги</string> <string name="grade_menu_summary">Итоги</string>
@ -238,12 +244,7 @@
<string name="message_chip_only_unread">Только непрочитанные</string> <string name="message_chip_only_unread">Только непрочитанные</string>
<string name="message_chip_only_with_attachments">Только с вложениями</string> <string name="message_chip_only_with_attachments">Только с вложениями</string>
<string name="message_read">Чтение: %s</string> <string name="message_read">Чтение: %s</string>
<plurals name="message_read_by"> <string name="message_read_by">Прочитано: %1$d из %2$d человек</string>
<item quantity="one">Прочитано: %1$d из %2$d человек</item>
<item quantity="few">Прочитано: %1$d из %2$d человек</item>
<item quantity="many">Прочитано: %1$d из %2$d человек</item>
<item quantity="other">Прочитано: %1$d из %2$d человек</item>
</plurals>
<plurals name="message_number_item"> <plurals name="message_number_item">
<item quantity="one">%d сообщение</item> <item quantity="one">%d сообщение</item>
<item quantity="few">%d сообщения</item> <item quantity="few">%d сообщения</item>
@ -402,8 +403,8 @@
<item quantity="many">У вас %1$d новая конференция</item> <item quantity="many">У вас %1$d новая конференция</item>
<item quantity="other">У вас %1$d новых конференций</item> <item quantity="other">У вас %1$d новых конференций</item>
</plurals> </plurals>
<string name="conferences_present">Present at conference</string> <string name="conferences_present">Присутствует на конференции</string>
<string name="conference_agenda">Agenda</string> <string name="conference_agenda">Повестка дня</string>
<!--Director information--> <!--Director information-->
<string name="school_announcement_title">Объявления школ</string> <string name="school_announcement_title">Объявления школ</string>
<string name="school_announcement_no_items">Нет объявлений о школе</string> <string name="school_announcement_no_items">Нет объявлений о школе</string>
@ -592,7 +593,7 @@
<string name="all_yes">Да</string> <string name="all_yes">Да</string>
<string name="all_no">Нет</string> <string name="all_no">Нет</string>
<string name="all_save">Сохранить</string> <string name="all_save">Сохранить</string>
<string name="all_title">Title</string> <string name="all_title">Тема</string>
<!--Timetable Widget--> <!--Timetable Widget-->
<string name="widget_timetable_no_items">Нет уроков</string> <string name="widget_timetable_no_items">Нет уроков</string>
<string name="widget_timetable_theme_title">Выбрать тему</string> <string name="widget_timetable_theme_title">Выбрать тему</string>
@ -602,7 +603,7 @@
<!--Preferences--> <!--Preferences-->
<string name="pref_view_header">Внешний вид приложения &amp; поведение</string> <string name="pref_view_header">Внешний вид приложения &amp; поведение</string>
<string name="pref_view_list">Окно по умолчанию</string> <string name="pref_view_list">Окно по умолчанию</string>
<string name="pref_view_grade_average_mode">Способ определения средней годовой оценки</string> <string name="pref_view_grade_average_mode">Рассчитанные средние параметры</string>
<string name="pref_view_grade_average_force_calc">Принудительно высчитать среднюю оценку через приложение</string> <string name="pref_view_grade_average_force_calc">Принудительно высчитать среднюю оценку через приложение</string>
<string name="pref_view_present">Показать присутствие</string> <string name="pref_view_present">Показать присутствие</string>
<string name="pref_view_app_theme">Тема</string> <string name="pref_view_app_theme">Тема</string>
@ -617,12 +618,18 @@
<string name="pref_notify_header">Уведомления</string> <string name="pref_notify_header">Уведомления</string>
<string name="pref_notify_switch">Показывать уведомления</string> <string name="pref_notify_switch">Показывать уведомления</string>
<string name="pref_notify_upcoming_lessons_switch">Показывать уведомления о будущих уроках</string> <string name="pref_notify_upcoming_lessons_switch">Показывать уведомления о будущих уроках</string>
<string name="pref_notify_upcoming_lessons_persistent_switch">Сделать уведомления о предстоящем уроке постоянным</string>
<string name="pref_notify_upcoming_lessons_persistent_summary">Выключить, когда уведомление не отображается в чата/полосе</string>
<string name="pref_notify_open_system_settings">Открыть настройки уведомлений системы</string> <string name="pref_notify_open_system_settings">Открыть настройки уведомлений системы</string>
<string name="pref_notify_fix_sync_issues">Исправить проблемы с синхронизацией и уведомлениями</string> <string name="pref_notify_fix_sync_issues">Исправить проблемы с синхронизацией и уведомлениями</string>
<string name="pref_notify_fix_sync_issues_message">На вашем устройстве могут быть проблемы с синхронизацией данных и уведомлениями.\n\nЧтобы их исправить, вам необходимо добавить Wulkanowy в авто-старт и выключить оптимизацию/экономию батареи в настройках устройства.</string> <string name="pref_notify_fix_sync_issues_message">На вашем устройстве могут быть проблемы с синхронизацией данных и уведомлениями.\n\nЧтобы их исправить, вам необходимо добавить Wulkanowy в авто-старт и выключить оптимизацию/экономию батареи в настройках устройства.</string>
<string name="pref_notify_fix_sync_issues_settings_button">Перейти в настройски</string> <string name="pref_notify_fix_sync_issues_settings_button">Перейти в настройски</string>
<string name="pref_notify_debug_switch">Показывать дебаг-уведомления</string> <string name="pref_notify_debug_switch">Показывать дебаг-уведомления</string>
<string name="pref_notify_disabled_summary">Синхронизация отключена</string> <string name="pref_notify_disabled_summary">Синхронизация отключена</string>
<string name="pref_notify_notifications_piggyback">Записывать официальные уведомления</string>
<string name="pref_notification_piggyback_popup_title">Показывать push-уведомления</string>
<string name="pref_notification_piggyback_popup_description">С помощью этой функции вы можете получить замену push-уведомлений, как в официальном приложении. Все, что вам нужно сделать, это разрешить Wulkanowy получать все уведомления в настройках системы.\n\nКак это работает?\nКогда вы получаете уведомление в Dziennik VULCAN, Wulkanowy будет уведомлен (это требует дополнительных прав) и запустит синхронизацию, чтобы отправить свое уведомление.\n\nТОЛЬКО ДЛЯ ПОЛЬЗОВАТЕЛЯ</string>
<string name="pref_notification_piggyback_popup_positive">Перейти к настройкам</string>
<string name="pref_services_header">Синхронизация</string> <string name="pref_services_header">Синхронизация</string>
<string name="pref_services_switch">Автоматическая синхронизация</string> <string name="pref_services_switch">Автоматическая синхронизация</string>
<string name="pref_services_suspended">Приостановить синхронизации во время каникул</string> <string name="pref_services_suspended">Приостановить синхронизации во время каникул</string>

View File

@ -41,8 +41,8 @@
<item>Farby známok v denníku</item> <item>Farby známok v denníku</item>
</string-array> </string-array>
<string-array name="grade_average_mode_entries"> <string-array name="grade_average_mode_entries">
<item>Priemer známok až od druhého semestra</item> <item>Priemer známok iba z vybraného semestra</item>
<item>Priemer známok z oboch semestrov</item> <item>Priemer z priemerov z oboch semestrov</item>
<item>Priemer známok z celého roka</item> <item>Priemer známok z celého roka</item>
</string-array> </string-array>
<string-array name="dashboard_tile_entries"> <string-array name="dashboard_tile_entries">

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