1
0
mirror of https://github.com/wulkanowy/wulkanowy.git synced 2024-09-19 23:09:09 -05:00

Merge branch 'release/2.5.0'

This commit is contained in:
Mikołaj Pich 2024-03-02 21:18:10 +01:00
commit 7b2c839775
No known key found for this signature in database
110 changed files with 6862 additions and 660 deletions

4
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,4 @@
# These are supported funding model platforms
github: wulkanowy
custom: https://www.paypal.com/paypalme/wulkanowy

View File

@ -27,8 +27,8 @@ android {
testApplicationId "io.github.tests.wulkanowy" testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 34 targetSdkVersion 34
versionCode 148 versionCode 149
versionName "2.4.2" versionName "2.5.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "app_name", "Wulkanowy" resValue "string", "app_name", "Wulkanowy"
@ -164,8 +164,8 @@ play {
defaultToAppBundles = false defaultToAppBundles = false
track = 'production' track = 'production'
releaseStatus = ReleaseStatus.IN_PROGRESS releaseStatus = ReleaseStatus.IN_PROGRESS
userFraction = 0.99d userFraction = 0.20d
updatePriority = 2 updatePriority = 1
enabled.set(false) enabled.set(false)
} }
@ -187,15 +187,15 @@ huaweiPublish {
ext { ext {
work_manager = "2.9.0" work_manager = "2.9.0"
android_hilt = "1.1.0" android_hilt = "1.2.0"
room = "2.6.1" room = "2.6.1"
chucker = "4.0.0" chucker = "4.0.0"
mockk = "1.13.9" mockk = "1.13.10"
coroutines = "1.8.0" coroutines = "1.8.0"
} }
dependencies { dependencies {
implementation 'io.github.wulkanowy:sdk:2.4.1' implementation 'io.github.wulkanowy:sdk:2.5.0'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
@ -246,13 +246,13 @@ dependencies {
implementation 'com.github.Faierbel:slf4j-timber:2.0' implementation 'com.github.Faierbel:slf4j-timber:2.0'
implementation 'com.github.bastienpaulfr:Treessence:1.1.2' implementation 'com.github.bastienpaulfr:Treessence:1.1.2'
implementation "com.mikepenz:aboutlibraries-core:$about_libraries" implementation "com.mikepenz:aboutlibraries-core:$about_libraries"
implementation 'io.coil-kt:coil:2.5.0' implementation 'io.coil-kt:coil:2.6.0'
implementation "io.github.wulkanowy:AppKillerManager:3.0.1" implementation "io.github.wulkanowy:AppKillerManager:3.0.1"
implementation 'me.xdrop:fuzzywuzzy:1.4.0' implementation 'me.xdrop:fuzzywuzzy:1.4.0'
implementation 'com.fredporciuncula:flow-preferences:1.9.1' implementation 'com.fredporciuncula:flow-preferences:1.9.1'
implementation 'org.apache.commons:commons-text:1.11.0' implementation 'org.apache.commons:commons-text:1.11.0'
playImplementation platform('com.google.firebase:firebase-bom:32.7.2') playImplementation platform('com.google.firebase:firebase-bom:32.7.3')
playImplementation 'com.google.firebase:firebase-analytics' playImplementation 'com.google.firebase:firebase-analytics'
playImplementation 'com.google.firebase:firebase-messaging' playImplementation 'com.google.firebase:firebase-messaging'
playImplementation 'com.google.firebase:firebase-crashlytics:' playImplementation 'com.google.firebase:firebase-crashlytics:'

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -54,5 +54,9 @@
{ {
"displayName": "Antoni Paduch", "displayName": "Antoni Paduch",
"githubUsername": "janAte1" "githubUsername": "janAte1"
},
{
"displayName": "Kamil Wąsik",
"githubUsername": "JestemKamil"
} }
] ]

View File

@ -38,17 +38,20 @@ internal class DataModule {
@Singleton @Singleton
@Provides @Provides
fun provideSdk(chuckerInterceptor: ChuckerInterceptor, remoteConfig: RemoteConfigHelper) = fun provideSdk(
Sdk().apply { chuckerInterceptor: ChuckerInterceptor,
androidVersion = android.os.Build.VERSION.RELEASE remoteConfig: RemoteConfigHelper,
buildTag = android.os.Build.MODEL webkitCookieManagerProxy: WebkitCookieManagerProxy,
userAgentTemplate = remoteConfig.userAgentTemplate ) = Sdk().apply {
setSimpleHttpLogger { Timber.d(it) } androidVersion = android.os.Build.VERSION.RELEASE
setAdditionalCookieManager(WebkitCookieManagerProxy()) buildTag = android.os.Build.MODEL
userAgentTemplate = remoteConfig.userAgentTemplate
setSimpleHttpLogger { Timber.d(it) }
setAdditionalCookieManager(webkitCookieManagerProxy)
// for debug only // for debug only
addInterceptor(chuckerInterceptor, network = true) addInterceptor(chuckerInterceptor, network = true)
} }
@Singleton @Singleton
@Provides @Provides
@ -254,6 +257,10 @@ internal class DataModule {
@Provides @Provides
fun provideAdminMessageDao(database: AppDatabase) = database.adminMessagesDao fun provideAdminMessageDao(database: AppDatabase) = database.adminMessagesDao
@Singleton
@Provides
fun provideMutesDao(database: AppDatabase) = database.mutedMessageSendersDao
@Singleton @Singleton
@Provides @Provides
fun provideGradeDescriptiveDao(database: AppDatabase) = database.gradeDescriptiveDao fun provideGradeDescriptiveDao(database: AppDatabase) = database.gradeDescriptiveDao

View File

@ -25,6 +25,7 @@ import io.github.wulkanowy.data.db.dao.MailboxDao
import io.github.wulkanowy.data.db.dao.MessageAttachmentDao 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.MutedMessageSendersDao
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.NotificationDao
import io.github.wulkanowy.data.db.dao.RecipientDao import io.github.wulkanowy.data.db.dao.RecipientDao
@ -56,6 +57,7 @@ import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Message 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.MutedMessageSender
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.Notification
import io.github.wulkanowy.data.db.entities.Recipient import io.github.wulkanowy.data.db.entities.Recipient
@ -157,6 +159,7 @@ import javax.inject.Singleton
SchoolAnnouncement::class, SchoolAnnouncement::class,
Notification::class, Notification::class,
AdminMessage::class, AdminMessage::class,
MutedMessageSender::class,
GradeDescriptive::class, GradeDescriptive::class,
], ],
autoMigrations = [ autoMigrations = [
@ -169,6 +172,8 @@ import javax.inject.Singleton
AutoMigration(from = 56, to = 57, spec = Migration57::class), AutoMigration(from = 56, to = 57, spec = Migration57::class),
AutoMigration(from = 57, to = 58, spec = Migration58::class), AutoMigration(from = 57, to = 58, spec = Migration58::class),
AutoMigration(from = 58, to = 59), AutoMigration(from = 58, to = 59),
AutoMigration(from = 59, to = 60),
AutoMigration(from = 60, to = 61),
], ],
version = AppDatabase.VERSION_SCHEMA, version = AppDatabase.VERSION_SCHEMA,
exportSchema = true exportSchema = true
@ -177,7 +182,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
companion object { companion object {
const val VERSION_SCHEMA = 59 const val VERSION_SCHEMA = 61
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(), Migration2(),
@ -303,5 +308,7 @@ abstract class AppDatabase : RoomDatabase() {
abstract val adminMessagesDao: AdminMessageDao abstract val adminMessagesDao: AdminMessageDao
abstract val mutedMessageSendersDao: MutedMessageSendersDao
abstract val gradeDescriptiveDao: GradeDescriptiveDao abstract val gradeDescriptiveDao: GradeDescriptiveDao
} }

View File

@ -2,24 +2,14 @@ package io.github.wulkanowy.data.db.dao
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Query import androidx.room.Query
import androidx.room.Transaction
import io.github.wulkanowy.data.db.entities.AdminMessage import io.github.wulkanowy.data.db.entities.AdminMessage
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
@Dao @Dao
abstract class AdminMessageDao : BaseDao<AdminMessage> { interface AdminMessageDao : BaseDao<AdminMessage> {
@Query("SELECT * FROM AdminMessages") @Query("SELECT * FROM AdminMessages")
abstract fun loadAll(): Flow<List<AdminMessage>> fun loadAll(): Flow<List<AdminMessage>>
@Transaction
open suspend fun removeOldAndSaveNew(
oldMessages: List<AdminMessage>,
newMessages: List<AdminMessage>
) {
deleteAll(oldMessages)
insertAll(newMessages)
}
} }

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.data.db.dao
import androidx.room.Delete import androidx.room.Delete
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Transaction
import androidx.room.Update import androidx.room.Update
interface BaseDao<T> { interface BaseDao<T> {
@ -15,4 +16,10 @@ interface BaseDao<T> {
@Delete @Delete
suspend fun deleteAll(items: List<T>) suspend fun deleteAll(items: List<T>)
@Transaction
suspend fun removeOldAndSaveNew(oldItems: List<T>, newItems: List<T>) {
deleteAll(oldItems)
insertAll(newItems)
}
} }

View File

@ -5,15 +5,23 @@ import androidx.room.Query
import androidx.room.Transaction import androidx.room.Transaction
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageWithAttachment import io.github.wulkanowy.data.db.entities.MessageWithAttachment
import io.github.wulkanowy.data.db.entities.MessageWithMutedAuthor
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface MessagesDao : BaseDao<Message> { interface MessagesDao : BaseDao<Message> {
@Transaction @Transaction
@Query("SELECT * FROM Messages WHERE message_global_key = :messageGlobalKey") @Query("SELECT * FROM Messages WHERE message_global_key = :messageGlobalKey")
fun loadMessageWithAttachment(messageGlobalKey: String): Flow<MessageWithAttachment?> fun loadMessageWithAttachment(messageGlobalKey: String): Flow<MessageWithAttachment?>
@Transaction
@Query("SELECT * FROM Messages WHERE mailbox_key = :mailboxKey AND folder_id = :folder ORDER BY date DESC")
fun loadMessagesWithMutedAuthor(mailboxKey: String, folder: Int): Flow<List<MessageWithMutedAuthor>>
@Transaction
@Query("SELECT * FROM Messages WHERE email = :email AND folder_id = :folder ORDER BY date DESC")
fun loadMessagesWithMutedAuthor(folder: Int, email: String): Flow<List<MessageWithMutedAuthor>>
@Query("SELECT * FROM Messages WHERE mailbox_key = :mailboxKey AND folder_id = :folder ORDER BY date DESC") @Query("SELECT * FROM Messages WHERE mailbox_key = :mailboxKey AND folder_id = :folder ORDER BY date DESC")
fun loadAll(mailboxKey: String, folder: Int): Flow<List<Message>> fun loadAll(mailboxKey: String, folder: Int): Flow<List<Message>>

View File

@ -0,0 +1,20 @@
package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import io.github.wulkanowy.data.db.entities.MutedMessageSender
@Dao
interface MutedMessageSendersDao : BaseDao<MutedMessageSender> {
@Query("SELECT COUNT(*) FROM MutedMessageSenders WHERE author = :author")
suspend fun checkMute(author: String): Boolean
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertMute(mute: MutedMessageSender): Long
@Query("DELETE FROM MutedMessageSenders WHERE author = :author")
suspend fun deleteMute(author: String)
}

View File

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

View File

@ -2,11 +2,15 @@ package io.github.wulkanowy.data.db.entities
import androidx.room.Embedded import androidx.room.Embedded
import androidx.room.Relation import androidx.room.Relation
import java.io.Serializable
data class MessageWithAttachment( data class MessageWithAttachment(
@Embedded @Embedded
val message: Message, val message: Message,
@Relation(parentColumn = "message_global_key", entityColumn = "message_global_key") @Relation(parentColumn = "message_global_key", entityColumn = "message_global_key")
val attachments: List<MessageAttachment> val attachments: List<MessageAttachment>,
)
@Relation(parentColumn = "correspondents", entityColumn = "author")
val mutedMessageSender: MutedMessageSender?,
) : Serializable

View File

@ -0,0 +1,12 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.Embedded
import androidx.room.Relation
data class MessageWithMutedAuthor(
@Embedded
val message: Message,
@Relation(parentColumn = "correspondents", entityColumn = "author")
val mutedMessageSender: MutedMessageSender?,
)

View File

@ -0,0 +1,15 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.io.Serializable
@Entity(tableName = "MutedMessageSenders")
data class MutedMessageSender(
@ColumnInfo(name = "author")
val author: String,
) : Serializable {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}

View File

@ -16,7 +16,9 @@ data class SchoolAnnouncement(
val subject: String, val subject: String,
val content: String val content: String,
val author: String? = null,
) : Serializable { ) : Serializable {
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)

View File

@ -3,5 +3,10 @@ package io.github.wulkanowy.data.enums
enum class MessageFolder(val id: Int = 1) { enum class MessageFolder(val id: Int = 1) {
RECEIVED(1), RECEIVED(1),
SENT(2), SENT(2),
TRASHED(3) TRASHED(3),
;
companion object {
fun byId(id: Int) = entries.first { it.id == id }
}
} }

View File

@ -3,12 +3,26 @@ package io.github.wulkanowy.data.mappers
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.sdk.pojo.DirectorInformation as SdkDirectorInformation import io.github.wulkanowy.sdk.pojo.DirectorInformation as SdkDirectorInformation
import io.github.wulkanowy.sdk.pojo.LastAnnouncement as SdkLastAnnouncement
@JvmName("mapDirectorInformationToEntities")
fun List<SdkDirectorInformation>.mapToEntities(student: Student) = map { fun List<SdkDirectorInformation>.mapToEntities(student: Student) = map {
SchoolAnnouncement( SchoolAnnouncement(
userLoginId = student.userLoginId, userLoginId = student.userLoginId,
date = it.date, date = it.date,
subject = it.subject, subject = it.subject,
content = it.content, content = it.content,
author = null,
)
}
@JvmName("mapLastAnnouncementsToEntities")
fun List<SdkLastAnnouncement>.mapToEntities(student: Student) = map {
SchoolAnnouncement(
userLoginId = student.userLoginId,
date = it.date,
subject = it.subject,
content = it.content,
author = it.author,
) )
} }

View File

@ -16,10 +16,8 @@ import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.switchSemester import io.github.wulkanowy.utils.switchSemester
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.withContext
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.LocalTime import java.time.LocalTime
@ -58,23 +56,22 @@ class AttendanceRepository @Inject constructor(
attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday) attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday)
}, },
fetch = { fetch = {
val lessons = withContext(Dispatchers.IO) { val lessons = timetableDb.load(
timetableDb.load( semester.diaryId, semester.studentId, start.monday, end.sunday
semester.diaryId, semester.studentId, start.monday, end.sunday )
)
}
sdk.init(student) sdk.init(student)
.switchSemester(semester) .switchSemester(semester)
.getAttendance(start.monday, end.sunday) .getAttendance(start.monday, end.sunday)
.mapToEntities(semester, lessons) .mapToEntities(semester, lessons)
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
attendanceDb.deleteAll(old uniqueSubtract new)
val attendanceToAdd = (new uniqueSubtract old).map { newAttendance -> val attendanceToAdd = (new uniqueSubtract old).map { newAttendance ->
newAttendance.apply { if (notify) isNotified = false } newAttendance.apply { if (notify) isNotified = false }
} }
attendanceDb.insertAll(attendanceToAdd) attendanceDb.removeOldAndSaveNew(
oldItems = old uniqueSubtract new,
newItems = attendanceToAdd,
)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
}, },
filterResult = { it.filter { item -> item.date in start..end } } filterResult = { it.filter { item -> item.date in start..end } }

View File

@ -1,5 +1,7 @@
package io.github.wulkanowy.data.repositories package io.github.wulkanowy.data.repositories
import androidx.room.withTransaction
import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao
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
@ -20,6 +22,7 @@ class AttendanceSummaryRepository @Inject constructor(
private val attendanceDb: AttendanceSummaryDao, private val attendanceDb: AttendanceSummaryDao,
private val sdk: Sdk, private val sdk: Sdk,
private val refreshHelper: AutoRefreshHelper, private val refreshHelper: AutoRefreshHelper,
private val appDatabase: AppDatabase,
) { ) {
private val saveFetchResultMutex = Mutex() private val saveFetchResultMutex = Mutex()
@ -46,8 +49,10 @@ class AttendanceSummaryRepository @Inject constructor(
.mapToEntities(semester, subjectId) .mapToEntities(semester, subjectId)
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
attendanceDb.deleteAll(old uniqueSubtract new) appDatabase.withTransaction {
attendanceDb.insertAll(new uniqueSubtract old) attendanceDb.deleteAll(old uniqueSubtract new)
attendanceDb.insertAll(new uniqueSubtract old)
}
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
} }
) )

View File

@ -6,7 +6,13 @@ 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.data.networkBoundResource import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.* import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.switchSemester
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate import java.time.LocalDate
import javax.inject.Inject import javax.inject.Inject
@ -53,8 +59,10 @@ class CompletedLessonsRepository @Inject constructor(
.mapToEntities(semester) .mapToEntities(semester)
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
completedLessonsDb.deleteAll(old uniqueSubtract new) completedLessonsDb.removeOldAndSaveNew(
completedLessonsDb.insertAll(new uniqueSubtract old) oldItems = old uniqueSubtract new,
newItems = new uniqueSubtract old,
)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
}, },
filterResult = { it.filter { item -> item.date in start..end } } filterResult = { it.filter { item -> item.date in start..end } }

View File

@ -53,12 +53,12 @@ class ConferenceRepository @Inject constructor(
.filter { it.date >= startDate } .filter { it.date >= startDate }
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
val conferencesToSave = (new uniqueSubtract old).onEach { conferenceDb.removeOldAndSaveNew(
if (notify) it.isNotified = false oldItems = old uniqueSubtract new,
} newItems = (new uniqueSubtract old).onEach {
if (notify) it.isNotified = false
conferenceDb.deleteAll(old uniqueSubtract new) },
conferenceDb.insertAll(conferencesToSave) )
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
} }
) )

View File

@ -62,12 +62,12 @@ class ExamRepository @Inject constructor(
.mapToEntities(semester) .mapToEntities(semester)
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
val examsToSave = (new uniqueSubtract old).onEach { examDb.removeOldAndSaveNew(
if (notify) it.isNotified = false oldItems = old uniqueSubtract new,
} newItems = (new uniqueSubtract old).onEach {
if (notify) it.isNotified = false
examDb.deleteAll(old uniqueSubtract new) },
examDb.insertAll(examsToSave) )
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
}, },
filterResult = { it.filter { item -> item.date in start..end } } filterResult = { it.filter { item -> item.date in start..end } }

View File

@ -87,10 +87,12 @@ class GradeRepository @Inject constructor(
new: List<GradeDescriptive>, new: List<GradeDescriptive>,
notify: Boolean notify: Boolean
) { ) {
gradeDescriptiveDb.deleteAll(old uniqueSubtract new) gradeDescriptiveDb.removeOldAndSaveNew(
gradeDescriptiveDb.insertAll((new uniqueSubtract old).onEach { oldItems = old uniqueSubtract new,
if (notify) it.isNotified = false newItems = (new uniqueSubtract old).onEach {
}) if (notify) it.isNotified = false
},
)
} }
private suspend fun refreshGradeDetails( private suspend fun refreshGradeDetails(
@ -101,13 +103,16 @@ class GradeRepository @Inject constructor(
) { ) {
val notifyBreakDate = oldGrades.maxByOrNull { it.date }?.date val notifyBreakDate = oldGrades.maxByOrNull { it.date }?.date
?: student.registrationDate.toLocalDate() ?: student.registrationDate.toLocalDate()
gradeDb.deleteAll(oldGrades uniqueSubtract newDetails)
gradeDb.insertAll((newDetails uniqueSubtract oldGrades).onEach { gradeDb.removeOldAndSaveNew(
if (it.date >= notifyBreakDate) it.apply { oldItems = oldGrades uniqueSubtract newDetails,
isRead = false newItems = (newDetails uniqueSubtract oldGrades).onEach {
if (notify) isNotified = false if (it.date >= notifyBreakDate) it.apply {
} isRead = false
}) if (notify) isNotified = false
}
},
)
} }
private suspend fun refreshGradeSummaries( private suspend fun refreshGradeSummaries(
@ -115,31 +120,43 @@ class GradeRepository @Inject constructor(
newSummary: List<GradeSummary>, newSummary: List<GradeSummary>,
notify: Boolean notify: Boolean
) { ) {
gradeSummaryDb.deleteAll(oldSummaries uniqueSubtract newSummary) gradeSummaryDb.removeOldAndSaveNew(
gradeSummaryDb.insertAll((newSummary uniqueSubtract oldSummaries).onEach { summary -> oldItems = oldSummaries uniqueSubtract newSummary,
val oldSummary = oldSummaries.find { old -> old.subject == summary.subject } newItems = (newSummary uniqueSubtract oldSummaries).onEach { summary ->
summary.isPredictedGradeNotified = when { getGradeSummaryWithUpdatedNotificationState(
summary.predictedGrade.isEmpty() -> true summary = summary,
notify && oldSummary?.predictedGrade != summary.predictedGrade -> false oldSummary = oldSummaries.find { it.subject == summary.subject },
else -> true notify = notify,
} )
summary.isFinalGradeNotified = when { },
summary.finalGrade.isEmpty() -> true )
notify && oldSummary?.finalGrade != summary.finalGrade -> false }
else -> true
}
summary.predictedGradeLastChange = when { private fun getGradeSummaryWithUpdatedNotificationState(
oldSummary == null -> Instant.now() summary: GradeSummary,
summary.predictedGrade != oldSummary.predictedGrade -> Instant.now() oldSummary: GradeSummary?,
else -> oldSummary.predictedGradeLastChange notify: Boolean,
} ) {
summary.finalGradeLastChange = when { summary.isPredictedGradeNotified = when {
oldSummary == null -> Instant.now() summary.predictedGrade.isEmpty() -> true
summary.finalGrade != oldSummary.finalGrade -> Instant.now() notify && oldSummary?.predictedGrade != summary.predictedGrade -> false
else -> oldSummary.finalGradeLastChange else -> true
} }
}) summary.isFinalGradeNotified = when {
summary.finalGrade.isEmpty() -> true
notify && oldSummary?.finalGrade != summary.finalGrade -> false
else -> true
}
summary.predictedGradeLastChange = when {
oldSummary == null -> Instant.now()
summary.predictedGrade != oldSummary.predictedGrade -> Instant.now()
else -> oldSummary.predictedGradeLastChange
}
summary.finalGradeLastChange = when {
oldSummary == null -> Instant.now()
summary.finalGrade != oldSummary.finalGrade -> Instant.now()
else -> oldSummary.finalGradeLastChange
}
} }
fun getUnreadGrades(semester: Semester): Flow<List<Grade>> { fun getUnreadGrades(semester: Semester): Flow<List<Grade>> {

View File

@ -19,7 +19,7 @@ import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.switchSemester import io.github.wulkanowy.utils.switchSemester
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import java.util.* import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -62,8 +62,10 @@ class GradeStatisticsRepository @Inject constructor(
.mapToEntities(semester) .mapToEntities(semester)
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
gradePartialStatisticsDb.deleteAll(old uniqueSubtract new) gradePartialStatisticsDb.removeOldAndSaveNew(
gradePartialStatisticsDb.insertAll(new uniqueSubtract old) oldItems = old uniqueSubtract new,
newItems = new uniqueSubtract old,
)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(partialCacheKey, semester)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(partialCacheKey, semester))
}, },
mapResult = { items -> mapResult = { items ->
@ -80,6 +82,7 @@ class GradeStatisticsRepository @Inject constructor(
) )
listOf(summaryItem) + items listOf(summaryItem) + items
} }
else -> items.filter { it.subject == subjectName } else -> items.filter { it.subject == subjectName }
}.mapPartialToStatisticItems() }.mapPartialToStatisticItems()
} }
@ -107,8 +110,10 @@ class GradeStatisticsRepository @Inject constructor(
.mapToEntities(semester) .mapToEntities(semester)
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
gradeSemesterStatisticsDb.deleteAll(old uniqueSubtract new) gradeSemesterStatisticsDb.removeOldAndSaveNew(
gradeSemesterStatisticsDb.insertAll(new uniqueSubtract old) oldItems = old uniqueSubtract new,
newItems = new uniqueSubtract old,
)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(semesterCacheKey, semester)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(semesterCacheKey, semester))
}, },
mapResult = { items -> mapResult = { items ->
@ -138,6 +143,7 @@ class GradeStatisticsRepository @Inject constructor(
} }
listOf(summaryItem) + itemsWithAverage listOf(summaryItem) + itemsWithAverage
} }
else -> itemsWithAverage.filter { it.subject == subjectName } else -> itemsWithAverage.filter { it.subject == subjectName }
}.mapSemesterToStatisticItems() }.mapSemesterToStatisticItems()
} }
@ -163,8 +169,10 @@ class GradeStatisticsRepository @Inject constructor(
.mapToEntities(semester) .mapToEntities(semester)
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
gradePointsStatisticsDb.deleteAll(old uniqueSubtract new) gradePointsStatisticsDb.removeOldAndSaveNew(
gradePointsStatisticsDb.insertAll(new uniqueSubtract old) oldItems = old uniqueSubtract new,
newItems = new uniqueSubtract old,
)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(pointsCacheKey, semester)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(pointsCacheKey, semester))
}, },
mapResult = { items -> mapResult = { items ->

View File

@ -61,14 +61,14 @@ class HomeworkRepository @Inject constructor(
.mapToEntities(semester) .mapToEntities(semester)
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
val homeWorkToSave = (new uniqueSubtract old).onEach {
if (notify) it.isNotified = false
}
val filteredOld = old.filterNot { it.isAddedByUser } val filteredOld = old.filterNot { it.isAddedByUser }
homeworkDb.deleteAll(filteredOld uniqueSubtract new) homeworkDb.removeOldAndSaveNew(
homeworkDb.insertAll(homeWorkToSave) oldItems = filteredOld uniqueSubtract new,
newItems = (new uniqueSubtract old).onEach {
if (notify) it.isNotified = false
},
)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
} }
) )

View File

@ -18,7 +18,7 @@ import javax.inject.Singleton
@Singleton @Singleton
class LuckyNumberRepository @Inject constructor( class LuckyNumberRepository @Inject constructor(
private val luckyNumberDb: LuckyNumberDao, private val luckyNumberDb: LuckyNumberDao,
private val sdk: Sdk private val sdk: Sdk,
) { ) {
private val saveFetchResultMutex = Mutex() private val saveFetchResultMutex = Mutex()
@ -39,11 +39,10 @@ class LuckyNumberRepository @Inject constructor(
newLuckyNumber ?: return@networkBoundResource newLuckyNumber ?: return@networkBoundResource
if (newLuckyNumber != oldLuckyNumber) { if (newLuckyNumber != oldLuckyNumber) {
val updatedLuckNumberList = luckyNumberDb.removeOldAndSaveNew(
listOf(newLuckyNumber.apply { if (notify) isNotified = false }) oldItems = listOfNotNull(oldLuckyNumber),
newItems = listOf(newLuckyNumber.apply { if (notify) isNotified = false }),
oldLuckyNumber?.let { luckyNumberDb.deleteAll(listOfNotNull(it)) } )
luckyNumberDb.insertAll(updatedLuckNumberList)
} }
} }
) )

View File

@ -8,13 +8,17 @@ import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.db.dao.MailboxDao import io.github.wulkanowy.data.db.dao.MailboxDao
import io.github.wulkanowy.data.db.dao.MessageAttachmentDao 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.MutedMessageSendersDao
import io.github.wulkanowy.data.db.entities.Mailbox import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageWithAttachment import io.github.wulkanowy.data.db.entities.MessageWithAttachment
import io.github.wulkanowy.data.db.entities.MessageWithMutedAuthor
import io.github.wulkanowy.data.db.entities.MutedMessageSender
import io.github.wulkanowy.data.db.entities.Recipient import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageFolder import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED
import io.github.wulkanowy.data.enums.MessageFolder.SENT
import io.github.wulkanowy.data.enums.MessageFolder.TRASHED import io.github.wulkanowy.data.enums.MessageFolder.TRASHED
import io.github.wulkanowy.data.mappers.mapFromEntities import io.github.wulkanowy.data.mappers.mapFromEntities
import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.mappers.mapToEntities
@ -22,6 +26,7 @@ import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.data.onResourceError import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceSuccess import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.pojos.MessageDraft import io.github.wulkanowy.data.pojos.MessageDraft
import io.github.wulkanowy.data.toFirstResult
import io.github.wulkanowy.data.waitForResult import io.github.wulkanowy.data.waitForResult
import io.github.wulkanowy.domain.messages.GetMailboxByStudentUseCase import io.github.wulkanowy.domain.messages.GetMailboxByStudentUseCase
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
@ -31,7 +36,6 @@ 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 kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -42,6 +46,7 @@ import javax.inject.Singleton
@Singleton @Singleton
class MessageRepository @Inject constructor( class MessageRepository @Inject constructor(
private val messagesDb: MessagesDao, private val messagesDb: MessagesDao,
private val mutedMessageSendersDao: MutedMessageSendersDao,
private val messageAttachmentDao: MessageAttachmentDao, private val messageAttachmentDao: MessageAttachmentDao,
private val sdk: Sdk, private val sdk: Sdk,
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
@ -51,7 +56,6 @@ class MessageRepository @Inject constructor(
private val mailboxDao: MailboxDao, private val mailboxDao: MailboxDao,
private val getMailboxByStudentUseCase: GetMailboxByStudentUseCase, private val getMailboxByStudentUseCase: GetMailboxByStudentUseCase,
) { ) {
private val saveFetchResultMutex = Mutex() private val saveFetchResultMutex = Mutex()
private val messagesCacheKey = "message" private val messagesCacheKey = "message"
@ -63,7 +67,7 @@ class MessageRepository @Inject constructor(
folder: MessageFolder, folder: MessageFolder,
forceRefresh: Boolean, forceRefresh: Boolean,
notify: Boolean = false, notify: Boolean = false,
): Flow<Resource<List<Message>>> = networkBoundResource( ): Flow<Resource<List<MessageWithMutedAuthor>>> = networkBoundResource(
mutex = saveFetchResultMutex, mutex = saveFetchResultMutex,
isResultEmpty = { it.isEmpty() }, isResultEmpty = { it.isEmpty() },
shouldFetch = { shouldFetch = {
@ -74,8 +78,8 @@ class MessageRepository @Inject constructor(
}, },
query = { query = {
if (mailbox == null) { if (mailbox == null) {
messagesDb.loadAll(folder.id, student.email) messagesDb.loadMessagesWithMutedAuthor(folder.id, student.email)
} else messagesDb.loadAll(mailbox.globalKey, folder.id) } else messagesDb.loadMessagesWithMutedAuthor(mailbox.globalKey, folder.id)
}, },
fetch = { fetch = {
sdk.init(student).getMessages( sdk.init(student).getMessages(
@ -83,12 +87,15 @@ class MessageRepository @Inject constructor(
mailboxKey = mailbox?.globalKey, mailboxKey = mailbox?.globalKey,
).mapToEntities(student, mailbox, mailboxDao.loadAll(student.email)) ).mapToEntities(student, mailbox, mailboxDao.loadAll(student.email))
}, },
saveFetchResult = { old, new -> saveFetchResult = { oldWithAuthors, new ->
messagesDb.deleteAll(old uniqueSubtract new) val old = oldWithAuthors.map { it.message }
messagesDb.insertAll((new uniqueSubtract old).onEach { messagesDb.removeOldAndSaveNew(
it.isNotified = !notify oldItems = old uniqueSubtract new,
}) newItems = (new uniqueSubtract old).onEach {
val muted = isMuted(it.correspondents)
it.isNotified = !notify || muted
},
)
refreshHelper.updateLastRefreshTimestamp( refreshHelper.updateLastRefreshTimestamp(
getRefreshKey(messagesCacheKey, mailbox, folder) getRefreshKey(messagesCacheKey, mailbox, folder)
) )
@ -106,9 +113,7 @@ class MessageRepository @Inject constructor(
Timber.d("Message content in db empty: ${it.message.content.isBlank()}") Timber.d("Message content in db empty: ${it.message.content.isBlank()}")
(it.message.unread && markAsRead) || it.message.content.isBlank() (it.message.unread && markAsRead) || it.message.content.isBlank()
}, },
query = { query = { messagesDb.loadMessageWithAttachment(message.messageGlobalKey) },
messagesDb.loadMessageWithAttachment(message.messageGlobalKey)
},
fetch = { fetch = {
sdk.init(student).getMessageDetails( sdk.init(student).getMessageDetails(
messageKey = it!!.message.messageGlobalKey, messageKey = it!!.message.messageGlobalKey,
@ -152,17 +157,30 @@ class MessageRepository @Inject constructor(
subject: String, subject: String,
content: String, content: String,
recipients: List<Recipient>, recipients: List<Recipient>,
mailboxId: String, mailbox: Mailbox,
) { ) {
sdk.init(student).sendMessage( sdk.init(student).sendMessage(
subject = subject, subject = subject,
content = content, content = content,
recipients = recipients.mapFromEntities(), recipients = recipients.mapFromEntities(),
mailboxId = mailboxId, mailboxId = mailbox.globalKey,
) )
refreshFolders(student, mailbox, listOf(SENT))
} }
suspend fun deleteMessages(student: Student, mailbox: Mailbox?, messages: List<Message>) { suspend fun restoreMessages(student: Student, mailbox: Mailbox?, messages: List<Message>) {
sdk.init(student).restoreMessages(
messages = messages.map { it.messageGlobalKey },
)
refreshFolders(student, mailbox)
}
suspend fun deleteMessage(student: Student, message: Message) {
deleteMessages(student, listOf(message))
}
suspend fun deleteMessages(student: Student, messages: List<Message>) {
val firstMessage = messages.first() val firstMessage = messages.first()
sdk.init(student).deleteMessages( sdk.init(student).deleteMessages(
messages = messages.map { it.messageGlobalKey }, messages = messages.map { it.messageGlobalKey },
@ -181,18 +199,24 @@ class MessageRepository @Inject constructor(
} }
messagesDb.updateAll(deletedMessages) messagesDb.updateAll(deletedMessages)
} else messagesDb.deleteAll(messages) } else {
messagesDb.deleteAll(messages)
getMessages( }
student = student,
mailbox = mailbox,
folder = TRASHED,
forceRefresh = true,
).first()
} }
suspend fun deleteMessage(student: Student, mailbox: Mailbox?, message: Message) { private suspend fun refreshFolders(
deleteMessages(student, mailbox, listOf(message)) student: Student,
mailbox: Mailbox?,
folders: List<MessageFolder> = MessageFolder.entries
) {
folders.forEach {
getMessages(
student = student,
mailbox = mailbox,
folder = it,
forceRefresh = true,
).toFirstResult()
}
} }
suspend fun getMailboxes(student: Student, forceRefresh: Boolean) = networkBoundResource( suspend fun getMailboxes(student: Student, forceRefresh: Boolean) = networkBoundResource(
@ -236,4 +260,18 @@ class MessageRepository @Inject constructor(
context.getString(R.string.pref_key_message_draft), context.getString(R.string.pref_key_message_draft),
value?.let { json.encodeToString(it) } value?.let { json.encodeToString(it) }
) )
private suspend fun isMuted(author: String): Boolean {
return mutedMessageSendersDao.checkMute(author)
}
suspend fun muteMessage(author: String) {
if (isMuted(author)) return
mutedMessageSendersDao.insertMute(MutedMessageSender(author))
}
suspend fun unmuteMessage(author: String) {
if (!isMuted(author)) return
mutedMessageSendersDao.deleteMute(author)
}
} }

View File

@ -48,9 +48,10 @@ class MobileDeviceRepository @Inject constructor(
.mapToEntities(student) .mapToEntities(student)
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
mobileDb.deleteAll(old uniqueSubtract new) mobileDb.removeOldAndSaveNew(
mobileDb.insertAll(new uniqueSubtract old) oldItems = old uniqueSubtract new,
newItems = new uniqueSubtract old,
)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
} }
) )

View File

@ -7,7 +7,12 @@ 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.data.networkBoundResource import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.* import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.switchSemester
import io.github.wulkanowy.utils.toLocalDate
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject import javax.inject.Inject
@ -46,14 +51,16 @@ class NoteRepository @Inject constructor(
.mapToEntities(semester) .mapToEntities(semester)
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
noteDb.deleteAll(old uniqueSubtract new) val notesToAdd = (new uniqueSubtract old).onEach {
noteDb.insertAll((new uniqueSubtract old).onEach {
if (it.date >= student.registrationDate.toLocalDate()) it.apply { if (it.date >= student.registrationDate.toLocalDate()) it.apply {
isRead = false isRead = false
if (notify) isNotified = false if (notify) isNotified = false
} }
}) }
noteDb.removeOldAndSaveNew(
oldItems = old uniqueSubtract new,
newItems = notesToAdd,
)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
} }
) )

View File

@ -1,7 +1,11 @@
package io.github.wulkanowy.data.repositories package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.db.dao.RecipientDao import io.github.wulkanowy.data.db.dao.RecipientDao
import io.github.wulkanowy.data.db.entities.* import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.MailboxType
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.Recipient
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.AutoRefreshHelper
@ -25,8 +29,10 @@ class RecipientRepository @Inject constructor(
.mapToEntities(mailbox.globalKey) .mapToEntities(mailbox.globalKey)
val old = recipientDb.loadAll(type, mailbox.globalKey) val old = recipientDb.loadAll(type, mailbox.globalKey)
recipientDb.deleteAll(old uniqueSubtract new) recipientDb.removeOldAndSaveNew(
recipientDb.insertAll(new uniqueSubtract old) oldItems = old uniqueSubtract new,
newItems = new uniqueSubtract old,
)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
} }

View File

@ -41,17 +41,18 @@ class SchoolAnnouncementRepository @Inject constructor(
schoolAnnouncementDb.loadAll(student.userLoginId) schoolAnnouncementDb.loadAll(student.userLoginId)
}, },
fetch = { fetch = {
sdk.init(student) val sdk = sdk.init(student)
.getDirectorInformation() val lastAnnouncements = sdk.getLastAnnouncements().mapToEntities(student)
.mapToEntities(student) val directorInformation = sdk.getDirectorInformation().mapToEntities(student)
lastAnnouncements + directorInformation
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
val schoolAnnouncementsToSave = (new uniqueSubtract old).onEach { schoolAnnouncementDb.removeOldAndSaveNew(
if (notify) it.isNotified = false oldItems = old uniqueSubtract new,
} newItems = (new uniqueSubtract old).onEach {
if (notify) it.isNotified = false
schoolAnnouncementDb.deleteAll(old uniqueSubtract new) },
schoolAnnouncementDb.insertAll(schoolAnnouncementsToSave) )
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
} }
) )

View File

@ -47,10 +47,10 @@ class SchoolRepository @Inject constructor(
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
if (old != null && new != old) { if (old != null && new != old) {
with(schoolDb) { schoolDb.removeOldAndSaveNew(
deleteAll(listOf(old)) oldItems = listOf(old),
insertAll(listOf(new)) newItems = listOf(new)
} )
} else if (old == null) { } else if (old == null) {
schoolDb.insertAll(listOf(new)) schoolDb.insertAll(listOf(new))
} }

View File

@ -5,7 +5,11 @@ 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.* import io.github.wulkanowy.utils.DispatchersProvider
import io.github.wulkanowy.utils.getCurrentOrLast
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.isCurrent
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -15,7 +19,7 @@ import javax.inject.Singleton
class SemesterRepository @Inject constructor( class SemesterRepository @Inject constructor(
private val semesterDb: SemesterDao, private val semesterDb: SemesterDao,
private val sdk: Sdk, private val sdk: Sdk,
private val dispatchers: DispatchersProvider private val dispatchers: DispatchersProvider,
) { ) {
suspend fun getSemesters( suspend fun getSemesters(
@ -45,6 +49,7 @@ class SemesterRepository @Inject constructor(
0 == it.diaryId && 0 == it.kindergartenDiaryId 0 == it.diaryId && 0 == it.kindergartenDiaryId
} == true } == true
} }
else -> false else -> false
} }
@ -59,8 +64,10 @@ class SemesterRepository @Inject constructor(
if (new.isEmpty()) return Timber.i("Empty semester list!") if (new.isEmpty()) return Timber.i("Empty semester list!")
val old = semesterDb.loadAll(student.studentId, student.classId) val old = semesterDb.loadAll(student.studentId, student.classId)
semesterDb.deleteAll(old.uniqueSubtract(new)) semesterDb.removeOldAndSaveNew(
semesterDb.insertSemesters(new.uniqueSubtract(old)) oldItems = old uniqueSubtract new,
newItems = new uniqueSubtract old,
)
} }
suspend fun getCurrentSemester(student: Student, forceRefresh: Boolean = false) = suspend fun getCurrentSemester(student: Student, forceRefresh: Boolean = false) =

View File

@ -15,7 +15,7 @@ import javax.inject.Singleton
@Singleton @Singleton
class StudentInfoRepository @Inject constructor( class StudentInfoRepository @Inject constructor(
private val studentInfoDao: StudentInfoDao, private val studentInfoDao: StudentInfoDao,
private val sdk: Sdk private val sdk: Sdk,
) { ) {
private val saveFetchResultMutex = Mutex() private val saveFetchResultMutex = Mutex()
@ -36,10 +36,10 @@ class StudentInfoRepository @Inject constructor(
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
if (old != null && new != old) { if (old != null && new != old) {
with(studentInfoDao) { studentInfoDao.removeOldAndSaveNew(
deleteAll(listOf(old)) oldItems = listOf(old),
insertAll(listOf(new)) newItems = listOf(new),
} )
} else if (old == null) { } else if (old == null) {
studentInfoDao.insertAll(listOf(new)) studentInfoDao.insertAll(listOf(new))
} }

View File

@ -45,9 +45,10 @@ class SubjectRepository @Inject constructor(
.mapToEntities(semester) .mapToEntities(semester)
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
subjectDao.deleteAll(old uniqueSubtract new) subjectDao.removeOldAndSaveNew(
subjectDao.insertAll(new uniqueSubtract old) oldItems = old uniqueSubtract new,
newItems = new uniqueSubtract old
)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
} }
) )

View File

@ -45,9 +45,10 @@ class TeacherRepository @Inject constructor(
.mapToEntities(semester) .mapToEntities(semester)
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
teacherDb.deleteAll(old uniqueSubtract new) teacherDb.removeOldAndSaveNew(
teacherDb.insertAll(new uniqueSubtract old) oldItems = old uniqueSubtract new,
newItems = new uniqueSubtract old,
)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
} }
) )

View File

@ -3,13 +3,23 @@ package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.db.dao.TimetableAdditionalDao import io.github.wulkanowy.data.db.dao.TimetableAdditionalDao
import io.github.wulkanowy.data.db.dao.TimetableDao import io.github.wulkanowy.data.db.dao.TimetableDao
import io.github.wulkanowy.data.db.dao.TimetableHeaderDao import io.github.wulkanowy.data.db.dao.TimetableHeaderDao
import io.github.wulkanowy.data.db.entities.* import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.db.entities.TimetableAdditional
import io.github.wulkanowy.data.db.entities.TimetableHeader
import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.data.pojos.TimetableFull import io.github.wulkanowy.data.pojos.TimetableFull
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper
import io.github.wulkanowy.utils.* import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.switchSemester
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
@ -121,12 +131,12 @@ class TimetableRepository @Inject constructor(
} }
} }
fun getTimetableFromDatabase( suspend fun getTimetableFromDatabase(
semester: Semester, semester: Semester,
from: LocalDate, start: LocalDate,
end: LocalDate end: LocalDate
): Flow<List<Timetable>> { ): List<Timetable> {
return timetableDb.loadAll(semester.diaryId, semester.studentId, from, end) return timetableDb.load(semester.diaryId, semester.studentId, start, end)
} }
suspend fun updateTimetable(timetable: List<Timetable>) { suspend fun updateTimetable(timetable: List<Timetable>) {
@ -144,8 +154,10 @@ class TimetableRepository @Inject constructor(
new.apply { if (notify) isNotified = false } new.apply { if (notify) isNotified = false }
} }
timetableDb.deleteAll(lessonsToRemove) timetableDb.removeOldAndSaveNew(
timetableDb.insertAll(lessonsToAdd) oldItems = lessonsToRemove,
newItems = lessonsToAdd,
)
schedulerHelper.cancelScheduled(lessonsToRemove, student) schedulerHelper.cancelScheduled(lessonsToRemove, student)
schedulerHelper.scheduleNotifications(lessonsToAdd, student) schedulerHelper.scheduleNotifications(lessonsToAdd, student)
@ -156,13 +168,17 @@ class TimetableRepository @Inject constructor(
new: List<TimetableAdditional> new: List<TimetableAdditional>
) { ) {
val oldFiltered = old.filter { !it.isAddedByUser } val oldFiltered = old.filter { !it.isAddedByUser }
timetableAdditionalDb.deleteAll(oldFiltered uniqueSubtract new) timetableAdditionalDb.removeOldAndSaveNew(
timetableAdditionalDb.insertAll(new uniqueSubtract old) oldItems = oldFiltered uniqueSubtract new,
newItems = new uniqueSubtract old,
)
} }
private suspend fun refreshDayHeaders(old: List<TimetableHeader>, new: List<TimetableHeader>) { private suspend fun refreshDayHeaders(old: List<TimetableHeader>, new: List<TimetableHeader>) {
timetableHeaderDb.deleteAll(old uniqueSubtract new) timetableHeaderDb.removeOldAndSaveNew(
timetableHeaderDb.insertAll(new uniqueSubtract old) oldItems = old uniqueSubtract new,
newItems = new uniqueSubtract old,
)
} }
fun getLastRefreshTimestamp(semester: Semester, start: LocalDate, end: LocalDate): Instant { fun getLastRefreshTimestamp(semester: Semester, start: LocalDate, end: LocalDate): Instant {

View File

@ -1,10 +1,7 @@
package io.github.wulkanowy.domain.timetable package io.github.wulkanowy.domain.timetable
import io.github.wulkanowy.data.dataOrNull
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.repositories.TimetableRepository import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.data.toFirstResult
import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.utils.sunday
import java.time.LocalDate import java.time.LocalDate
@ -16,18 +13,14 @@ class IsStudentHasLessonsOnWeekendUseCase @Inject constructor(
) { ) {
suspend operator fun invoke( suspend operator fun invoke(
student: Student,
semester: Semester, semester: Semester,
currentDate: LocalDate = LocalDate.now(), currentDate: LocalDate = LocalDate.now(),
): Boolean { ): Boolean {
val lessons = timetableRepository.getTimetable( val lessons = timetableRepository.getTimetableFromDatabase(
student = student,
semester = semester, semester = semester,
start = currentDate.monday, start = currentDate.monday,
end = currentDate.sunday, end = currentDate.sunday,
forceRefresh = false, )
timetableType = TimetableRepository.TimetableType.NORMAL
).toFirstResult().dataOrNull?.lessons.orEmpty()
return isWeekendHasLessonsUseCase(lessons) return isWeekendHasLessonsUseCase(lessons)
} }
} }

View File

@ -6,7 +6,6 @@ import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.data.waitForResult import io.github.wulkanowy.data.waitForResult
import io.github.wulkanowy.services.sync.notifications.ChangeTimetableNotification import io.github.wulkanowy.services.sync.notifications.ChangeTimetableNotification
import io.github.wulkanowy.utils.nextOrSameSchoolDay import io.github.wulkanowy.utils.nextOrSameSchoolDay
import kotlinx.coroutines.flow.first
import java.time.LocalDate.now import java.time.LocalDate.now
import javax.inject.Inject import javax.inject.Inject
@ -31,10 +30,9 @@ class TimetableWork @Inject constructor(
timetableRepository.getTimetableFromDatabase( timetableRepository.getTimetableFromDatabase(
semester = semester, semester = semester,
from = startDate, start = startDate,
end = endDate, end = endDate,
) )
.first()
.filterNot { it.isNotified } .filterNot { it.isNotified }
.let { .let {
if (it.isNotEmpty()) changeTimetableNotification.notify(it, student) if (it.isNotEmpty()) changeTimetableNotification.notify(it, student)

View File

@ -17,6 +17,8 @@ import io.github.wulkanowy.utils.FragmentLifecycleLogger
import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.lifecycleAwareVariable import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.openInternetBrowser import io.github.wulkanowy.utils.openInternetBrowser
import timber.log.Timber
import java.time.Instant
import javax.inject.Inject import javax.inject.Inject
abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> : abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
@ -36,6 +38,8 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
abstract var presenter: T abstract var presenter: T
private var lastDialogOpenTime = mutableMapOf<String, Instant>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
inject() inject()
themeManager.applyActivityTheme(this) themeManager.applyActivityTheme(this)
@ -70,6 +74,8 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
} }
override fun showExpiredCredentialsDialog() { override fun showExpiredCredentialsDialog() {
if (!shouldShowDialog(DIALOG_ERROR_BAD_CREDENTIALS)) return
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)
.setTitle(R.string.main_expired_credentials_title) .setTitle(R.string.main_expired_credentials_title)
.setMessage(R.string.main_expired_credentials_description) .setMessage(R.string.main_expired_credentials_description)
@ -83,6 +89,8 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
} }
override fun showDecryptionFailedDialog() { override fun showDecryptionFailedDialog() {
if (!shouldShowDialog(DIALOG_ERROR_DECRYPTION_FAILED)) return
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)
.setTitle(R.string.main_session_expired) .setTitle(R.string.main_session_expired)
.setMessage(R.string.main_session_relogin) .setMessage(R.string.main_session_relogin)
@ -119,4 +127,21 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
protected open fun inject() { protected open fun inject() {
throw UnsupportedOperationException() throw UnsupportedOperationException()
} }
private fun shouldShowDialog(name: String): Boolean {
val lastOpenTime = lastDialogOpenTime[name]
val now = Instant.now()
if (lastOpenTime != null && now.isBefore(lastOpenTime.plusSeconds(1))) {
Timber.i("Dialog $name was shown less than a second ago. Skip")
return false
}
lastDialogOpenTime[name] = Instant.now()
return true
}
companion object {
private const val DIALOG_ERROR_BAD_CREDENTIALS = "dialog_error_bad_credentials"
private const val DIALOG_ERROR_DECRYPTION_FAILED = "dialog_error_decryption_failed"
}
} }

View File

@ -34,7 +34,7 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co
} }
protected open fun proceed(error: Throwable) { protected open fun proceed(error: Throwable) {
showErrorMessage(context.resources.getErrorString(error), error) showDefaultMessage(error)
when (error) { when (error) {
is PasswordChangeRequiredException -> onPasswordChangeRequired(error.redirectUrl) is PasswordChangeRequiredException -> onPasswordChangeRequired(error.redirectUrl)
is ScramblerException -> onDecryptionFailed() is ScramblerException -> onDecryptionFailed()
@ -45,6 +45,10 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co
} }
} }
fun showDefaultMessage(error: Throwable) {
showErrorMessage(context.resources.getErrorString(error), error)
}
open fun clear() { open fun clear() {
showErrorMessage = { _, _ -> } showErrorMessage = { _, _ -> }
onExpiredCredentials = {} onExpiredCredentials = {}

View File

@ -4,18 +4,14 @@ import android.annotation.SuppressLint
import io.github.wulkanowy.data.* import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.db.entities.Attendance
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.Timetable
import io.github.wulkanowy.data.repositories.AttendanceRepository import io.github.wulkanowy.data.repositories.AttendanceRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.* import io.github.wulkanowy.utils.*
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import timber.log.Timber import timber.log.Timber
import java.time.DayOfWeek import java.time.DayOfWeek
@ -210,7 +206,7 @@ class AttendancePresenter @Inject constructor(
val semester = semesterRepository.getCurrentSemester(student) val semester = semesterRepository.getCurrentSemester(student)
checkInitialAndCurrentDate(student, semester) checkInitialAndCurrentDate(semester)
attendanceRepository.getAttendance( attendanceRepository.getAttendance(
student = student, student = student,
semester = semester, semester = semester,
@ -266,15 +262,13 @@ class AttendancePresenter @Inject constructor(
.launch() .launch()
} }
private suspend fun checkInitialAndCurrentDate(student: Student, semester: Semester) { private suspend fun checkInitialAndCurrentDate(semester: Semester) {
if (initialDate == null) { if (initialDate == null) {
val lessons = attendanceRepository.getAttendance( val lessons = attendanceRepository.getAttendanceFromDatabase(
student = student,
semester = semester, semester = semester,
start = now().monday, start = now().monday,
end = now().sunday, end = now().sunday,
forceRefresh = false, ).firstOrNull().orEmpty()
).toFirstResult().dataOrNull.orEmpty()
isWeekendHasLessons = isWeekendHasLessons(lessons) isWeekendHasLessons = isWeekendHasLessons(lessons)
initialDate = getInitialDate(semester) initialDate = getInitialDate(semester)
} }
@ -316,6 +310,7 @@ class AttendancePresenter @Inject constructor(
showContent(false) showContent(false)
showExcuseButton(false) showExcuseButton(false)
} }
is Resource.Success -> { is Resource.Success -> {
Timber.i("Excusing for absence result: Success") Timber.i("Excusing for absence result: Success")
analytics.logEvent("excuse_absence", "items" to attendanceToExcuseList.size) analytics.logEvent("excuse_absence", "items" to attendanceToExcuseList.size)
@ -328,6 +323,7 @@ class AttendancePresenter @Inject constructor(
} }
loadData(forceRefresh = true) loadData(forceRefresh = true)
} }
is Resource.Error -> { is Resource.Error -> {
Timber.i("Excusing for absence result: An exception occurred") Timber.i("Excusing for absence result: An exception occurred")
errorHandler.dispatch(it.error) errorHandler.dispatch(it.error)

View File

@ -62,7 +62,11 @@ class AuthPresenter @Inject constructor(
} }
isSuccess isSuccess
} }
.onFailure { errorHandler.dispatch(it) } .onFailure {
errorHandler.dispatch(it)
view?.showProgress(false)
view?.showContent(true)
}
.onSuccess { .onSuccess {
if (it) { if (it) {
view?.showSuccess(true) view?.showSuccess(true)

View File

@ -13,6 +13,7 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.DialogCaptchaBinding import io.github.wulkanowy.databinding.DialogCaptchaBinding
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.ui.base.BaseDialogFragment import io.github.wulkanowy.ui.base.BaseDialogFragment
import io.github.wulkanowy.utils.WebkitCookieManagerProxy
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -22,6 +23,9 @@ class CaptchaDialog : BaseDialogFragment<DialogCaptchaBinding>() {
@Inject @Inject
lateinit var sdk: Sdk lateinit var sdk: Sdk
@Inject
lateinit var webkitCookieManagerProxy: WebkitCookieManagerProxy
private var webView: WebView? = null private var webView: WebView? = null
companion object { companion object {
@ -80,6 +84,7 @@ class CaptchaDialog : BaseDialogFragment<DialogCaptchaBinding>() {
} }
override fun onDestroy() { override fun onDestroy() {
webkitCookieManagerProxy.webkitCookieManager?.flush()
webView?.destroy() webView?.destroy()
super.onDestroy() super.onDestroy()
} }

View File

@ -304,6 +304,7 @@ class DashboardPresenter @Inject constructor(
forceRefresh = forceRefresh forceRefresh = forceRefresh
) )
} }
.mapResourceData { it.map { messageWithAuthor -> messageWithAuthor.message } }
.onResourceError { errorHandler.dispatch(it) } .onResourceError { errorHandler.dispatch(it) }
.takeIf { DashboardItem.Tile.MESSAGES in selectedTiles } ?: flowSuccess .takeIf { DashboardItem.Tile.MESSAGES in selectedTiles } ?: flowSuccess
@ -438,7 +439,7 @@ class DashboardPresenter @Inject constructor(
private fun loadLessons(student: Student, forceRefresh: Boolean) { private fun loadLessons(student: Student, forceRefresh: Boolean) {
flatResourceFlow { flatResourceFlow {
val semester = semesterRepository.getCurrentSemester(student) val semester = semesterRepository.getCurrentSemester(student)
val date = when (isStudentHasLessonsOnWeekendUseCase(student, semester)) { val date = when (isStudentHasLessonsOnWeekendUseCase(semester)) {
true -> LocalDate.now() true -> LocalDate.now()
else -> LocalDate.now().nextOrSameSchoolDay else -> LocalDate.now().nextOrSameSchoolDay
} }

View File

@ -159,7 +159,7 @@ class GradeAverageProvider @Inject constructor(
?.updateModifiers(student, config).orEmpty() ?.updateModifiers(student, config).orEmpty()
(updatedSecondSemesterGrades + updatedFirstSemesterGrades).calcAverage( (updatedSecondSemesterGrades + updatedFirstSemesterGrades).calcAverage(
config.isOptionalArithmeticAverage isOptionalArithmeticAverage = config.isOptionalArithmeticAverage,
) )
} else { } else {
secondSemesterSubject.average secondSemesterSubject.average
@ -173,13 +173,21 @@ class GradeAverageProvider @Inject constructor(
config: AverageCalcParams, config: AverageCalcParams,
): Double { ): Double {
return if (!isAnyVulcanAverage || config.forceAverageCalc) { return if (!isAnyVulcanAverage || config.forceAverageCalc) {
val divider = if (secondSemesterSubject.grades.any { it.weightValue > .0 }) 2 else 1 val isSecondSemesterHasWeightGrade = secondSemesterSubject.grades
.any { it.weightValue > .0 }
val isSecondSemesterHasArithmeticGrade = secondSemesterSubject.grades
.all { it.weightValue == .0 } && config.isOptionalArithmeticAverage
val isSecondSemesterHaveAverage =
isSecondSemesterHasWeightGrade || isSecondSemesterHasArithmeticGrade
val divider = if (isSecondSemesterHaveAverage) 2 else 1
val secondSemesterAverage = secondSemesterSubject.grades val secondSemesterAverage = secondSemesterSubject.grades
.updateModifiers(student, config) .updateModifiers(student, config)
.calcAverage(config.isOptionalArithmeticAverage) .calcAverage(isOptionalArithmeticAverage = config.isOptionalArithmeticAverage)
val firstSemesterAverage = firstSemesterSubject?.grades val firstSemesterAverage = firstSemesterSubject?.grades
?.updateModifiers(student, config) ?.updateModifiers(student, config)
?.calcAverage(config.isOptionalArithmeticAverage) ?: secondSemesterAverage ?.calcAverage(isOptionalArithmeticAverage = config.isOptionalArithmeticAverage)
?: secondSemesterAverage
(secondSemesterAverage + firstSemesterAverage) / divider (secondSemesterAverage + firstSemesterAverage) / divider
} else { } else {
@ -225,7 +233,7 @@ class GradeAverageProvider @Inject constructor(
subject = summary.subject, subject = summary.subject,
average = if (!isAnyAverage || params.forceAverageCalc) { average = if (!isAnyAverage || params.forceAverageCalc) {
grades.updateModifiers(student, params) grades.updateModifiers(student, params)
.calcAverage(params.isOptionalArithmeticAverage) .calcAverage(isOptionalArithmeticAverage = params.isOptionalArithmeticAverage)
} else summary.average, } else summary.average,
points = summary.pointsSum, points = summary.pointsSum,
summary = summary, summary = summary,
@ -286,8 +294,13 @@ class GradeAverageProvider @Inject constructor(
proposedPoints = "", proposedPoints = "",
finalPoints = "", finalPoints = "",
pointsSum = "", pointsSum = "",
average = if (calcAverage) details.updateModifiers(student, params) average = when {
.calcAverage(params.isOptionalArithmeticAverage) else .0 calcAverage -> details
.updateModifiers(student, params)
.calcAverage(isOptionalArithmeticAverage = params.isOptionalArithmeticAverage)
else -> .0
}
) )
} }
} }

View File

@ -14,6 +14,7 @@ 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.data.resourceFlow import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
@ -204,6 +205,9 @@ class LoginFormPresenter @Inject constructor(
} }
.onResourceError { .onResourceError {
loginErrorHandler.dispatch(it) loginErrorHandler.dispatch(it)
if (it is InvalidSymbolException) {
loginErrorHandler.showDefaultMessage(it)
}
lastError = it lastError = it
view?.showContact(true) view?.showContact(true)
analytics.logEvent( analytics.logEvent(

View File

@ -50,12 +50,15 @@ class MessagePreviewAdapter @Inject constructor() :
ViewType.MESSAGE.id -> MessageViewHolder( ViewType.MESSAGE.id -> MessageViewHolder(
ItemMessagePreviewBinding.inflate(inflater, parent, false) ItemMessagePreviewBinding.inflate(inflater, parent, false)
) )
ViewType.DIVIDER.id -> DividerViewHolder( ViewType.DIVIDER.id -> DividerViewHolder(
ItemMessageDividerBinding.inflate(inflater, parent, false) ItemMessageDividerBinding.inflate(inflater, parent, false)
) )
ViewType.ATTACHMENT.id -> AttachmentViewHolder( ViewType.ATTACHMENT.id -> AttachmentViewHolder(
ItemMessageAttachmentBinding.inflate(inflater, parent, false) ItemMessageAttachmentBinding.inflate(inflater, parent, false)
) )
else -> throw IllegalStateException() else -> throw IllegalStateException()
} }
} }
@ -66,6 +69,7 @@ class MessagePreviewAdapter @Inject constructor() :
holder, holder,
requireNotNull(messageWithAttachment).message requireNotNull(messageWithAttachment).message
) )
is AttachmentViewHolder -> bindAttachment( is AttachmentViewHolder -> bindAttachment(
holder, holder,
requireNotNull(messageWithAttachment).attachments[position - 2] requireNotNull(messageWithAttachment).attachments[position - 2]
@ -82,9 +86,11 @@ class MessagePreviewAdapter @Inject constructor() :
recipientCount > 1 -> { recipientCount > 1 -> {
context.getString(R.string.message_read_by, message.readBy, recipientCount) context.getString(R.string.message_read_by, message.readBy, recipientCount)
} }
message.readBy == 1 || (isReceived && !message.unread) -> { message.readBy == 1 || (isReceived && !message.unread) -> {
context.getString(R.string.message_read, context.getString(R.string.all_yes)) context.getString(R.string.message_read, context.getString(R.string.all_yes))
} }
else -> context.getString(R.string.message_read, context.getString(R.string.all_no)) else -> context.getString(R.string.message_read, context.getString(R.string.all_no))
} }

View File

@ -44,18 +44,33 @@ class MessagePreviewFragment :
private var menuForwardButton: MenuItem? = null private var menuForwardButton: MenuItem? = null
private var menuRestoreButton: MenuItem? = null
private var menuDeleteButton: MenuItem? = null private var menuDeleteButton: MenuItem? = null
private var menuDeleteForeverButton: MenuItem? = null
private var menuShareButton: MenuItem? = null private var menuShareButton: MenuItem? = null
private var menuPrintButton: MenuItem? = null private var menuPrintButton: MenuItem? = null
private var menuMuteButton: MenuItem? = null
override val titleStringId: Int override val titleStringId: Int
get() = R.string.message_title get() = R.string.message_title
override val deleteMessageSuccessString: String override val deleteMessageSuccessString: String
get() = getString(R.string.message_delete_success) get() = getString(R.string.message_delete_success)
override val muteMessageSuccessString: String
get() = getString(R.string.message_mute_success)
override val unmuteMessageSuccessString: String
get() = getString(R.string.message_unmute_success)
override val restoreMessageSuccessString: String
get() = getString(R.string.message_restore_success)
override val messageNoSubjectString: String override val messageNoSubjectString: String
get() = getString(R.string.message_no_subject) get() = getString(R.string.message_no_subject)
@ -103,9 +118,12 @@ class MessagePreviewFragment :
inflater.inflate(R.menu.action_menu_message_preview, menu) inflater.inflate(R.menu.action_menu_message_preview, menu)
menuReplyButton = menu.findItem(R.id.messagePreviewMenuReply) menuReplyButton = menu.findItem(R.id.messagePreviewMenuReply)
menuForwardButton = menu.findItem(R.id.messagePreviewMenuForward) menuForwardButton = menu.findItem(R.id.messagePreviewMenuForward)
menuRestoreButton = menu.findItem(R.id.messagePreviewMenuRestore)
menuDeleteButton = menu.findItem(R.id.messagePreviewMenuDelete) menuDeleteButton = menu.findItem(R.id.messagePreviewMenuDelete)
menuDeleteForeverButton = menu.findItem(R.id.messagePreviewMenuDeleteForever)
menuShareButton = menu.findItem(R.id.messagePreviewMenuShare) menuShareButton = menu.findItem(R.id.messagePreviewMenuShare)
menuPrintButton = menu.findItem(R.id.messagePreviewMenuPrint) menuPrintButton = menu.findItem(R.id.messagePreviewMenuPrint)
menuMuteButton = menu.findItem(R.id.messagePreviewMenuMute)
presenter.onCreateOptionsMenu() presenter.onCreateOptionsMenu()
menu.findItem(R.id.mainMenuAccount).isVisible = false menu.findItem(R.id.mainMenuAccount).isVisible = false
@ -115,9 +133,12 @@ class MessagePreviewFragment :
return when (item.itemId) { return when (item.itemId) {
R.id.messagePreviewMenuReply -> presenter.onReply() R.id.messagePreviewMenuReply -> presenter.onReply()
R.id.messagePreviewMenuForward -> presenter.onForward() R.id.messagePreviewMenuForward -> presenter.onForward()
R.id.messagePreviewMenuRestore -> presenter.onMessageRestore()
R.id.messagePreviewMenuDelete -> presenter.onMessageDelete() R.id.messagePreviewMenuDelete -> presenter.onMessageDelete()
R.id.messagePreviewMenuDeleteForever -> presenter.onMessageDelete()
R.id.messagePreviewMenuShare -> presenter.onShare() R.id.messagePreviewMenuShare -> presenter.onShare()
R.id.messagePreviewMenuPrint -> presenter.onPrint() R.id.messagePreviewMenuPrint -> presenter.onPrint()
R.id.messagePreviewMenuMute -> presenter.onMute()
else -> false else -> false
} }
} }
@ -129,6 +150,11 @@ class MessagePreviewFragment :
} }
} }
override fun updateMuteToggleButton(isMuted: Boolean) {
menuMuteButton?.setTitle(if (isMuted) R.string.message_unmute else R.string.message_mute)
}
override fun showProgress(show: Boolean) { override fun showProgress(show: Boolean) {
binding.messagePreviewProgress.visibility = if (show) VISIBLE else GONE binding.messagePreviewProgress.visibility = if (show) VISIBLE else GONE
} }
@ -137,20 +163,15 @@ class MessagePreviewFragment :
binding.messagePreviewRecycler.visibility = if (show) VISIBLE else GONE binding.messagePreviewRecycler.visibility = if (show) VISIBLE else GONE
} }
override fun showOptions(show: Boolean, isReplayable: Boolean) { override fun showOptions(show: Boolean, isReplayable: Boolean, isRestorable: Boolean) {
menuReplyButton?.isVisible = isReplayable menuReplyButton?.isVisible = show && isReplayable
menuForwardButton?.isVisible = show menuForwardButton?.isVisible = show
menuDeleteButton?.isVisible = show menuRestoreButton?.isVisible = show && isRestorable
menuDeleteButton?.isVisible = show && !isRestorable
menuDeleteForeverButton?.isVisible = show && isRestorable
menuShareButton?.isVisible = show menuShareButton?.isVisible = show
menuPrintButton?.isVisible = show menuPrintButton?.isVisible = show
} menuMuteButton?.isVisible = show && isReplayable
override fun setDeletedOptionsLabels() {
menuDeleteButton?.setTitle(R.string.message_delete_forever)
}
override fun setNotDeletedOptionsLabels() {
menuDeleteButton?.setTitle(R.string.message_move_to_trash)
} }
override fun showErrorView(show: Boolean) { override fun showErrorView(show: Boolean) {
@ -213,7 +234,7 @@ class MessagePreviewFragment :
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
outState.putSerializable(MESSAGE_ID_KEY, presenter.message) outState.putSerializable(MESSAGE_ID_KEY, presenter.messageWithAttachments)
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
} }

View File

@ -5,7 +5,7 @@ import androidx.core.text.parseAsHtml
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.* import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageAttachment import io.github.wulkanowy.data.db.entities.MessageWithAttachment
import io.github.wulkanowy.data.enums.MessageFolder import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.repositories.MessageRepository import io.github.wulkanowy.data.repositories.MessageRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
@ -14,9 +14,11 @@ import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
class MessagePreviewPresenter @Inject constructor( class MessagePreviewPresenter @Inject constructor(
errorHandler: ErrorHandler, errorHandler: ErrorHandler,
@ -26,9 +28,7 @@ class MessagePreviewPresenter @Inject constructor(
private val analytics: AnalyticsHelper private val analytics: AnalyticsHelper
) : BasePresenter<MessagePreviewView>(errorHandler, studentRepository) { ) : BasePresenter<MessagePreviewView>(errorHandler, studentRepository) {
var message: Message? = null var messageWithAttachments: MessageWithAttachment? = null
var attachments: List<MessageAttachment>? = null
private lateinit var lastError: Throwable private lateinit var lastError: Throwable
@ -38,7 +38,6 @@ class MessagePreviewPresenter @Inject constructor(
super.onAttachView(view) super.onAttachView(view)
view.initView() view.initView()
errorHandler.showErrorMessage = ::showErrorViewOnError errorHandler.showErrorMessage = ::showErrorViewOnError
this.message = message
loadData(requireNotNull(message)) loadData(requireNotNull(message))
} }
@ -66,25 +65,24 @@ class MessagePreviewPresenter @Inject constructor(
.logResourceStatus("message ${messageToLoad.messageId} preview") .logResourceStatus("message ${messageToLoad.messageId} preview")
.onResourceData { .onResourceData {
if (it != null) { if (it != null) {
message = it.message messageWithAttachments = it
attachments = it.attachments
view?.apply { view?.apply {
setMessageWithAttachment(it) setMessageWithAttachment(it)
showContent(true) showContent(true)
initOptions() initOptions()
updateMuteToggleButton(isMuted = it.mutedMessageSender != null)
if (preferencesRepository.isIncognitoMode && it.message.unread) { if (preferencesRepository.isIncognitoMode && it.message.unread) {
showMessage(R.string.message_incognito_description) showMessage(R.string.message_incognito_description)
} }
} }
} else { } else {
delay(1.seconds)
view?.run { view?.run {
showMessage(messageNotExists) showMessage(messageNotExists)
popView() popView()
} }
} }
} }.onResourceSuccess {
.onResourceSuccess {
if (it != null) { if (it != null) {
analytics.logEvent( analytics.logEvent(
"load_item", "load_item",
@ -92,31 +90,28 @@ class MessagePreviewPresenter @Inject constructor(
"length" to it.message.content.length "length" to it.message.content.length
) )
} }
} }.onResourceNotLoading { view?.showProgress(false) }.onResourceError {
.onResourceNotLoading { view?.showProgress(false) }
.onResourceError {
retryCallback = { onMessageLoadRetry(messageToLoad) } retryCallback = { onMessageLoadRetry(messageToLoad) }
errorHandler.dispatch(it) errorHandler.dispatch(it)
} }.launch()
.launch()
} }
fun onReply(): Boolean { fun onReply(): Boolean {
return if (message != null) { return if (messageWithAttachments?.message != null) {
view?.openMessageReply(message) view?.openMessageReply(messageWithAttachments?.message)
true true
} else false } else false
} }
fun onForward(): Boolean { fun onForward(): Boolean {
return if (message != null) { return if (messageWithAttachments?.message != null) {
view?.openMessageForward(message) view?.openMessageForward(messageWithAttachments?.message)
true true
} else false } else false
} }
fun onShare(): Boolean { fun onShare(): Boolean {
val message = message ?: return false val message = messageWithAttachments?.message ?: return false
val subject = message.subject.ifBlank { view?.messageNoSubjectString.orEmpty() } val subject = message.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }
val text = buildString { val text = buildString {
@ -129,13 +124,15 @@ class MessagePreviewPresenter @Inject constructor(
appendLine(message.content.parseAsHtml()) appendLine(message.content.parseAsHtml())
if (!attachments.isNullOrEmpty()) { if (!messageWithAttachments?.attachments.isNullOrEmpty()) {
appendLine() appendLine()
appendLine("Załączniki:") appendLine("Załączniki:")
append(attachments.orEmpty().joinToString(separator = "\n") { attachment -> append(
"${attachment.filename}: ${attachment.url}" messageWithAttachments?.attachments.orEmpty()
}) .joinToString(separator = "\n") { attachment ->
"${attachment.filename}: ${attachment.url}"
})
} }
} }
@ -148,7 +145,7 @@ class MessagePreviewPresenter @Inject constructor(
@SuppressLint("NewApi") @SuppressLint("NewApi")
fun onPrint(): Boolean { fun onPrint(): Boolean {
val message = message ?: return false val message = messageWithAttachments?.message ?: return false
val subject = message.subject.ifBlank { view?.messageNoSubjectString.orEmpty() } val subject = message.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }
val dateString = message.date.toFormattedString("yyyy-MM-dd HH:mm:ss") val dateString = message.date.toFormattedString("yyyy-MM-dd HH:mm:ss")
@ -159,8 +156,7 @@ class MessagePreviewPresenter @Inject constructor(
append("<div><h4>Od</h4>${message.sender}</div>") append("<div><h4>Od</h4>${message.sender}</div>")
append("<div><h4>DO</h4>${message.recipients}</div>") append("<div><h4>DO</h4>${message.recipients}</div>")
} }
val messageContent = "<p>${message.content}</p>" val messageContent = "<p>${message.content}</p>".replace(Regex("[\\n\\r]{2,}"), "</p><p>")
.replace(Regex("[\\n\\r]{2,}"), "</p><p>")
.replace(Regex("[\\n\\r]"), "<br>") .replace(Regex("[\\n\\r]"), "<br>")
val jobName = buildString { val jobName = buildString {
@ -171,9 +167,7 @@ class MessagePreviewPresenter @Inject constructor(
} }
view?.apply { view?.apply {
val html = printHTML val html = printHTML.replace("%SUBJECT%", subject).replace("%CONTENT%", messageContent)
.replace("%SUBJECT%", subject)
.replace("%CONTENT%", messageContent)
.replace("%INFO%", infoContent) .replace("%INFO%", infoContent)
printDocument(html, jobName) printDocument(html, jobName)
} }
@ -181,34 +175,69 @@ class MessagePreviewPresenter @Inject constructor(
return true return true
} }
private fun deleteMessage() { private fun restoreMessage() {
message ?: return val message = messageWithAttachments?.message ?: return
view?.run { view?.run {
showContent(false) showContent(false)
showProgress(true) showProgress(true)
showOptions(show = false, isReplayable = false) showOptions(
show = false,
isReplayable = false,
isRestorable = false,
)
showErrorView(false) showErrorView(false)
} }
Timber.i("Restore message ${message.messageGlobalKey}")
Timber.i("Delete message ${message?.messageGlobalKey}")
presenterScope.launch { presenterScope.launch {
runCatching { runCatching {
val student = studentRepository.getCurrentStudent(decryptPass = true) val student = studentRepository.getCurrentStudent(decryptPass = true)
val mailbox = messageRepository.getMailboxByStudent(student) val mailbox = messageRepository.getMailboxByStudent(student)
messageRepository.deleteMessage(student, mailbox, message!!) messageRepository.restoreMessages(student, mailbox, listOfNotNull(message))
} }
.onFailure { .onFailure {
retryCallback = { onMessageDelete() } retryCallback = { onMessageRestore() }
errorHandler.dispatch(it) errorHandler.dispatch(it)
} }
.onSuccess { .onSuccess {
view?.run { view?.run {
showMessage(deleteMessageSuccessString) showMessage(restoreMessageSuccessString)
popView() popView()
} }
} }
view?.showProgress(false)
}
}
private fun deleteMessage() {
messageWithAttachments?.message ?: return
view?.run {
showContent(false)
showProgress(true)
showOptions(
show = false,
isReplayable = false,
isRestorable = false,
)
showErrorView(false)
}
Timber.i("Delete message ${messageWithAttachments?.message?.messageGlobalKey}")
presenterScope.launch {
runCatching {
val student = studentRepository.getCurrentStudent(decryptPass = true)
messageRepository.deleteMessage(student, messageWithAttachments?.message!!)
}.onFailure {
retryCallback = { onMessageDelete() }
errorHandler.dispatch(it)
}.onSuccess {
view?.run {
showMessage(deleteMessageSuccessString)
popView()
}
}
view?.showProgress(false) view?.showProgress(false)
} }
@ -224,6 +253,11 @@ class MessagePreviewPresenter @Inject constructor(
} }
} }
fun onMessageRestore(): Boolean {
restoreMessage()
return true
}
fun onMessageDelete(): Boolean { fun onMessageDelete(): Boolean {
deleteMessage() deleteMessage()
return true return true
@ -232,20 +266,39 @@ class MessagePreviewPresenter @Inject constructor(
private fun initOptions() { private fun initOptions() {
view?.apply { view?.apply {
showOptions( showOptions(
show = message != null, show = messageWithAttachments?.message != null,
isReplayable = message?.folderId != MessageFolder.SENT.id, isReplayable = messageWithAttachments?.message?.folderId == MessageFolder.RECEIVED.id,
isRestorable = messageWithAttachments?.message?.folderId == MessageFolder.TRASHED.id,
) )
message?.let {
when (it.folderId == MessageFolder.TRASHED.id) {
true -> setDeletedOptionsLabels()
false -> setNotDeletedOptionsLabels()
}
}
} }
} }
fun onCreateOptionsMenu() { fun onCreateOptionsMenu() {
initOptions() initOptions()
} }
fun onMute(): Boolean {
val message = messageWithAttachments?.message ?: return false
val isMuted = messageWithAttachments?.mutedMessageSender != null
presenterScope.launch {
runCatching {
when (isMuted) {
true -> {
messageRepository.unmuteMessage(message.correspondents)
view?.run { showMessage(unmuteMessageSuccessString) }
}
false -> {
messageRepository.muteMessage(message.correspondents)
view?.run { showMessage(muteMessageSuccessString) }
}
}
}.onFailure {
errorHandler.dispatch(it)
}
}
view?.updateMuteToggleButton(isMuted)
return true
}
} }

View File

@ -9,6 +9,12 @@ interface MessagePreviewView : BaseView {
val deleteMessageSuccessString: String val deleteMessageSuccessString: String
val muteMessageSuccessString: String
val unmuteMessageSuccessString: String
val restoreMessageSuccessString: String
val messageNoSubjectString: String val messageNoSubjectString: String
val printHTML: String val printHTML: String
@ -19,6 +25,8 @@ interface MessagePreviewView : BaseView {
fun setMessageWithAttachment(item: MessageWithAttachment) fun setMessageWithAttachment(item: MessageWithAttachment)
fun updateMuteToggleButton(isMuted: Boolean)
fun showProgress(show: Boolean) fun showProgress(show: Boolean)
fun showContent(show: Boolean) fun showContent(show: Boolean)
@ -29,11 +37,7 @@ interface MessagePreviewView : BaseView {
fun setErrorRetryCallback(callback: () -> Unit) fun setErrorRetryCallback(callback: () -> Unit)
fun showOptions(show: Boolean, isReplayable: Boolean) fun showOptions(show: Boolean, isReplayable: Boolean, isRestorable: Boolean)
fun setDeletedOptionsLabels()
fun setNotDeletedOptionsLabels()
fun openMessageReply(message: Message?) fun openMessageReply(message: Message?)

View File

@ -203,7 +203,7 @@ class SendMessagePresenter @Inject constructor(
subject = subject, subject = subject,
content = content, content = content,
recipients = recipients, recipients = recipients,
mailboxId = mailbox.globalKey, mailbox = mailbox,
) )
}.logResourceStatus("sending message").onEach { }.logResourceStatus("sending message").onEach {
when (it) { when (it) {

View File

@ -18,8 +18,7 @@ import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
import javax.inject.Inject import javax.inject.Inject
class MessageTabAdapter @Inject constructor() : class MessageTabAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
lateinit var onItemClickListener: (MessageTabDataItem.MessageItem, position: Int) -> Unit lateinit var onItemClickListener: (MessageTabDataItem.MessageItem, position: Int) -> Unit
@ -52,10 +51,11 @@ class MessageTabAdapter @Inject constructor() :
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context) val inflater = LayoutInflater.from(parent.context)
return when (MessageItemViewType.values()[viewType]) { return when (MessageItemViewType.entries[viewType]) {
MessageItemViewType.FILTERS -> HeaderViewHolder( MessageItemViewType.FILTERS -> HeaderViewHolder(
ItemMessageChipsBinding.inflate(inflater, parent, false) ItemMessageChipsBinding.inflate(inflater, parent, false)
) )
MessageItemViewType.MESSAGE -> ItemViewHolder( MessageItemViewType.MESSAGE -> ItemViewHolder(
ItemMessageBinding.inflate(inflater, parent, false) ItemMessageBinding.inflate(inflater, parent, false)
) )
@ -137,7 +137,12 @@ class MessageTabAdapter @Inject constructor() :
ImageViewCompat.setImageTintList(this, ColorStateList.valueOf(currentTextColor)) ImageViewCompat.setImageTintList(this, ColorStateList.valueOf(currentTextColor))
isVisible = message.hasAttachments isVisible = message.hasAttachments
} }
messageItemUnreadIndicator.isVisible = message.unread messageItemUnreadIndicator.isVisible = message.unread || item.isMuted
when (item.isMuted) {
true -> messageItemUnreadIndicator.setImageResource(R.drawable.ic_notifications_off)
else -> messageItemUnreadIndicator.setImageResource(R.drawable.ic_circle_notification)
}
root.setOnClickListener { root.setOnClickListener {
holder.bindingAdapterPosition.let { holder.bindingAdapterPosition.let {
@ -165,8 +170,7 @@ class MessageTabAdapter @Inject constructor() :
RecyclerView.ViewHolder(binding.root) RecyclerView.ViewHolder(binding.root)
private class MessageTabDiffUtil( private class MessageTabDiffUtil(
private val old: List<MessageTabDataItem>, private val old: List<MessageTabDataItem>, private val new: List<MessageTabDataItem>
private val new: List<MessageTabDataItem>
) : DiffUtil.Callback() { ) : DiffUtil.Callback() {
override fun getOldListSize(): Int = old.size override fun getOldListSize(): Int = old.size

View File

@ -6,6 +6,7 @@ sealed class MessageTabDataItem(val viewType: MessageItemViewType) {
data class MessageItem( data class MessageItem(
val message: Message, val message: Message,
val isMuted: Boolean,
val isSelected: Boolean, val isSelected: Boolean,
val isActionMode: Boolean val isActionMode: Boolean
) : MessageTabDataItem(MessageItemViewType.MESSAGE) ) : MessageTabDataItem(MessageItemViewType.MESSAGE)

View File

@ -5,7 +5,9 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.View.* import android.view.View.GONE
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.widget.CompoundButton import android.widget.CompoundButton
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
@ -64,10 +66,12 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
} }
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
if (presenter.folder == MessageFolder.TRASHED) { val isTrashFolder = presenter.folder == MessageFolder.TRASHED
val menuItem = menu.findItem(R.id.messageTabContextMenuDelete)
menuItem.setTitle(R.string.message_delete_forever) menu.findItem(R.id.messageTabContextMenuDelete).setVisible(!isTrashFolder)
} menu.findItem(R.id.messageTabContextMenuDeleteForever).setVisible(isTrashFolder)
menu.findItem(R.id.messageTabContextMenuRestore).setVisible(isTrashFolder)
return presenter.onPrepareActionMode() return presenter.onPrepareActionMode()
} }
@ -79,6 +83,8 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
override fun onActionItemClicked(mode: ActionMode, menu: MenuItem): Boolean { override fun onActionItemClicked(mode: ActionMode, menu: MenuItem): Boolean {
when (menu.itemId) { when (menu.itemId) {
R.id.messageTabContextMenuDelete -> presenter.onActionModeSelectDelete() R.id.messageTabContextMenuDelete -> presenter.onActionModeSelectDelete()
R.id.messageTabContextMenuRestore -> presenter.onActionModeSelectRestore()
R.id.messageTabContextMenuDeleteForever -> presenter.onActionModeSelectDelete()
R.id.messageTabContextMenuSelectAll -> presenter.onActionModeSelectCheckAll() R.id.messageTabContextMenuSelectAll -> presenter.onActionModeSelectCheckAll()
} }
return true return true

View File

@ -4,6 +4,7 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.* import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Mailbox import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageWithMutedAuthor
import io.github.wulkanowy.data.enums.MessageFolder import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.repositories.MessageRepository import io.github.wulkanowy.data.repositories.MessageRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
@ -39,7 +40,7 @@ class MessageTabPresenter @Inject constructor(
private var mailboxes: List<Mailbox> = emptyList() private var mailboxes: List<Mailbox> = emptyList()
private var selectedMailbox: Mailbox? = null private var selectedMailbox: Mailbox? = null
private var messages = emptyList<Message>() private var messages = emptyList<MessageWithMutedAuthor>()
private val searchChannel = Channel<String>() private val searchChannel = Channel<String>()
@ -120,8 +121,27 @@ class MessageTabPresenter @Inject constructor(
return true return true
} }
fun onActionModeSelectRestore() {
Timber.i("Restore ${messagesToDelete.size} messages")
val messageList = messagesToDelete.toList()
presenterScope.launch {
view?.run {
showProgress(true)
showContent(false)
showActionMode(false)
}
runCatching {
val student = studentRepository.getCurrentStudent(true)
messageRepository.restoreMessages(student, selectedMailbox, messageList)
}
.onFailure(errorHandler::dispatch)
.onSuccess { view?.showMessage(R.string.message_messages_restored) }
}
}
fun onActionModeSelectDelete() { fun onActionModeSelectDelete() {
Timber.i("Delete ${messagesToDelete.size} messages)") Timber.i("Delete ${messagesToDelete.size} messages")
val messageList = messagesToDelete.toList() val messageList = messagesToDelete.toList()
presenterScope.launch { presenterScope.launch {
@ -133,7 +153,7 @@ class MessageTabPresenter @Inject constructor(
runCatching { runCatching {
val student = studentRepository.getCurrentStudent(true) val student = studentRepository.getCurrentStudent(true)
messageRepository.deleteMessages(student, selectedMailbox, messageList) messageRepository.deleteMessages(student, messageList)
} }
.onFailure(errorHandler::dispatch) .onFailure(errorHandler::dispatch)
.onSuccess { view?.showMessage(R.string.message_messages_deleted) } .onSuccess { view?.showMessage(R.string.message_messages_deleted) }
@ -141,7 +161,7 @@ class MessageTabPresenter @Inject constructor(
} }
fun onActionModeSelectCheckAll() { fun onActionModeSelectCheckAll() {
val messagesToSelect = getFilteredData() val messagesToSelect = getFilteredData().map { it.message }
val isAllSelected = messagesToDelete.containsAll(messagesToSelect) val isAllSelected = messagesToDelete.containsAll(messagesToSelect)
if (isAllSelected) { if (isAllSelected) {
@ -188,7 +208,7 @@ class MessageTabPresenter @Inject constructor(
view?.showActionMode(false) view?.showActionMode(false)
} }
val filteredData = getFilteredData() val filteredData = getFilteredData().map { it.message }
view?.run { view?.run {
updateActionModeTitle(messagesToDelete.size) updateActionModeTitle(messagesToDelete.size)
@ -320,25 +340,31 @@ class MessageTabPresenter @Inject constructor(
} }
} }
private fun getFilteredData(): List<Message> { private fun getFilteredData(): List<MessageWithMutedAuthor> {
if (lastSearchQuery.trim().isEmpty()) { if (lastSearchQuery.trim().isEmpty()) {
val sortedMessages = messages.sortedByDescending { it.date } val sortedMessages = messages.sortedByDescending { it.message.date }
return when { return when {
(onlyUnread == true) && onlyWithAttachments -> sortedMessages.filter { it.unread == onlyUnread && it.hasAttachments == onlyWithAttachments } (onlyUnread == true) && onlyWithAttachments -> sortedMessages.filter {
(onlyUnread == true) -> sortedMessages.filter { it.unread == onlyUnread } it.message.unread == onlyUnread && it.message.hasAttachments == onlyWithAttachments
onlyWithAttachments -> sortedMessages.filter { it.hasAttachments == onlyWithAttachments } }
(onlyUnread == true) -> sortedMessages.filter { it.message.unread == onlyUnread }
onlyWithAttachments -> sortedMessages.filter { it.message.hasAttachments == onlyWithAttachments }
else -> sortedMessages else -> sortedMessages
} }
} else { } else {
val sortedMessages = messages val sortedMessages = messages
.map { it to calculateMatchRatio(it, lastSearchQuery) } .map { it to calculateMatchRatio(it.message, lastSearchQuery) }
.sortedWith(compareBy<Pair<Message, Int>> { -it.second }.thenByDescending { it.first.date }) .sortedWith(compareBy<Pair<MessageWithMutedAuthor, Int>> { -it.second }.thenByDescending { it.first.message.date })
.filter { it.second > 6000 } .filter { it.second > 6000 }
.map { it.first } .map { it.first }
return when { return when {
(onlyUnread == true) && onlyWithAttachments -> sortedMessages.filter { it.unread == onlyUnread && it.hasAttachments == onlyWithAttachments } (onlyUnread == true) && onlyWithAttachments -> sortedMessages.filter {
(onlyUnread == true) -> sortedMessages.filter { it.unread == onlyUnread } it.message.unread == onlyUnread && it.message.hasAttachments == onlyWithAttachments
onlyWithAttachments -> sortedMessages.filter { it.hasAttachments == onlyWithAttachments } }
(onlyUnread == true) -> sortedMessages.filter { it.message.unread == onlyUnread }
onlyWithAttachments -> sortedMessages.filter { it.message.hasAttachments == onlyWithAttachments }
else -> sortedMessages else -> sortedMessages
} }
} }
@ -367,8 +393,9 @@ class MessageTabPresenter @Inject constructor(
addAll(data.map { message -> addAll(data.map { message ->
MessageTabDataItem.MessageItem( MessageTabDataItem.MessageItem(
message = message, message = message.message,
isSelected = messagesToDelete.any { it.messageGlobalKey == message.messageGlobalKey }, isMuted = message.mutedMessageSender != null,
isSelected = messagesToDelete.any { it.messageGlobalKey == message.message.messageGlobalKey },
isActionMode = isActionMode isActionMode = isActionMode
) )
}) })

View File

@ -2,6 +2,7 @@ package io.github.wulkanowy.ui.modules.schoolannouncement
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.databinding.ItemSchoolAnnouncementBinding import io.github.wulkanowy.databinding.ItemSchoolAnnouncementBinding
@ -29,6 +30,10 @@ class SchoolAnnouncementAdapter @Inject constructor() :
schoolAnnouncementItemDate.text = item.date.toFormattedString() schoolAnnouncementItemDate.text = item.date.toFormattedString()
schoolAnnouncementItemType.text = item.subject schoolAnnouncementItemType.text = item.subject
schoolAnnouncementItemContent.text = item.content.parseUonetHtml() schoolAnnouncementItemContent.text = item.content.parseUonetHtml()
with(schoolAnnouncementItemAuthor) {
text = item.author
isVisible = !item.author.isNullOrBlank()
}
root.setOnClickListener { onItemClickListener(item) } root.setOnClickListener { onItemClickListener(item) }
} }

View File

@ -3,7 +3,6 @@ package io.github.wulkanowy.ui.modules.timetable
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
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.Timetable import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.enums.TimetableGapsMode.BETWEEN_AND_BEFORE_LESSONS import io.github.wulkanowy.data.enums.TimetableGapsMode.BETWEEN_AND_BEFORE_LESSONS
import io.github.wulkanowy.data.enums.TimetableGapsMode.NO_GAPS import io.github.wulkanowy.data.enums.TimetableGapsMode.NO_GAPS
@ -150,7 +149,7 @@ class TimetablePresenter @Inject constructor(
val student = studentRepository.getCurrentStudent() val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student) val semester = semesterRepository.getCurrentSemester(student)
checkInitialAndCurrentDate(student, semester) checkInitialAndCurrentDate(semester)
timetableRepository.getTimetable( timetableRepository.getTimetable(
student = student, student = student,
semester = semester, semester = semester,
@ -194,9 +193,9 @@ class TimetablePresenter @Inject constructor(
.launch() .launch()
} }
private suspend fun checkInitialAndCurrentDate(student: Student, semester: Semester) { private suspend fun checkInitialAndCurrentDate(semester: Semester) {
if (initialDate == null) { if (initialDate == null) {
isWeekendHasLessons = isStudentHasLessonsOnWeekendUseCase(student, semester) isWeekendHasLessons = isStudentHasLessonsOnWeekendUseCase(semester)
initialDate = getInitialDate(semester) initialDate = getInitialDate(semester)
} }

View File

@ -3,11 +3,13 @@ package io.github.wulkanowy.utils
import android.content.res.Resources import android.content.res.Resources
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException
import io.github.wulkanowy.sdk.scrapper.exception.AccountInactiveException
import io.github.wulkanowy.sdk.scrapper.exception.CloudflareVerificationException import io.github.wulkanowy.sdk.scrapper.exception.CloudflareVerificationException
import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException
import io.github.wulkanowy.sdk.scrapper.exception.ScrapperException import io.github.wulkanowy.sdk.scrapper.exception.ScrapperException
import io.github.wulkanowy.sdk.scrapper.exception.ServiceUnavailableException import io.github.wulkanowy.sdk.scrapper.exception.ServiceUnavailableException
import io.github.wulkanowy.sdk.scrapper.exception.VulcanException import io.github.wulkanowy.sdk.scrapper.exception.VulcanException
import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException
import io.github.wulkanowy.sdk.scrapper.login.NotLoggedInException import io.github.wulkanowy.sdk.scrapper.login.NotLoggedInException
import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException
import okhttp3.internal.http2.StreamResetException import okhttp3.internal.http2.StreamResetException
@ -33,6 +35,8 @@ fun Resources.getErrorString(error: Throwable): String = when (error) {
is ServiceUnavailableException -> R.string.error_service_unavailable is ServiceUnavailableException -> R.string.error_service_unavailable
is FeatureDisabledException -> R.string.error_feature_disabled is FeatureDisabledException -> R.string.error_feature_disabled
is FeatureNotAvailableException -> R.string.error_feature_not_available is FeatureNotAvailableException -> R.string.error_feature_not_available
is BadCredentialsException -> R.string.error_password_invalid
is AccountInactiveException -> R.string.error_account_inactive
is VulcanException -> R.string.error_unknown_uonet is VulcanException -> R.string.error_unknown_uonet
is ScrapperException -> R.string.error_unknown_app is ScrapperException -> R.string.error_unknown_app
is CloudflareVerificationException -> R.string.error_cloudflare_captcha is CloudflareVerificationException -> R.string.error_cloudflare_captcha

View File

@ -5,17 +5,21 @@ import java.net.CookiePolicy
import java.net.CookieStore import java.net.CookieStore
import java.net.HttpCookie import java.net.HttpCookie
import java.net.URI import java.net.URI
import javax.inject.Inject
import javax.inject.Singleton
import android.webkit.CookieManager as WebkitCookieManager import android.webkit.CookieManager as WebkitCookieManager
import java.net.CookieManager as JavaCookieManager import java.net.CookieManager as JavaCookieManager
class WebkitCookieManagerProxy : JavaCookieManager(null, CookiePolicy.ACCEPT_ALL) { @Singleton
class WebkitCookieManagerProxy @Inject constructor() :
JavaCookieManager(null, CookiePolicy.ACCEPT_ALL) {
private val webkitCookieManager: WebkitCookieManager? = getWebkitCookieManager() val webkitCookieManager: WebkitCookieManager? = getCookieManager()
/** /**
* @see [https://stackoverflow.com/a/70354583/6695449] * @see [https://stackoverflow.com/a/70354583/6695449]
*/ */
private fun getWebkitCookieManager(): WebkitCookieManager? { private fun getCookieManager(): WebkitCookieManager? {
return try { return try {
WebkitCookieManager.getInstance() WebkitCookieManager.getInstance()
} catch (e: AndroidRuntimeException) { } catch (e: AndroidRuntimeException) {

View File

@ -1,7 +1,11 @@
Wersja 2.4.2 Wersja 2.5.0
- naprawiliśmy crash przy przełączaniu uczniów, motywów i języków — dodaliśmy wyświetlanie ogłoszeń
- naprawiliśmy crash przy dodawaniu dodatkowych lekcji — dodaliśmy opcję przywracania wiadomości z kosza
- naprawiliśmy obsługę błędów widżetach — dodaliśmy opcję wyciszania nadawców wiadomości
— naprawiliśmy opcjonalne liczenie średniej arytmetycznej, kiedy brak ocen z wagą w drugim semestrze
— usprawniliśmy ładowanie frekwencji i planu lekcji
— naprawiliśmy usprawiedliwianie nieobecności i autoryzację u użytkowników eduOne
— zmieniliśmy komunikat o zmienionym haśle
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/colorPrimary" />
<size
android:width="10dp"
android:height="10dp" />
</shape>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#fff"
android:pathData="M14.12,10.47L12,12.59l-2.13,-2.12 -1.41,1.41L10.59,14l-2.12,2.12 1.41,1.41L12,15.41l2.12,2.12 1.41,-1.41L13.41,14l2.12,-2.12zM15.5,4l-1,-1h-5l-1,1H5v2h14V4zM6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM8,9h8v10H8V9z" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#fff"
android:pathData="M15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4zM6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L18,7L6,7v12zM8,14L8,9h8v10L8,19v-5zM10,18h4v-4h2l-4,-4 -4,4h2z" />
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="17dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="17dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M20,18.69L7.84,6.14 5.27,3.49 4,4.76l2.8,2.8v0.01c-0.52,0.99 -0.8,2.16 -0.8,3.42v5l-2,2v1h13.73l2,2L21,19.72l-1,-1.03zM12,22c1.11,0 2,-0.89 2,-2h-4c0,1.11 0.89,2 2,2zM18,14.68L18,11c0,-3.08 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68c-0.15,0.03 -0.29,0.08 -0.42,0.12 -0.1,0.03 -0.2,0.07 -0.3,0.11h-0.01c-0.01,0 -0.01,0 -0.02,0.01 -0.23,0.09 -0.46,0.2 -0.68,0.31 0,0 -0.01,0 -0.01,0.01L18,14.68z"/>
</vector>

View File

@ -16,7 +16,7 @@
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/main_toolbar" android:id="@+id/main_toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" /> android:layout_height="wrap_content" />
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView <androidx.fragment.app.FragmentContainerView

View File

@ -85,7 +85,7 @@
android:id="@+id/accountEditDetailsSave" android:id="@+id/accountEditDetailsSave"
style="@style/Widget.Material3.Button.TextButton.Dialog" style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="36dp" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:layout_marginEnd="24dp" android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp" android:layout_marginBottom="24dp"
@ -93,6 +93,7 @@
android:insetTop="0dp" android:insetTop="0dp"
android:insetRight="0dp" android:insetRight="0dp"
android:insetBottom="0dp" android:insetBottom="0dp"
android:minHeight="36dp"
android:text="@string/all_save" android:text="@string/all_save"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@ -102,7 +103,7 @@
android:id="@+id/accountEditDetailsCancel" android:id="@+id/accountEditDetailsCancel"
style="@style/Widget.Material3.Button.TextButton.Dialog" style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="36dp" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:layout_marginBottom="24dp" android:layout_marginBottom="24dp"
@ -110,6 +111,7 @@
android:insetTop="0dp" android:insetTop="0dp"
android:insetRight="0dp" android:insetRight="0dp"
android:insetBottom="0dp" android:insetBottom="0dp"
android:minHeight="36dp"
android:text="@android:string/cancel" android:text="@android:string/cancel"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/accountEditDetailsSave" app:layout_constraintEnd_toStartOf="@id/accountEditDetailsSave"

View File

@ -113,7 +113,7 @@
android:id="@+id/additionalLessonDialogClose" android:id="@+id/additionalLessonDialogClose"
style="@style/Widget.Material3.Button.TextButton.Dialog" style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="36dp" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
@ -122,6 +122,7 @@
android:insetTop="0dp" android:insetTop="0dp"
android:insetRight="0dp" android:insetRight="0dp"
android:insetBottom="0dp" android:insetBottom="0dp"
android:minHeight="36dp"
android:text="@string/all_close" android:text="@string/all_close"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/additionalLessonDialogAdd" app:layout_constraintEnd_toStartOf="@+id/additionalLessonDialogAdd"
@ -131,7 +132,7 @@
android:id="@+id/additionalLessonDialogAdd" android:id="@+id/additionalLessonDialogAdd"
style="@style/Widget.Material3.Button.TextButton.Dialog" style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="36dp" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:layout_marginBottom="24dp" android:layout_marginBottom="24dp"
@ -139,6 +140,7 @@
android:insetTop="0dp" android:insetTop="0dp"
android:insetRight="0dp" android:insetRight="0dp"
android:insetBottom="0dp" android:insetBottom="0dp"
android:minHeight="36dp"
android:text="@string/all_add" android:text="@string/all_add"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View File

@ -150,7 +150,7 @@
android:id="@+id/attendanceDialogClose" android:id="@+id/attendanceDialogClose"
style="@style/Widget.Material3.Button.TextButton.Dialog" style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="36dp" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_marginBottom="24dp" android:layout_marginBottom="24dp"
@ -158,6 +158,7 @@
android:insetTop="0dp" android:insetTop="0dp"
android:insetRight="0dp" android:insetRight="0dp"
android:insetBottom="0dp" android:insetBottom="0dp"
android:minHeight="36dp"
android:text="@string/all_close" android:text="@string/all_close"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View File

@ -7,15 +7,18 @@
tools:context=".ui.modules.captcha.CaptchaDialog"> tools:context=".ui.modules.captcha.CaptchaDialog">
<TextView <TextView
android:id="@+id/captcha_title"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp" android:layout_marginHorizontal="20dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:paddingVertical="10dp"
android:text="@string/captcha_dialog_title" android:text="@string/captcha_dialog_title"
app:layout_constraintBottom_toBottomOf="@id/captcha_close" app:layout_constraintBottom_toBottomOf="@id/captcha_close"
app:layout_constraintEnd_toStartOf="@id/captcha_refresh" app:layout_constraintEnd_toStartOf="@id/captcha_refresh"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/captcha_refresh" android:id="@+id/captcha_refresh"
@ -41,11 +44,29 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/captcha_toolbar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="captcha_title,captcha_close,captcha_refresh" />
<WebView <WebView
android:id="@+id/captcha_webview" android:id="@+id/captcha_webview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/captcha_description"
app:layout_constraintDimensionRatio="1"
app:layout_constraintTop_toBottomOf="@id/captcha_toolbar" />
<TextView
android:id="@+id/captcha_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="20dp"
android:paddingVertical="10dp"
android:text="@string/captcha_dialog_description"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/captcha_close" /> app:layout_constraintTop_toBottomOf="@id/captcha_webview" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -181,7 +181,7 @@
android:id="@+id/conferenceDialogClose" android:id="@+id/conferenceDialogClose"
style="@style/Widget.Material3.Button.TextButton.Dialog" style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="36dp" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_marginBottom="24dp" android:layout_marginBottom="24dp"
@ -189,6 +189,7 @@
android:insetTop="0dp" android:insetTop="0dp"
android:insetRight="0dp" android:insetRight="0dp"
android:insetBottom="0dp" android:insetBottom="0dp"
android:minHeight="36dp"
android:text="@string/all_close" android:text="@string/all_close"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View File

@ -220,7 +220,7 @@
android:id="@+id/examDialogAddToCalendar" android:id="@+id/examDialogAddToCalendar"
style="@style/Widget.Material3.Button.TextButton.Dialog" style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="36dp" android:layout_height="wrap_content"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:layout_marginBottom="24dp" android:layout_marginBottom="24dp"
android:contentDescription="@string/all_add_to_calendar" android:contentDescription="@string/all_add_to_calendar"
@ -228,6 +228,7 @@
android:insetTop="0dp" android:insetTop="0dp"
android:insetRight="0dp" android:insetRight="0dp"
android:insetBottom="0dp" android:insetBottom="0dp"
android:minHeight="36dp"
android:text="@string/all_add" android:text="@string/all_add"
app:icon="@drawable/ic_calendar_all" app:icon="@drawable/ic_calendar_all"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
@ -237,7 +238,7 @@
android:id="@+id/examDialogClose" android:id="@+id/examDialogClose"
style="@style/Widget.Material3.Button.TextButton.Dialog" style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="36dp" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_marginBottom="24dp" android:layout_marginBottom="24dp"
@ -245,6 +246,7 @@
android:insetTop="0dp" android:insetTop="0dp"
android:insetRight="0dp" android:insetRight="0dp"
android:insetBottom="0dp" android:insetBottom="0dp"
android:minHeight="36dp"
android:text="@string/all_close" android:text="@string/all_close"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View File

@ -212,7 +212,7 @@
android:id="@+id/gradeDialogClose" android:id="@+id/gradeDialogClose"
style="@style/Widget.Material3.Button.TextButton.Dialog" style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="36dp" android:layout_height="wrap_content"
android:layout_below="@+id/gradeDialogColorValue" android:layout_below="@+id/gradeDialogColorValue"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
@ -222,6 +222,7 @@
android:insetTop="0dp" android:insetTop="0dp"
android:insetRight="0dp" android:insetRight="0dp"
android:insetBottom="0dp" android:insetBottom="0dp"
android:minHeight="36dp"
android:text="@string/all_close" /> android:text="@string/all_close" />
</RelativeLayout> </RelativeLayout>
</LinearLayout> </LinearLayout>

View File

@ -27,7 +27,7 @@
android:id="@+id/homeworkDialogRead" android:id="@+id/homeworkDialogRead"
style="@style/Widget.Material3.Button.TextButton.Dialog" style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="36dp" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:layout_marginBottom="24dp" android:layout_marginBottom="24dp"
@ -35,6 +35,7 @@
android:insetTop="0dp" android:insetTop="0dp"
android:insetRight="0dp" android:insetRight="0dp"
android:insetBottom="0dp" android:insetBottom="0dp"
android:minHeight="36dp"
android:text="@string/homework_mark_as_done" android:text="@string/homework_mark_as_done"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/homeworkDialogClose" /> app:layout_constraintEnd_toStartOf="@+id/homeworkDialogClose" />
@ -43,13 +44,14 @@
android:id="@+id/homeworkDialogClose" android:id="@+id/homeworkDialogClose"
style="@style/Widget.Material3.Button.TextButton.Dialog" style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="36dp" android:layout_height="wrap_content"
android:layout_marginEnd="24dp" android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp" android:layout_marginBottom="24dp"
android:insetLeft="0dp" android:insetLeft="0dp"
android:insetTop="0dp" android:insetTop="0dp"
android:insetRight="0dp" android:insetRight="0dp"
android:insetBottom="0dp" android:insetBottom="0dp"
android:minHeight="36dp"
android:text="@string/all_close" android:text="@string/all_close"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" /> app:layout_constraintEnd_toEndOf="parent" />

View File

@ -94,7 +94,7 @@
android:id="@+id/homeworkDialogClose" android:id="@+id/homeworkDialogClose"
style="@style/Widget.Material3.Button.TextButton.Dialog" style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="36dp" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
@ -103,6 +103,7 @@
android:insetTop="0dp" android:insetTop="0dp"
android:insetRight="0dp" android:insetRight="0dp"
android:insetBottom="0dp" android:insetBottom="0dp"
android:minHeight="36dp"
android:text="@string/all_close" android:text="@string/all_close"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/homeworkDialogAdd" app:layout_constraintEnd_toStartOf="@+id/homeworkDialogAdd"
@ -112,13 +113,14 @@
android:id="@+id/homeworkDialogAdd" android:id="@+id/homeworkDialogAdd"
style="@style/Widget.Material3.Button.TextButton.Dialog" style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="36dp" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:layout_marginBottom="24dp" android:layout_marginBottom="24dp"
android:insetLeft="0dp" android:insetLeft="0dp"
android:insetTop="0dp" android:insetTop="0dp"
android:insetRight="0dp" android:insetRight="0dp"
android:insetBottom="0dp" android:insetBottom="0dp"
android:minHeight="36dp"
android:text="@string/all_add" android:text="@string/all_add"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View File

@ -212,7 +212,7 @@
android:id="@+id/completedLessonDialogClose" android:id="@+id/completedLessonDialogClose"
style="@style/Widget.Material3.Button.TextButton.Dialog" style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="36dp" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_marginBottom="24dp" android:layout_marginBottom="24dp"
@ -220,6 +220,7 @@
android:insetTop="0dp" android:insetTop="0dp"
android:insetRight="0dp" android:insetRight="0dp"
android:insetBottom="0dp" android:insetBottom="0dp"
android:minHeight="36dp"
android:text="@string/all_close" android:text="@string/all_close"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View File

@ -18,10 +18,10 @@
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:contentDescription="@string/mobile_device_qr" android:contentDescription="@string/mobile_device_qr"
tools:src="@tools:sample/avatars"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
<TextView <TextView
android:id="@+id/mobileDeviceDialogTokenTitle" android:id="@+id/mobileDeviceDialogTokenTitle"
@ -66,6 +66,7 @@
app:layout_constraintHorizontal_bias="0" app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/mobileDeviceDialogTokenValue" /> app:layout_constraintTop_toBottomOf="@+id/mobileDeviceDialogTokenValue" />
<TextView <TextView
android:id="@+id/mobileDeviceDialogSymbolValue" android:id="@+id/mobileDeviceDialogSymbolValue"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -113,7 +114,7 @@
android:id="@+id/mobileDeviceDialogClose" android:id="@+id/mobileDeviceDialogClose"
style="@style/Widget.Material3.Button.TextButton.Dialog" style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="36dp" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:layout_marginEnd="24dp" android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp" android:layout_marginBottom="24dp"
@ -121,6 +122,7 @@
android:insetTop="0dp" android:insetTop="0dp"
android:insetRight="0dp" android:insetRight="0dp"
android:insetBottom="0dp" android:insetBottom="0dp"
android:minHeight="36dp"
android:text="@string/all_close" android:text="@string/all_close"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@ -131,19 +133,19 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="invisible" android:visibility="invisible"
tools:visibility="visible" app:constraint_referenced_ids="mobileDeviceQr,mobileDeviceDialogTokenTitle,mobileDeviceDialogTokenValue,mobileDeviceDialogSymbolTitle,mobileDeviceDialogSymbolValue,mobileDeviceDialogPinTitle,mobileDeviceDialogPinValue,mobileDeviceDialogClose"
app:constraint_referenced_ids="mobileDeviceQr,mobileDeviceDialogTokenTitle,mobileDeviceDialogTokenValue,mobileDeviceDialogSymbolTitle,mobileDeviceDialogSymbolValue,mobileDeviceDialogPinTitle,mobileDeviceDialogPinValue,mobileDeviceDialogClose" /> tools:visibility="visible" />
<com.google.android.material.progressindicator.CircularProgressIndicator <com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/mobileDeviceDialogProgress" android:id="@+id/mobileDeviceDialogProgress"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:indeterminate="true" android:indeterminate="true"
tools:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" /> tools:visibility="invisible" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>

View File

@ -180,7 +180,7 @@
android:id="@+id/noteDialogClose" android:id="@+id/noteDialogClose"
style="@style/Widget.Material3.Button.TextButton.Dialog" style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="36dp" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_marginBottom="24dp" android:layout_marginBottom="24dp"
@ -188,6 +188,7 @@
android:insetTop="0dp" android:insetTop="0dp"
android:insetRight="0dp" android:insetRight="0dp"
android:insetBottom="0dp" android:insetBottom="0dp"
android:minHeight="36dp"
android:text="@string/all_close" android:text="@string/all_close"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View File

@ -122,7 +122,7 @@
android:id="@+id/announcementDialogClose" android:id="@+id/announcementDialogClose"
style="@style/Widget.Material3.Button.TextButton.Dialog" style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="36dp" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_marginBottom="24dp" android:layout_marginBottom="24dp"
@ -130,6 +130,7 @@
android:insetTop="0dp" android:insetTop="0dp"
android:insetRight="0dp" android:insetRight="0dp"
android:insetBottom="0dp" android:insetBottom="0dp"
android:minHeight="36dp"
android:text="@string/all_close" android:text="@string/all_close"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View File

@ -263,7 +263,7 @@
android:id="@+id/timetableDialogClose" android:id="@+id/timetableDialogClose"
style="@style/Widget.Material3.Button.TextButton.Dialog" style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="36dp" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_marginBottom="24dp" android:layout_marginBottom="24dp"
@ -271,6 +271,7 @@
android:insetTop="0dp" android:insetTop="0dp"
android:insetRight="0dp" android:insetRight="0dp"
android:insetBottom="0dp" android:insetBottom="0dp"
android:minHeight="36dp"
android:text="@string/all_close" android:text="@string/all_close"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View File

@ -45,6 +45,9 @@
android:textColor="?android:textColorSecondary" android:textColor="?android:textColorSecondary"
android:textSize="12sp" android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/gradeHeaderPointsSum"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="@id/gradeHeaderSubject" app:layout_constraintStart_toStartOf="@id/gradeHeaderSubject"
app:layout_constraintTop_toBottomOf="@+id/gradeHeaderSubject" app:layout_constraintTop_toBottomOf="@+id/gradeHeaderSubject"
tools:text="Average: 6,00" /> tools:text="Average: 6,00" />
@ -55,8 +58,12 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="10dp" android:layout_marginStart="10dp"
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorSecondary" android:textColor="?android:textColorSecondary"
android:textSize="12sp" android:textSize="12sp"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toStartOf="@id/gradeHeaderNumber"
app:layout_constraintStart_toEndOf="@+id/gradeHeaderAverage" app:layout_constraintStart_toEndOf="@+id/gradeHeaderAverage"
app:layout_constraintTop_toBottomOf="@+id/gradeHeaderSubject" app:layout_constraintTop_toBottomOf="@+id/gradeHeaderSubject"
tools:text="Points: 123/200 (61,5%)" /> tools:text="Points: 123/200 (61,5%)" />
@ -67,8 +74,13 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="10dp" android:layout_marginStart="10dp"
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorSecondary" android:textColor="?android:textColorSecondary"
android:textSize="12sp" android:textSize="12sp"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/gradeHeaderPointsSum" app:layout_constraintStart_toEndOf="@+id/gradeHeaderPointsSum"
app:layout_constraintTop_toBottomOf="@id/gradeHeaderSubject" app:layout_constraintTop_toBottomOf="@id/gradeHeaderSubject"
tools:text="12 grades" /> tools:text="12 grades" />
@ -85,6 +97,9 @@
android:paddingRight="5dp" android:paddingRight="5dp"
android:textColor="?colorOnPrimary" android:textColor="?colorOnPrimary"
android:textSize="14sp" android:textSize="14sp"
app:autoSizeMaxTextSize="16dp"
app:autoSizeMinTextSize="10dp"
app:autoSizeTextType="uniform"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"

View File

@ -81,9 +81,9 @@
<ImageView <ImageView
android:id="@+id/messageItemUnreadIndicator" android:id="@+id/messageItemUnreadIndicator"
android:layout_width="10dp" android:layout_width="wrap_content"
android:layout_height="10dp" android:layout_height="wrap_content"
android:src="@drawable/ic_circle" android:src="@drawable/ic_circle_notification"
app:layout_constraintBottom_toBottomOf="@id/messageItemDate" app:layout_constraintBottom_toBottomOf="@id/messageItemDate"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/messageItemDate" app:layout_constraintTop_toTopOf="@id/messageItemDate"

View File

@ -11,27 +11,41 @@
android:id="@+id/schoolAnnouncementItemDate" android:id="@+id/schoolAnnouncementItemDate"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="15dp" android:layout_marginHorizontal="15dp"
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:layout_marginEnd="10dp"
android:textColor="?android:textColorSecondary" android:textColor="?android:textColorSecondary"
android:textSize="15sp" android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toStartOf="@id/schoolAnnouncementItemAuthor"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/date/ddmmyy" /> tools:text="@tools:sample/date/ddmmyy" />
<TextView
android:id="@+id/schoolAnnouncementItemAuthor"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="10dp"
android:gravity="end"
android:textColor="?android:textColorSecondary"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/schoolAnnouncementItemDate"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/full_names" />
<TextView <TextView
android:id="@+id/schoolAnnouncementItemType" android:id="@+id/schoolAnnouncementItemType"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="5dp" android:layout_marginHorizontal="15dp"
android:layout_marginBottom="5dp" android:layout_marginVertical="5dp"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="2" android:maxLines="2"
android:textSize="16sp" android:textSize="16sp"
app:layout_constraintEnd_toEndOf="@id/schoolAnnouncementItemDate" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/schoolAnnouncementItemDate" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/schoolAnnouncementItemDate" app:layout_constraintTop_toBottomOf="@id/schoolAnnouncementItemDate"
app:layout_goneMarginEnd="0dp" app:layout_goneMarginEnd="0dp"
tools:text="@tools:sample/lorem" /> tools:text="@tools:sample/lorem" />
@ -40,6 +54,7 @@
android:id="@+id/schoolAnnouncementItemContent" android:id="@+id/schoolAnnouncementItemContent"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="15dp"
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:layout_marginBottom="15dp" android:layout_marginBottom="15dp"
android:ellipsize="end" android:ellipsize="end"
@ -47,8 +62,8 @@
android:maxLines="2" android:maxLines="2"
android:textSize="14sp" android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/schoolAnnouncementItemType" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/schoolAnnouncementItemDate" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/schoolAnnouncementItemType" app:layout_constraintTop_toBottomOf="@id/schoolAnnouncementItemType"
tools:text="@tools:sample/lorem/random" /> tools:text="@tools:sample/lorem/random" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,7 +1,6 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout 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"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/relativeLayout2"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?selectableItemBackground" android:background="?selectableItemBackground"
@ -15,15 +14,17 @@
android:id="@+id/timetableItemNumber" android:id="@+id/timetableItemNumber"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minWidth="40dp"
android:minHeight="40dp"
android:gravity="center" android:gravity="center"
android:includeFontPadding="false" android:includeFontPadding="false"
android:maxLength="2" android:maxLength="2"
android:minWidth="40dp"
android:minHeight="40dp"
android:textColor="?android:textColorPrimary" android:textColor="?android:textColorPrimary"
android:textSize="32sp" android:textSize="32sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0"
tools:text="5" /> tools:text="5" />
<TextView <TextView
@ -38,7 +39,7 @@
android:textSize="15sp" android:textSize="15sp"
app:layout_constraintEnd_toStartOf="@id/timetableItemTimeBarrier" app:layout_constraintEnd_toStartOf="@id/timetableItemTimeBarrier"
app:layout_constraintStart_toEndOf="@+id/timetableItemTimeStart" app:layout_constraintStart_toEndOf="@+id/timetableItemTimeStart"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="@id/timetableItemTimeStart"
tools:text="@tools:sample/lorem" /> tools:text="@tools:sample/lorem" />
<TextView <TextView
@ -49,8 +50,11 @@
android:maxLines="1" android:maxLines="1"
android:textColor="?android:textColorSecondary" android:textColor="?android:textColorSecondary"
android:textSize="13sp" android:textSize="13sp"
app:layout_constraintBottom_toTopOf="@id/timetableItemTimeFinish"
app:layout_constraintStart_toEndOf="@id/timetableItemNumber" app:layout_constraintStart_toEndOf="@id/timetableItemNumber"
app:layout_constraintTop_toTopOf="@id/timetableItemNumber" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0"
app:layout_constraintVertical_chainStyle="packed"
tools:text="11:11" /> tools:text="11:11" />
<TextView <TextView
@ -58,11 +62,13 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:maxLines="1" android:maxLines="1"
android:textColor="?android:textColorSecondary" android:textColor="?android:textColorSecondary"
android:textSize="13sp" android:textSize="13sp"
app:layout_constraintBottom_toBottomOf="@+id/timetableItemNumber" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/timetableItemNumber" app:layout_constraintStart_toEndOf="@id/timetableItemNumber"
app:layout_constraintTop_toBottomOf="@id/timetableItemTimeStart"
tools:text="12:00" /> tools:text="12:00" />
<TextView <TextView
@ -70,11 +76,16 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="10dp" android:layout_marginStart="10dp"
android:layout_marginEnd="0dp"
android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textColor="?android:textColorSecondary" android:textColor="?android:textColorSecondary"
android:textSize="13sp" android:textSize="13sp"
app:layout_constraintBottom_toBottomOf="@+id/timetableItemNumber" app:layout_constraintEnd_toStartOf="@id/timetableItemGroup"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@+id/timetableItemTimeStart" app:layout_constraintStart_toEndOf="@+id/timetableItemTimeStart"
app:layout_constraintTop_toTopOf="@id/timetableItemTimeFinish"
tools:text="22" tools:text="22"
tools:visibility="visible" /> tools:visibility="visible" />
@ -83,13 +94,14 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="10dp" android:layout_marginStart="10dp"
android:layout_marginTop="0dp" android:layout_marginEnd="0dp"
android:layout_marginEnd="5dp" android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorSecondary" android:textColor="?android:textColorSecondary"
android:textSize="13sp" android:textSize="13sp"
app:layout_constraintEnd_toStartOf="@+id/timetableItemTeacher" app:layout_constraintEnd_toStartOf="@+id/timetableItemTeacher"
app:layout_constraintStart_toEndOf="@+id/timetableItemRoom" app:layout_constraintStart_toEndOf="@+id/timetableItemRoom"
app:layout_constraintTop_toTopOf="@+id/timetableItemTimeFinish" app:layout_constraintTop_toTopOf="@id/timetableItemTimeFinish"
tools:text="(2/2)" tools:text="(2/2)"
tools:visibility="visible" /> tools:visibility="visible" />
@ -98,13 +110,15 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="10dp" android:layout_marginStart="10dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="8dp"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textColor="?android:textColorSecondary" android:textColor="?android:textColorSecondary"
android:textSize="13sp" android:textSize="13sp"
app:layout_constraintBottom_toBottomOf="@+id/timetableItemNumber" app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/timetableItemGroup" app:layout_constraintStart_toEndOf="@id/timetableItemGroup"
app:layout_constraintTop_toTopOf="@id/timetableItemTimeFinish"
tools:text="Agata Kowalska - Błaszczyk" tools:text="Agata Kowalska - Błaszczyk"
tools:visibility="visible" /> tools:visibility="visible" />
@ -118,8 +132,8 @@
android:textColor="?colorTimetableChange" android:textColor="?colorTimetableChange"
android:textSize="13sp" android:textSize="13sp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/timetableItemTeacher" app:layout_constraintStart_toEndOf="@id/timetableItemTimeFinish"
app:layout_constraintTop_toTopOf="@+id/timetableItemTimeFinish" app:layout_constraintTop_toTopOf="@id/timetableItemTimeFinish"
tools:text="Lekcja odwołana: uczniowie zwolnieni do domu" tools:text="Lekcja odwołana: uczniowie zwolnieni do domu"
tools:visibility="gone" /> tools:visibility="gone" />
@ -168,7 +182,7 @@
android:visibility="gone" android:visibility="gone"
app:backgroundTint="?colorPrimary" app:backgroundTint="?colorPrimary"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="@id/timetableItemTimeStart"
tools:text="jeszcze 15 min" tools:text="jeszcze 15 min"
tools:visibility="visible" /> tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android" <TextView 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:id="@+id/dashboard_small_grade_subitem_value" android:id="@+id/dashboard_small_grade_subitem_value"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -10,7 +11,11 @@
android:gravity="center" android:gravity="center"
android:maxLength="5" android:maxLength="5"
android:minWidth="20dp" android:minWidth="20dp"
android:padding="1dp"
android:textColor="@android:color/white" android:textColor="@android:color/white"
android:textSize="12sp" android:textSize="12sp"
android:textStyle="bold" android:textStyle="bold"
app:autoSizeMaxTextSize="14dp"
app:autoSizeMinTextSize="10dp"
app:autoSizeTextType="uniform"
tools:text="6" /> tools:text="6" />

View File

@ -29,6 +29,13 @@
android:title="@string/message_forward" android:title="@string/message_forward"
app:iconTint="@color/material_on_surface_emphasis_medium" app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
<item
android:id="@+id/messagePreviewMenuRestore"
android:icon="@drawable/ic_menu_message_restore"
android:orderInCategory="1"
android:title="@string/message_restore_from_trash"
app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="ifRoom" />
<item <item
android:id="@+id/messagePreviewMenuDelete" android:id="@+id/messagePreviewMenuDelete"
android:icon="@drawable/ic_menu_message_delete" android:icon="@drawable/ic_menu_message_delete"
@ -36,4 +43,18 @@
android:title="@string/message_move_to_trash" android:title="@string/message_move_to_trash"
app:iconTint="@color/material_on_surface_emphasis_medium" app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
<item
android:id="@+id/messagePreviewMenuDeleteForever"
android:icon="@drawable/ic_menu_message_delete_forever"
android:orderInCategory="1"
android:title="@string/message_delete_forever"
app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="ifRoom" />
<item
android:id="@+id/messagePreviewMenuMute"
android:icon="@drawable/ic_settings_notifications"
android:orderInCategory="1"
android:title="@string/message_mute"
app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="ifRoom" />
</menu> </menu>

View File

@ -1,6 +1,13 @@
<?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/messageTabContextMenuRestore"
android:icon="@drawable/ic_menu_message_restore"
android:orderInCategory="1"
android:title="@string/message_restore_from_trash"
app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="always" />
<item <item
android:id="@+id/messageTabContextMenuDelete" android:id="@+id/messageTabContextMenuDelete"
android:icon="@drawable/ic_menu_message_delete" android:icon="@drawable/ic_menu_message_delete"
@ -8,6 +15,13 @@
android:title="@string/message_move_to_trash" android:title="@string/message_move_to_trash"
app:iconTint="@color/material_on_surface_emphasis_medium" app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="always" /> app:showAsAction="always" />
<item
android:id="@+id/messageTabContextMenuDeleteForever"
android:icon="@drawable/ic_menu_message_delete_forever"
android:orderInCategory="1"
android:title="@string/message_delete_forever"
app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="always" />
<item <item
android:id="@+id/messageTabContextMenuSelectAll" android:id="@+id/messageTabContextMenuSelectAll"
android:icon="@drawable/ic_message_select_all" android:icon="@drawable/ic_message_select_all"

View File

@ -56,7 +56,7 @@
<string name="login_invalid_email">Neplatný e-mail</string> <string name="login_invalid_email">Neplatný e-mail</string>
<string name="login_invalid_login">Místo e-mailu použijte přiřazené přihlašovací údaje</string> <string name="login_invalid_login">Místo e-mailu použijte přiřazené přihlašovací údaje</string>
<string name="login_invalid_custom_email">Použijte přiřazené přihlašovací nebo e-mail v @%1$s</string> <string name="login_invalid_custom_email">Použijte přiřazené přihlašovací nebo e-mail v @%1$s</string>
<string name="login_invalid_domain_suffix">Invalid domain suffix</string> <string name="login_invalid_domain_suffix">Neplatná přípona domény</string>
<string name="login_invalid_symbol">Neplatný symbol. Pokud jej nemůžete najít, kontaktujte školu</string> <string name="login_invalid_symbol">Neplatný symbol. Pokud jej nemůžete najít, kontaktujte školu</string>
<string name="login_invalid_symbol_definitely">Nevymýšlejte si! Pokud symbol nemůžete najít, kontaktujte školu</string> <string name="login_invalid_symbol_definitely">Nevymýšlejte si! Pokud symbol nemůžete najít, kontaktujte školu</string>
<string name="login_incorrect_symbol">Žák nebyl nalezen. Zkontrolujte správnost symbolu a vybrané varianty deníku UONET+</string> <string name="login_incorrect_symbol">Žák nebyl nalezen. Zkontrolujte správnost symbolu a vybrané varianty deníku UONET+</string>
@ -98,8 +98,8 @@
<string name="main_log_in">Přihlásit se</string> <string name="main_log_in">Přihlásit se</string>
<string name="main_session_expired">Relace vypršela</string> <string name="main_session_expired">Relace vypršela</string>
<string name="main_session_relogin">Relace vypršela. Přihlaste se prosím znovu</string> <string name="main_session_relogin">Relace vypršela. Přihlaste se prosím znovu</string>
<string name="main_expired_credentials_description">Heslo k vašemu účtu bylo změněno. Musíte se znovu přihlásit do Wulkanového</string> <string name="main_expired_credentials_title">Heslo vypršelo nebo bylo změněno</string>
<string name="main_expired_credentials_title">Heslo bylo změněno</string> <string name="main_expired_credentials_description">Platnost hesla k vašemu účtu vypršela nebo bylo změněno. Budete se muset znovu přihlásit do Wulkanového</string>
<string name="main_support_title">Podpora aplikace</string> <string name="main_support_title">Podpora aplikace</string>
<string name="main_support_description">Líbí se Vám tato aplikace? Podpořte její vývoj tím, že povolíte neinvazivní reklamy, které můžete kdykoliv vypnout</string> <string name="main_support_description">Líbí se Vám tato aplikace? Podpořte její vývoj tím, že povolíte neinvazivní reklamy, které můžete kdykoliv vypnout</string>
<string name="main_support_positive">Zapnout reklamy</string> <string name="main_support_positive">Zapnout reklamy</string>
@ -336,8 +336,10 @@
<string name="message_forward">Poslat dále</string> <string name="message_forward">Poslat dále</string>
<string name="message_select_all">Vybrat vše</string> <string name="message_select_all">Vybrat vše</string>
<string name="message_unselect_all">Odznačit vše</string> <string name="message_unselect_all">Odznačit vše</string>
<string name="message_restore_from_trash">Obnovit z koše</string>
<string name="message_move_to_trash">Přesunout do koše</string> <string name="message_move_to_trash">Přesunout do koše</string>
<string name="message_delete_forever">Odstranit natrvalo</string> <string name="message_delete_forever">Odstranit natrvalo</string>
<string name="message_restore_success">Zpráva úspěšně obnovena</string>
<string name="message_delete_success">Zpráva byla úspěšně odstraněna</string> <string name="message_delete_success">Zpráva byla úspěšně odstraněna</string>
<string name="message_mailbox_type_student">žák</string> <string name="message_mailbox_type_student">žák</string>
<string name="message_mailbox_type_parent">rodič</string> <string name="message_mailbox_type_parent">rodič</string>
@ -383,6 +385,7 @@
<item quantity="other">%1$d vybraných</item> <item quantity="other">%1$d vybraných</item>
</plurals> </plurals>
<string name="message_messages_deleted">Zprávy odstraněné</string> <string name="message_messages_deleted">Zprávy odstraněné</string>
<string name="message_messages_restored">Obnovené zprávy</string>
<string name="message_mailbox_chooser_title">Vyberte poštovní schránku</string> <string name="message_mailbox_chooser_title">Vyberte poštovní schránku</string>
<string name="message_incognito_mode_on">Anonymní režim je zapnutý</string> <string name="message_incognito_mode_on">Anonymní režim je zapnutý</string>
<string name="message_incognito_description">Díky anonymnímu režimu není odesílatel upozorněn, když si zprávu přečtete</string> <string name="message_incognito_description">Díky anonymnímu režimu není odesílatel upozorněn, když si zprávu přečtete</string>
@ -849,13 +852,16 @@
<string name="auth_description">Pro provoz aplikace potřebujeme potvrdit vaši identitu. Zadejte PESEL žáka &lt;b&gt;%1$s&lt;/b&gt; v níže uvedeném poli</string> <string name="auth_description">Pro provoz aplikace potřebujeme potvrdit vaši identitu. Zadejte PESEL žáka &lt;b&gt;%1$s&lt;/b&gt; v níže uvedeném poli</string>
<string name="auth_button_skip">Zatím přeskočit</string> <string name="auth_button_skip">Zatím přeskočit</string>
<!--Captcha--> <!--Captcha-->
<string name="captcha_dialog_title">Probíhá ověřování. Počkejte…</string> <string name="captcha_dialog_title">VULCAN\'s website requires verification</string>
<string name="captcha_dialog_description"><b>Why am I seeing this?</b>\nThe register website from which Wulkanowy downloads data displays the same screen as above, so Wulkanowy must also show it to be able to download data from this website. There\'s no way around it</string>
<string name="captcha_verified_message">Úspěšně ověřeno</string> <string name="captcha_verified_message">Úspěšně ověřeno</string>
<!--Errors--> <!--Errors-->
<string name="error_no_internet">Žádné internetové připojení</string> <string name="error_no_internet">Žádné internetové připojení</string>
<string name="error_invalid_device_datetime">Vyskytla se chyba. Zkontrolujte hodiny svého zařízení</string> <string name="error_invalid_device_datetime">Vyskytla se chyba. Zkontrolujte hodiny svého zařízení</string>
<string name="error_account_inactive">Tento účet je neaktivní. Zkuste se znovu přihlásit</string>
<string name="error_timeout">Nelze se připojit ke deníku. Servery mohou být přetíženy. Prosím zkuste to znovu později</string> <string name="error_timeout">Nelze se připojit ke deníku. Servery mohou být přetíženy. Prosím zkuste to znovu později</string>
<string name="error_login_failed">Načítání dat se nezdařilo. Prosím zkuste to znovu později</string> <string name="error_login_failed">Načítání dat se nezdařilo. Prosím zkuste to znovu později</string>
<string name="error_password_invalid">Vaše heslo vypršelo nebo bylo změněno. Přihlaste se znovu</string>
<string name="error_password_change_required">Je vyžadována změna hesla pro deník</string> <string name="error_password_change_required">Je vyžadována změna hesla pro deník</string>
<string name="error_service_unavailable">Probíhá údržba deníku UONET+. Zkuste to později znovu</string> <string name="error_service_unavailable">Probíhá údržba deníku UONET+. Zkuste to později znovu</string>
<string name="error_unknown_uonet">Neznámá chyba deniku UONET+. Prosím zkuste to znovu později</string> <string name="error_unknown_uonet">Neznámá chyba deniku UONET+. Prosím zkuste to znovu později</string>
@ -865,4 +871,9 @@
<string name="error_feature_disabled">Funkce je deaktivována přes vaší školou</string> <string name="error_feature_disabled">Funkce je deaktivována přes vaší školou</string>
<string name="error_feature_not_available">Funkce není k dispozici. Přihlaste se v jiném režimu než Mobile API</string> <string name="error_feature_not_available">Funkce není k dispozici. Přihlaste se v jiném režimu než Mobile API</string>
<string name="error_field_required">Toto pole je povinné</string> <string name="error_field_required">Toto pole je povinné</string>
<!-- Mute system -->
<string name="message_mute">Ztlumit</string>
<string name="message_unmute">Zrušit ztlumení</string>
<string name="message_mute_success">Ztlumili jste tohoto uživatele</string>
<string name="message_unmute_success">Zrušili jste ztlumení tohoto uživatele</string>
</resources> </resources>

View File

@ -98,8 +98,8 @@
<string name="main_log_in">Anmelden</string> <string name="main_log_in">Anmelden</string>
<string name="main_session_expired">Die Sitzung ist abgelaufen</string> <string name="main_session_expired">Die Sitzung ist abgelaufen</string>
<string name="main_session_relogin">Die Sitzung ist abgelaufen, bitte loggen Sie sich erneut ein</string> <string name="main_session_relogin">Die Sitzung ist abgelaufen, bitte loggen Sie sich erneut ein</string>
<string name="main_expired_credentials_description">Your account password has been changed. You need to log in to Wulkanowy again</string> <string name="main_expired_credentials_title">Password has expired or been changed</string>
<string name="main_expired_credentials_title">Password changed</string> <string name="main_expired_credentials_description">Your account password has expired or been changed. You will need to log in to Wulkanowy again</string>
<string name="main_support_title">Anwendungsunterstützung</string> <string name="main_support_title">Anwendungsunterstützung</string>
<string name="main_support_description">Gefällt Ihnen diese App? Unterstützen Sie ihre Entwicklung, indem Sie nicht-invasive Werbung aktivieren, die Sie jederzeit deaktivieren können</string> <string name="main_support_description">Gefällt Ihnen diese App? Unterstützen Sie ihre Entwicklung, indem Sie nicht-invasive Werbung aktivieren, die Sie jederzeit deaktivieren können</string>
<string name="main_support_positive">Werbung aktivieren</string> <string name="main_support_positive">Werbung aktivieren</string>
@ -296,8 +296,10 @@
<string name="message_forward">Weiterleiten</string> <string name="message_forward">Weiterleiten</string>
<string name="message_select_all">Alle auswählen</string> <string name="message_select_all">Alle auswählen</string>
<string name="message_unselect_all">Alle abwählen</string> <string name="message_unselect_all">Alle abwählen</string>
<string name="message_restore_from_trash">Restore from trash</string>
<string name="message_move_to_trash">In Papierkorb verschieben</string> <string name="message_move_to_trash">In Papierkorb verschieben</string>
<string name="message_delete_forever">Dauerhaft löschen</string> <string name="message_delete_forever">Dauerhaft löschen</string>
<string name="message_restore_success">Message restored successfully</string>
<string name="message_delete_success">Nachricht erfolgreich gelöscht</string> <string name="message_delete_success">Nachricht erfolgreich gelöscht</string>
<string name="message_mailbox_type_student">schüler</string> <string name="message_mailbox_type_student">schüler</string>
<string name="message_mailbox_type_parent">Eltern</string> <string name="message_mailbox_type_parent">Eltern</string>
@ -335,6 +337,7 @@
<item quantity="other">%1$d ausgewählt</item> <item quantity="other">%1$d ausgewählt</item>
</plurals> </plurals>
<string name="message_messages_deleted">Nachrichten gelöscht</string> <string name="message_messages_deleted">Nachrichten gelöscht</string>
<string name="message_messages_restored">Messages restored</string>
<string name="message_mailbox_chooser_title">Postfach auswählen</string> <string name="message_mailbox_chooser_title">Postfach auswählen</string>
<string name="message_incognito_mode_on">Incognito mode is on</string> <string name="message_incognito_mode_on">Incognito mode is on</string>
<string name="message_incognito_description">Thanks to incognito mode sender is not notified when you read the message</string> <string name="message_incognito_description">Thanks to incognito mode sender is not notified when you read the message</string>
@ -755,13 +758,16 @@
<string name="auth_description">To operate the application, we need to confirm your identity. Please enter the student\'s PESEL &lt;b&gt;%1$s&lt;/b&gt; in the field below</string> <string name="auth_description">To operate the application, we need to confirm your identity. Please enter the student\'s PESEL &lt;b&gt;%1$s&lt;/b&gt; in the field below</string>
<string name="auth_button_skip">Skip for now</string> <string name="auth_button_skip">Skip for now</string>
<!--Captcha--> <!--Captcha-->
<string name="captcha_dialog_title">Verification is in progress. Wait…</string> <string name="captcha_dialog_title">VULCAN\'s website requires verification</string>
<string name="captcha_dialog_description"><b>Why am I seeing this?</b>\nThe register website from which Wulkanowy downloads data displays the same screen as above, so Wulkanowy must also show it to be able to download data from this website. There\'s no way around it</string>
<string name="captcha_verified_message">Verified successfully</string> <string name="captcha_verified_message">Verified successfully</string>
<!--Errors--> <!--Errors-->
<string name="error_no_internet">Keine Internetverbindung</string> <string name="error_no_internet">Keine Internetverbindung</string>
<string name="error_invalid_device_datetime">Es ist ein Fehler aufgetreten. Überprüfen Sie Ihre Geräteuhr</string> <string name="error_invalid_device_datetime">Es ist ein Fehler aufgetreten. Überprüfen Sie Ihre Geräteuhr</string>
<string name="error_account_inactive">This account is inactive. Try logging in again</string>
<string name="error_timeout">Registrierungsverbindung fehlgeschlagen. Server können überlastet sein. Bitte versuchen Sie es später noch einmal</string> <string name="error_timeout">Registrierungsverbindung fehlgeschlagen. Server können überlastet sein. Bitte versuchen Sie es später noch einmal</string>
<string name="error_login_failed">Das Laden der Daten ist fehlgeschlagen. Bitte versuchen Sie es später noch einmal</string> <string name="error_login_failed">Das Laden der Daten ist fehlgeschlagen. Bitte versuchen Sie es später noch einmal</string>
<string name="error_password_invalid">Your password has expired or been changed. Please log in again</string>
<string name="error_password_change_required">Passwortänderung für Registrierung erforderlich</string> <string name="error_password_change_required">Passwortänderung für Registrierung erforderlich</string>
<string name="error_service_unavailable">Wartung im Gange UONET + Klassenbuch. Versuchen Sie es später noch einmal</string> <string name="error_service_unavailable">Wartung im Gange UONET + Klassenbuch. Versuchen Sie es später noch einmal</string>
<string name="error_unknown_uonet">Unbekannter UONET + Registerfehler. Versuchen Sie es später erneut</string> <string name="error_unknown_uonet">Unbekannter UONET + Registerfehler. Versuchen Sie es später erneut</string>
@ -771,4 +777,9 @@
<string name="error_feature_disabled">Funktion, die von Ihrer Schule deaktiviert wurde</string> <string name="error_feature_disabled">Funktion, die von Ihrer Schule deaktiviert wurde</string>
<string name="error_feature_not_available">Feature in diesem Modus nicht verfügbar</string> <string name="error_feature_not_available">Feature in diesem Modus nicht verfügbar</string>
<string name="error_field_required">Dieses Feld ist erforderlich</string> <string name="error_field_required">Dieses Feld ist erforderlich</string>
<!-- Mute system -->
<string name="message_mute">Mute</string>
<string name="message_unmute">Unmute</string>
<string name="message_mute_success">You have muted this user</string>
<string name="message_unmute_success">You have unmuted this user</string>
</resources> </resources>

View File

@ -98,8 +98,8 @@
<string name="main_log_in">Zaloguj się</string> <string name="main_log_in">Zaloguj się</string>
<string name="main_session_expired">Sesja wygasła</string> <string name="main_session_expired">Sesja wygasła</string>
<string name="main_session_relogin">Sesja wygasła, zaloguj się ponownie</string> <string name="main_session_relogin">Sesja wygasła, zaloguj się ponownie</string>
<string name="main_expired_credentials_description">Hasło do Twojego konta zostało zmienione. Musisz zalogować się ponownie do Wulkanowego</string> <string name="main_expired_credentials_title">Hasło wygasło lub zostało zmienione</string>
<string name="main_expired_credentials_title">Hasło zostało zmienione</string> <string name="main_expired_credentials_description">Hasło do twojego konta wygasło lub zostało zmienione. Musisz zalogować się ponownie do Wulkanowego</string>
<string name="main_support_title">Wparcie aplikacji</string> <string name="main_support_title">Wparcie aplikacji</string>
<string name="main_support_description">Podoba Ci się ta aplikacja? Wspieraj jej rozwój poprzez włączenie nieinwazyjnych reklam, które możesz wyłączyć w dowolnym momencie</string> <string name="main_support_description">Podoba Ci się ta aplikacja? Wspieraj jej rozwój poprzez włączenie nieinwazyjnych reklam, które możesz wyłączyć w dowolnym momencie</string>
<string name="main_support_positive">Włącz reklamy</string> <string name="main_support_positive">Włącz reklamy</string>
@ -336,8 +336,10 @@
<string name="message_forward">Prześlij dalej</string> <string name="message_forward">Prześlij dalej</string>
<string name="message_select_all">Zaznacz wszystkie</string> <string name="message_select_all">Zaznacz wszystkie</string>
<string name="message_unselect_all">Odznacz wszystkie</string> <string name="message_unselect_all">Odznacz wszystkie</string>
<string name="message_restore_from_trash">Przywróć z kosza</string>
<string name="message_move_to_trash">Przenieś do kosza</string> <string name="message_move_to_trash">Przenieś do kosza</string>
<string name="message_delete_forever">Usuń trwale</string> <string name="message_delete_forever">Usuń trwale</string>
<string name="message_restore_success">Wiadomość przywrócona pomyślnie</string>
<string name="message_delete_success">Wiadomość usunięta pomyślnie</string> <string name="message_delete_success">Wiadomość usunięta pomyślnie</string>
<string name="message_mailbox_type_student">uczeń</string> <string name="message_mailbox_type_student">uczeń</string>
<string name="message_mailbox_type_parent">rodzic</string> <string name="message_mailbox_type_parent">rodzic</string>
@ -383,6 +385,7 @@
<item quantity="other">%1$d wybranych</item> <item quantity="other">%1$d wybranych</item>
</plurals> </plurals>
<string name="message_messages_deleted">Wiadomości zostały usunięte</string> <string name="message_messages_deleted">Wiadomości zostały usunięte</string>
<string name="message_messages_restored">Wiadomości przywrócone</string>
<string name="message_mailbox_chooser_title">Wybierz skrzynkę</string> <string name="message_mailbox_chooser_title">Wybierz skrzynkę</string>
<string name="message_incognito_mode_on">Tryb incognito jest włączony</string> <string name="message_incognito_mode_on">Tryb incognito jest włączony</string>
<string name="message_incognito_description">Dzięki trybowi incognito nadawca nie zobaczy, że przeczytałeś tę wiadomość</string> <string name="message_incognito_description">Dzięki trybowi incognito nadawca nie zobaczy, że przeczytałeś tę wiadomość</string>
@ -849,13 +852,16 @@
<string name="auth_description">Rodzicu, musimy mieć pewność, że Twój adres e-mail został powiązany z prawidłowym kontem ucznia. W celu autoryzacji konta podaj numer PESEL ucznia &lt;b&gt;%1$s&lt;/b&gt; w polu poniżej</string> <string name="auth_description">Rodzicu, musimy mieć pewność, że Twój adres e-mail został powiązany z prawidłowym kontem ucznia. W celu autoryzacji konta podaj numer PESEL ucznia &lt;b&gt;%1$s&lt;/b&gt; w polu poniżej</string>
<string name="auth_button_skip">Na razie pomiń</string> <string name="auth_button_skip">Na razie pomiń</string>
<!--Captcha--> <!--Captcha-->
<string name="captcha_dialog_title">Trwa weryfikacja. Czekaj…</string> <string name="captcha_dialog_title">Strona dziennika VULCAN wymaga weryfikacji</string>
<string name="captcha_dialog_description"><b>Dlaczego to widzę?</b>\nStrona internetowa dziennika, z której Wulkanowy pobiera dane, wyświetla ten sam ekran jak powyżej, więc Wulkanowy musi również ją pokazać, aby móc pobrać dane z tej witryny. Nie da się tego obejść</string>
<string name="captcha_verified_message">Pomyślnie zweryfikowano</string> <string name="captcha_verified_message">Pomyślnie zweryfikowano</string>
<!--Errors--> <!--Errors-->
<string name="error_no_internet">Brak połączenia z internetem</string> <string name="error_no_internet">Brak połączenia z internetem</string>
<string name="error_invalid_device_datetime">Wystąpił błąd. Sprawdź poprawność daty w urządzeniu</string> <string name="error_invalid_device_datetime">Wystąpił błąd. Sprawdź poprawność daty w urządzeniu</string>
<string name="error_account_inactive">Konto jest nieaktywne. Spróbuj zalogować się ponownie</string>
<string name="error_timeout">Nie udało się połączyć z dziennikiem. Serwery mogą być przeciążone. Spróbuj ponownie później</string> <string name="error_timeout">Nie udało się połączyć z dziennikiem. Serwery mogą być przeciążone. Spróbuj ponownie później</string>
<string name="error_login_failed">Ładowanie danych nie powiodło się. Spróbuj ponownie później</string> <string name="error_login_failed">Ładowanie danych nie powiodło się. Spróbuj ponownie później</string>
<string name="error_password_invalid">Twoje hasło wygasło lub zostało zmienione. Zaloguj się ponownie</string>
<string name="error_password_change_required">Wymagana zmiana hasła do dziennika</string> <string name="error_password_change_required">Wymagana zmiana hasła do dziennika</string>
<string name="error_service_unavailable">Trwa przerwa techniczna dziennika UONET+. Spróbuj ponownie później</string> <string name="error_service_unavailable">Trwa przerwa techniczna dziennika UONET+. Spróbuj ponownie później</string>
<string name="error_unknown_uonet">Nieznany błąd dziennika UONET+. Spróbuj ponownie później</string> <string name="error_unknown_uonet">Nieznany błąd dziennika UONET+. Spróbuj ponownie później</string>
@ -865,4 +871,9 @@
<string name="error_feature_disabled">Funkcja wyłączona przez szkołę</string> <string name="error_feature_disabled">Funkcja wyłączona przez szkołę</string>
<string name="error_feature_not_available">Funkcja niedostępna. Zaloguj się w trybie innym niż Mobilne API</string> <string name="error_feature_not_available">Funkcja niedostępna. Zaloguj się w trybie innym niż Mobilne API</string>
<string name="error_field_required">To pole jest wymagane</string> <string name="error_field_required">To pole jest wymagane</string>
<!-- Mute system -->
<string name="message_mute">Wycisz</string>
<string name="message_unmute">Wyłącz wyciszenie</string>
<string name="message_mute_success">Wyciszyleś tego użytkownika</string>
<string name="message_unmute_success">Wyłączyłeś wyciszenie tego użytkownika</string>
</resources> </resources>

View File

@ -98,8 +98,8 @@
<string name="main_log_in">Войти</string> <string name="main_log_in">Войти</string>
<string name="main_session_expired">Сеанс истёк</string> <string name="main_session_expired">Сеанс истёк</string>
<string name="main_session_relogin">Сеанс истёк, авторизуйтесь снова</string> <string name="main_session_relogin">Сеанс истёк, авторизуйтесь снова</string>
<string name="main_expired_credentials_description">Your account password has been changed. You need to log in to Wulkanowy again</string> <string name="main_expired_credentials_title">Password has expired or been changed</string>
<string name="main_expired_credentials_title">Password changed</string> <string name="main_expired_credentials_description">Your account password has expired or been changed. You will need to log in to Wulkanowy again</string>
<string name="main_support_title">Поддержка приложения</string> <string name="main_support_title">Поддержка приложения</string>
<string name="main_support_description">Вам нравится это приложение? Поддержите его разработку, включив неинвазивную рекламу, которую можно отключить в любое время</string> <string name="main_support_description">Вам нравится это приложение? Поддержите его разработку, включив неинвазивную рекламу, которую можно отключить в любое время</string>
<string name="main_support_positive">Включить рекламу</string> <string name="main_support_positive">Включить рекламу</string>
@ -336,8 +336,10 @@
<string name="message_forward">Переслать</string> <string name="message_forward">Переслать</string>
<string name="message_select_all">Выбрать все</string> <string name="message_select_all">Выбрать все</string>
<string name="message_unselect_all">Отменить выбор</string> <string name="message_unselect_all">Отменить выбор</string>
<string name="message_restore_from_trash">Restore from trash</string>
<string name="message_move_to_trash">Перенести в корзину</string> <string name="message_move_to_trash">Перенести в корзину</string>
<string name="message_delete_forever">Удалить навсегда</string> <string name="message_delete_forever">Удалить навсегда</string>
<string name="message_restore_success">Message restored successfully</string>
<string name="message_delete_success">Сообщение успешно удалено</string> <string name="message_delete_success">Сообщение успешно удалено</string>
<string name="message_mailbox_type_student">ученик</string> <string name="message_mailbox_type_student">ученик</string>
<string name="message_mailbox_type_parent">родитель</string> <string name="message_mailbox_type_parent">родитель</string>
@ -383,6 +385,7 @@
<item quantity="other">%1$d выбрано</item> <item quantity="other">%1$d выбрано</item>
</plurals> </plurals>
<string name="message_messages_deleted">Сообщение удалено</string> <string name="message_messages_deleted">Сообщение удалено</string>
<string name="message_messages_restored">Messages restored</string>
<string name="message_mailbox_chooser_title">Выбрать почтовый ящик</string> <string name="message_mailbox_chooser_title">Выбрать почтовый ящик</string>
<string name="message_incognito_mode_on">Incognito mode is on</string> <string name="message_incognito_mode_on">Incognito mode is on</string>
<string name="message_incognito_description">Thanks to incognito mode sender is not notified when you read the message</string> <string name="message_incognito_description">Thanks to incognito mode sender is not notified when you read the message</string>
@ -849,13 +852,16 @@
<string name="auth_description">Для работы приложения нам необходимо подтвердить вашу личность. Введите PESEL учащегося &lt;b&gt;%1$s&lt;/b&gt; в поле ниже</string> <string name="auth_description">Для работы приложения нам необходимо подтвердить вашу личность. Введите PESEL учащегося &lt;b&gt;%1$s&lt;/b&gt; в поле ниже</string>
<string name="auth_button_skip">Пропустить сейчас</string> <string name="auth_button_skip">Пропустить сейчас</string>
<!--Captcha--> <!--Captcha-->
<string name="captcha_dialog_title">Verification is in progress. Wait…</string> <string name="captcha_dialog_title">VULCAN\'s website requires verification</string>
<string name="captcha_dialog_description"><b>Why am I seeing this?</b>\nThe register website from which Wulkanowy downloads data displays the same screen as above, so Wulkanowy must also show it to be able to download data from this website. There\'s no way around it</string>
<string name="captcha_verified_message">Verified successfully</string> <string name="captcha_verified_message">Verified successfully</string>
<!--Errors--> <!--Errors-->
<string name="error_no_internet">Интернет-соединение отсутствует</string> <string name="error_no_internet">Интернет-соединение отсутствует</string>
<string name="error_invalid_device_datetime">Произошла ошибка. Проверьте время на вашем устройстве</string> <string name="error_invalid_device_datetime">Произошла ошибка. Проверьте время на вашем устройстве</string>
<string name="error_account_inactive">This account is inactive. Try logging in again</string>
<string name="error_timeout">Не удалось подключиться к дневнику. Возможно, сервера перегружены, повторите попытку позже</string> <string name="error_timeout">Не удалось подключиться к дневнику. Возможно, сервера перегружены, повторите попытку позже</string>
<string name="error_login_failed">Не удалось загрузить данные, повторите попытку позже</string> <string name="error_login_failed">Не удалось загрузить данные, повторите попытку позже</string>
<string name="error_password_invalid">Your password has expired or been changed. Please log in again</string>
<string name="error_password_change_required">Необходимо изменить пароль дневника</string> <string name="error_password_change_required">Необходимо изменить пароль дневника</string>
<string name="error_service_unavailable">UONET+ проводит техническое обслуживание, повторите попытку позже</string> <string name="error_service_unavailable">UONET+ проводит техническое обслуживание, повторите попытку позже</string>
<string name="error_unknown_uonet">Неизвестная ошибка дневника UONET+, повторите попытку позже</string> <string name="error_unknown_uonet">Неизвестная ошибка дневника UONET+, повторите попытку позже</string>
@ -865,4 +871,9 @@
<string name="error_feature_disabled">Функция отключена вашей школой</string> <string name="error_feature_disabled">Функция отключена вашей школой</string>
<string name="error_feature_not_available">Функция недоступна в режиме Mobile API. Воспользуйтесь другим режимом</string> <string name="error_feature_not_available">Функция недоступна в режиме Mobile API. Воспользуйтесь другим режимом</string>
<string name="error_field_required">Это поле обязательно</string> <string name="error_field_required">Это поле обязательно</string>
<!-- Mute system -->
<string name="message_mute">Mute</string>
<string name="message_unmute">Unmute</string>
<string name="message_mute_success">You have muted this user</string>
<string name="message_unmute_success">You have unmuted this user</string>
</resources> </resources>

View File

@ -56,7 +56,7 @@
<string name="login_invalid_email">Neplatný e-mail</string> <string name="login_invalid_email">Neplatný e-mail</string>
<string name="login_invalid_login">Namiesto e-mailu použite priradené prihlasovacie údaje</string> <string name="login_invalid_login">Namiesto e-mailu použite priradené prihlasovacie údaje</string>
<string name="login_invalid_custom_email">Použite priradené prihlasovacie alebo e-mail v @%1$s</string> <string name="login_invalid_custom_email">Použite priradené prihlasovacie alebo e-mail v @%1$s</string>
<string name="login_invalid_domain_suffix">Invalid domain suffix</string> <string name="login_invalid_domain_suffix">Neplatná prípona domény</string>
<string name="login_invalid_symbol">Neplatný symbol. Pokiaľ ho nemôžete nájsť, kontaktujte školu</string> <string name="login_invalid_symbol">Neplatný symbol. Pokiaľ ho nemôžete nájsť, kontaktujte školu</string>
<string name="login_invalid_symbol_definitely">Nevymýšľajte si! Pokiaľ symbol nemôžete nájsť, kontaktujte školu</string> <string name="login_invalid_symbol_definitely">Nevymýšľajte si! Pokiaľ symbol nemôžete nájsť, kontaktujte školu</string>
<string name="login_incorrect_symbol">Žiak nebol nájdený. Skontrolujte správnosť symbolu a vybrané varianty denníka UONET+</string> <string name="login_incorrect_symbol">Žiak nebol nájdený. Skontrolujte správnosť symbolu a vybrané varianty denníka UONET+</string>
@ -98,8 +98,8 @@
<string name="main_log_in">Prihlásiť sa</string> <string name="main_log_in">Prihlásiť sa</string>
<string name="main_session_expired">Relácia vypršala</string> <string name="main_session_expired">Relácia vypršala</string>
<string name="main_session_relogin">Relácia vypršala. Prihláste sa prosím znovu</string> <string name="main_session_relogin">Relácia vypršala. Prihláste sa prosím znovu</string>
<string name="main_expired_credentials_description">Heslo k vášmu účtu bolo zmenené. Musíte sa znovu prihlásiť do Wulkanového</string> <string name="main_expired_credentials_title">Heslo vypršalo alebo bolo zmenené</string>
<string name="main_expired_credentials_title">Heslo bolo zmenené</string> <string name="main_expired_credentials_description">Platnosť hesla k vášmu účtu vypršala alebo bolo zmenené. Budete sa musieť znova prihlásiť do Wulkanového</string>
<string name="main_support_title">Podpora aplikácie</string> <string name="main_support_title">Podpora aplikácie</string>
<string name="main_support_description">Páči sa Vám táto aplikácia? Podporte jej vývoj tým, že povolíte neinvazívne reklamy, ktoré môžete kedykoľvek vypnúť</string> <string name="main_support_description">Páči sa Vám táto aplikácia? Podporte jej vývoj tým, že povolíte neinvazívne reklamy, ktoré môžete kedykoľvek vypnúť</string>
<string name="main_support_positive">Zapnúť reklamy</string> <string name="main_support_positive">Zapnúť reklamy</string>
@ -336,8 +336,10 @@
<string name="message_forward">Poslať ďalej</string> <string name="message_forward">Poslať ďalej</string>
<string name="message_select_all">Vybrať všetko</string> <string name="message_select_all">Vybrať všetko</string>
<string name="message_unselect_all">Odznačiť všetko</string> <string name="message_unselect_all">Odznačiť všetko</string>
<string name="message_restore_from_trash">Obnoviť z koša</string>
<string name="message_move_to_trash">Presunúť do koša</string> <string name="message_move_to_trash">Presunúť do koša</string>
<string name="message_delete_forever">Odstrániť natrvalo</string> <string name="message_delete_forever">Odstrániť natrvalo</string>
<string name="message_restore_success">Správa úspešne obnovená</string>
<string name="message_delete_success">Správa bola úspešne odstránená</string> <string name="message_delete_success">Správa bola úspešne odstránená</string>
<string name="message_mailbox_type_student">žiak</string> <string name="message_mailbox_type_student">žiak</string>
<string name="message_mailbox_type_parent">rodič</string> <string name="message_mailbox_type_parent">rodič</string>
@ -383,6 +385,7 @@
<item quantity="other">%1$d vybraných</item> <item quantity="other">%1$d vybraných</item>
</plurals> </plurals>
<string name="message_messages_deleted">Správy odstránené</string> <string name="message_messages_deleted">Správy odstránené</string>
<string name="message_messages_restored">Obnovené správy</string>
<string name="message_mailbox_chooser_title">Vyberte poštovú schránku</string> <string name="message_mailbox_chooser_title">Vyberte poštovú schránku</string>
<string name="message_incognito_mode_on">Režim inkognito je zapnutý</string> <string name="message_incognito_mode_on">Režim inkognito je zapnutý</string>
<string name="message_incognito_description">Vďaka inkognito režimu nie je odosielateľ upozornený, keď si správu prečítate</string> <string name="message_incognito_description">Vďaka inkognito režimu nie je odosielateľ upozornený, keď si správu prečítate</string>
@ -849,13 +852,16 @@
<string name="auth_description">Na prevádzku aplikácie potrebujeme potvrdiť vašu identitu. Zadajte PESEL žiaka &lt;b&gt;%1$s&lt;/b&gt; v nižšie uvedenom poli</string> <string name="auth_description">Na prevádzku aplikácie potrebujeme potvrdiť vašu identitu. Zadajte PESEL žiaka &lt;b&gt;%1$s&lt;/b&gt; v nižšie uvedenom poli</string>
<string name="auth_button_skip">Zatiaľ preskočiť</string> <string name="auth_button_skip">Zatiaľ preskočiť</string>
<!--Captcha--> <!--Captcha-->
<string name="captcha_dialog_title">Overovanie prebieha. Počkajte…</string> <string name="captcha_dialog_title">VULCAN\'s website requires verification</string>
<string name="captcha_dialog_description"><b>Why am I seeing this?</b>\nThe register website from which Wulkanowy downloads data displays the same screen as above, so Wulkanowy must also show it to be able to download data from this website. There\'s no way around it</string>
<string name="captcha_verified_message">Úspešne overené</string> <string name="captcha_verified_message">Úspešne overené</string>
<!--Errors--> <!--Errors-->
<string name="error_no_internet">Žiadne internetové pripojenie</string> <string name="error_no_internet">Žiadne internetové pripojenie</string>
<string name="error_invalid_device_datetime">Vyskytla sa chyba. Skontrolujte hodiny svojho zariadenia</string> <string name="error_invalid_device_datetime">Vyskytla sa chyba. Skontrolujte hodiny svojho zariadenia</string>
<string name="error_account_inactive">Tento účet je neaktívny. Skúste sa znova prihlásiť</string>
<string name="error_timeout">Nedá sa pripojiť ku denníku. Servery môžu byť preťažené. Prosím skúste to znova neskôr</string> <string name="error_timeout">Nedá sa pripojiť ku denníku. Servery môžu byť preťažené. Prosím skúste to znova neskôr</string>
<string name="error_login_failed">Načítanie údajov zlyhalo. Skúste neskôr prosím</string> <string name="error_login_failed">Načítanie údajov zlyhalo. Skúste neskôr prosím</string>
<string name="error_password_invalid">Vaše heslo vypršalo alebo bolo zmenené. Prihláste sa znova</string>
<string name="error_password_change_required">Je vyžadovaná zmena hesla pre denník</string> <string name="error_password_change_required">Je vyžadovaná zmena hesla pre denník</string>
<string name="error_service_unavailable">Prebieha údržba denníka UONET+. Skúste to neskôr znova</string> <string name="error_service_unavailable">Prebieha údržba denníka UONET+. Skúste to neskôr znova</string>
<string name="error_unknown_uonet">Neznáma chyba dennika UONET+. Prosím skúste to znova neskôr</string> <string name="error_unknown_uonet">Neznáma chyba dennika UONET+. Prosím skúste to znova neskôr</string>
@ -865,4 +871,9 @@
<string name="error_feature_disabled">Funkcia je deaktivovaná cez vašou školou</string> <string name="error_feature_disabled">Funkcia je deaktivovaná cez vašou školou</string>
<string name="error_feature_not_available">Funkcia nie je k dispozícii. Prihláste sa v inom režime než Mobile API</string> <string name="error_feature_not_available">Funkcia nie je k dispozícii. Prihláste sa v inom režime než Mobile API</string>
<string name="error_field_required">Toto pole je povinné</string> <string name="error_field_required">Toto pole je povinné</string>
<!-- Mute system -->
<string name="message_mute">Stlmiť</string>
<string name="message_unmute">Zrušiť stlmenie</string>
<string name="message_mute_success">Stlmili ste tohto používateľa</string>
<string name="message_unmute_success">Zrušili ste stlmenie tohto používateľa</string>
</resources> </resources>

View File

@ -56,7 +56,7 @@
<string name="login_invalid_email">Недійсна адреса e-mail</string> <string name="login_invalid_email">Недійсна адреса e-mail</string>
<string name="login_invalid_login">Використовуйте призначений логін замість адреси e-mail</string> <string name="login_invalid_login">Використовуйте призначений логін замість адреси e-mail</string>
<string name="login_invalid_custom_email">Використовуйте призначений логін або адресу e-mail в @%1$s</string> <string name="login_invalid_custom_email">Використовуйте призначений логін або адресу e-mail в @%1$s</string>
<string name="login_invalid_domain_suffix">Invalid domain suffix</string> <string name="login_invalid_domain_suffix">Невірний суфікс домену</string>
<string name="login_invalid_symbol">Некоректний символ. Якщо ви не можете знайти його, будь ласка, зв\'яжіться зі школою</string> <string name="login_invalid_symbol">Некоректний символ. Якщо ви не можете знайти його, будь ласка, зв\'яжіться зі школою</string>
<string name="login_invalid_symbol_definitely">Не вигадуйте! Якщо ви не можете знайти його, будь ласка, зв\'яжіться зі школою</string> <string name="login_invalid_symbol_definitely">Не вигадуйте! Якщо ви не можете знайти його, будь ласка, зв\'яжіться зі школою</string>
<string name="login_incorrect_symbol">Студента не знайдено. Перевірте symbol та обраний тип щоденника UONET+</string> <string name="login_incorrect_symbol">Студента не знайдено. Перевірте symbol та обраний тип щоденника UONET+</string>
@ -98,8 +98,8 @@
<string name="main_log_in">Увійти</string> <string name="main_log_in">Увійти</string>
<string name="main_session_expired">Минув термін дії сесії</string> <string name="main_session_expired">Минув термін дії сесії</string>
<string name="main_session_relogin">Минув термін дії сесії, авторизуйтеся знову</string> <string name="main_session_relogin">Минув термін дії сесії, авторизуйтеся знову</string>
<string name="main_expired_credentials_description">Пароль вашого облікового запису був змінений. Ви повинні увійти в Wulkanowy знову</string> <string name="main_expired_credentials_title">Термін дії пароля закінчився або його було змінено</string>
<string name="main_expired_credentials_title">Пароль змінено</string> <string name="main_expired_credentials_description">Термін дії пароля для вашого облікового запису закінчився або було змінено. Необхідно зайти в Wulkanowy знову</string>
<string name="main_support_title">Підтримка додатку</string> <string name="main_support_title">Підтримка додатку</string>
<string name="main_support_description">Вам подобається цей додаток? Підтримайте його розвиток, увімкнувши неінвазивну рекламу, яку ви можете відключити в будь-який час</string> <string name="main_support_description">Вам подобається цей додаток? Підтримайте його розвиток, увімкнувши неінвазивну рекламу, яку ви можете відключити в будь-який час</string>
<string name="main_support_positive">Увімкнути рекламу</string> <string name="main_support_positive">Увімкнути рекламу</string>
@ -336,8 +336,10 @@
<string name="message_forward">Переслати</string> <string name="message_forward">Переслати</string>
<string name="message_select_all">Вибрати всі</string> <string name="message_select_all">Вибрати всі</string>
<string name="message_unselect_all">Відмінити вибір</string> <string name="message_unselect_all">Відмінити вибір</string>
<string name="message_restore_from_trash">Відновити зі смітника</string>
<string name="message_move_to_trash">Перемістити до кошика</string> <string name="message_move_to_trash">Перемістити до кошика</string>
<string name="message_delete_forever">Видалити назавжди</string> <string name="message_delete_forever">Видалити назавжди</string>
<string name="message_restore_success">Повідомлення успішно відновлено</string>
<string name="message_delete_success">Лист було успішно видалено</string> <string name="message_delete_success">Лист було успішно видалено</string>
<string name="message_mailbox_type_student">учень</string> <string name="message_mailbox_type_student">учень</string>
<string name="message_mailbox_type_parent">родич</string> <string name="message_mailbox_type_parent">родич</string>
@ -383,6 +385,7 @@
<item quantity="other">%1$d вибрано</item> <item quantity="other">%1$d вибрано</item>
</plurals> </plurals>
<string name="message_messages_deleted">Листи видалено</string> <string name="message_messages_deleted">Листи видалено</string>
<string name="message_messages_restored">Повідомлення відновлені</string>
<string name="message_mailbox_chooser_title">Вибрати поштову скриньку</string> <string name="message_mailbox_chooser_title">Вибрати поштову скриньку</string>
<string name="message_incognito_mode_on">Режим анонімності включено</string> <string name="message_incognito_mode_on">Режим анонімності включено</string>
<string name="message_incognito_description">Завдяки режиму анонімності, відправник не буде сповіщений коли ви прочитаєте повідомлення</string> <string name="message_incognito_description">Завдяки режиму анонімності, відправник не буде сповіщений коли ви прочитаєте повідомлення</string>
@ -849,13 +852,16 @@
<string name="auth_description">Для роботи програми нам потрібно підтвердити вашу особу. Будь ласка, введіть число PESEL &lt;b&gt;%1$s&lt;/b&gt; студента в поле нижче</string> <string name="auth_description">Для роботи програми нам потрібно підтвердити вашу особу. Будь ласка, введіть число PESEL &lt;b&gt;%1$s&lt;/b&gt; студента в поле нижче</string>
<string name="auth_button_skip">Поки що пропустити</string> <string name="auth_button_skip">Поки що пропустити</string>
<!--Captcha--> <!--Captcha-->
<string name="captcha_dialog_title">Верифікація в процесі. Чекайте…</string> <string name="captcha_dialog_title">Веб-сайт VULCAN потребує підтвердження</string>
<string name="captcha_dialog_description"><b>Чому я це бачу?</b>\nСайт реєстру, з якого Wulkanowy завантажує дані, відображає той самий екран, що й вище, тому Wulkanowy також повинен показувати його, щоб мати змогу завантажувати дані з цього сайту. Це неможливо обійти</string>
<string name="captcha_verified_message">Верифікація завершена</string> <string name="captcha_verified_message">Верифікація завершена</string>
<!--Errors--> <!--Errors-->
<string name="error_no_internet">Немає з\'єднання з інтернетом</string> <string name="error_no_internet">Немає з\'єднання з інтернетом</string>
<string name="error_invalid_device_datetime">Сталася помилка. Перевірте годинник пристрою</string> <string name="error_invalid_device_datetime">Сталася помилка. Перевірте годинник пристрою</string>
<string name="error_account_inactive">Цей обліковий запис неактивний. Спробуйте увійти ще раз</string>
<string name="error_timeout">Помилка підключення до щоденнику. Сервери можуть бути перевантажені, спробуйте пізніше</string> <string name="error_timeout">Помилка підключення до щоденнику. Сервери можуть бути перевантажені, спробуйте пізніше</string>
<string name="error_login_failed">Помилка завантаження даних, спробуйте пізніше</string> <string name="error_login_failed">Помилка завантаження даних, спробуйте пізніше</string>
<string name="error_password_invalid">Термін дії вашого пароля минув або був змінений. Будь ласка увійдіть знову</string>
<string name="error_password_change_required">Необхідна зміна пароля щоденника</string> <string name="error_password_change_required">Необхідна зміна пароля щоденника</string>
<string name="error_service_unavailable">UONET+ проводить технічне осблуговування, спробуйте пізніше</string> <string name="error_service_unavailable">UONET+ проводить технічне осблуговування, спробуйте пізніше</string>
<string name="error_unknown_uonet">Невідома помилка щоденника UONET+, спробуйте пізніше</string> <string name="error_unknown_uonet">Невідома помилка щоденника UONET+, спробуйте пізніше</string>
@ -865,4 +871,9 @@
<string name="error_feature_disabled">Функція вимкнена вашою школою</string> <string name="error_feature_disabled">Функція вимкнена вашою школою</string>
<string name="error_feature_not_available">Функція недоступна в режимі Mobile API. Увійдіть в інший режим</string> <string name="error_feature_not_available">Функція недоступна в режимі Mobile API. Увійдіть в інший режим</string>
<string name="error_field_required">Це поле обовʼязкове</string> <string name="error_field_required">Це поле обовʼязкове</string>
<!-- Mute system -->
<string name="message_mute">Вимкнути сповіщення</string>
<string name="message_unmute">Ввімкнути сповіщення</string>
<string name="message_mute_success">Ви ігноруєте цього користувача</string>
<string name="message_unmute_success">Ви не ігноруєте цього користувача</string>
</resources> </resources>

View File

@ -66,7 +66,7 @@
<item>gminaulanmajorat</item> <item>gminaulanmajorat</item>
<item>gminaozorkow</item> <item>gminaozorkow</item>
<item>gminalopiennikgorny</item> <item>gminalopiennikgorny</item>
<item>warszawa</item> <item>saas1</item>
<item>powiatwulkanowy</item> <item>powiatwulkanowy</item>
</string-array> </string-array>
</resources> </resources>

File diff suppressed because it is too large Load Diff

View File

@ -109,8 +109,8 @@
<string name="main_log_in">Log in</string> <string name="main_log_in">Log in</string>
<string name="main_session_expired">Session expired</string> <string name="main_session_expired">Session expired</string>
<string name="main_session_relogin">Session expired, log in again</string> <string name="main_session_relogin">Session expired, log in again</string>
<string name="main_expired_credentials_description">Your account password has been changed. You need to log in to Wulkanowy again</string> <string name="main_expired_credentials_title">Password has expired or been changed</string>
<string name="main_expired_credentials_title">Password changed</string> <string name="main_expired_credentials_description">Your account password has expired or been changed. You will need to log in to Wulkanowy again</string>
<string name="main_support_title">Application support</string> <string name="main_support_title">Application support</string>
<string name="main_support_description">Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time</string> <string name="main_support_description">Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time</string>
<string name="main_support_positive">Enable ads</string> <string name="main_support_positive">Enable ads</string>
@ -325,8 +325,10 @@
<string name="message_forward">Forward</string> <string name="message_forward">Forward</string>
<string name="message_select_all">Select all</string> <string name="message_select_all">Select all</string>
<string name="message_unselect_all">Unselect all</string> <string name="message_unselect_all">Unselect all</string>
<string name="message_restore_from_trash">Restore from trash</string>
<string name="message_move_to_trash">Move to trash</string> <string name="message_move_to_trash">Move to trash</string>
<string name="message_delete_forever">Delete permanently</string> <string name="message_delete_forever">Delete permanently</string>
<string name="message_restore_success">Message restored successfully</string>
<string name="message_delete_success">Message deleted successfully</string> <string name="message_delete_success">Message deleted successfully</string>
<string name="message_mailbox_type_student">student</string> <string name="message_mailbox_type_student">student</string>
<string name="message_mailbox_type_parent">parent</string> <string name="message_mailbox_type_parent">parent</string>
@ -364,6 +366,7 @@
<item quantity="other">%1$d selected</item> <item quantity="other">%1$d selected</item>
</plurals> </plurals>
<string name="message_messages_deleted">Messages deleted</string> <string name="message_messages_deleted">Messages deleted</string>
<string name="message_messages_restored">Messages restored</string>
<string name="message_mailbox_chooser_title">Choose mailbox</string> <string name="message_mailbox_chooser_title">Choose mailbox</string>
<string name="message_incognito_mode_on">Incognito mode is on</string> <string name="message_incognito_mode_on">Incognito mode is on</string>
<string name="message_incognito_description">Thanks to incognito mode sender is not notified when you read the message</string> <string name="message_incognito_description">Thanks to incognito mode sender is not notified when you read the message</string>
@ -845,15 +848,18 @@
<!--Captcha--> <!--Captcha-->
<string name="captcha_dialog_title">Verification is in progress. Wait…</string> <string name="captcha_dialog_title">VULCAN\'s website requires verification</string>
<string name="captcha_dialog_description"><b>Why am I seeing this?</b>\nThe register website from which Wulkanowy downloads data displays the same screen as above, so Wulkanowy must also show it to be able to download data from this website. There\'s no way around it</string>
<string name="captcha_verified_message">Verified successfully</string> <string name="captcha_verified_message">Verified successfully</string>
<!--Errors--> <!--Errors-->
<string name="error_no_internet">No internet connection</string> <string name="error_no_internet">No internet connection</string>
<string name="error_invalid_device_datetime">An error occurred. Check your device clock</string> <string name="error_invalid_device_datetime">An error occurred. Check your device clock</string>
<string name="error_account_inactive">This account is inactive. Try logging in again</string>
<string name="error_timeout">Connection to register failed. Servers can be overloaded. Please try again later</string> <string name="error_timeout">Connection to register failed. Servers can be overloaded. Please try again later</string>
<string name="error_login_failed">Loading data failed. Please try again later</string> <string name="error_login_failed">Loading data failed. Please try again later</string>
<string name="error_password_invalid">Your password has expired or been changed. Please log in again</string>
<string name="error_password_change_required">Register password change required</string> <string name="error_password_change_required">Register password change required</string>
<string name="error_service_unavailable">Maintenance underway UONET + register. Try again later</string> <string name="error_service_unavailable">Maintenance underway UONET + register. Try again later</string>
<string name="error_unknown_uonet">Unknown UONET + register error. Try again later</string> <string name="error_unknown_uonet">Unknown UONET + register error. Try again later</string>
@ -863,4 +869,10 @@
<string name="error_feature_disabled">Feature disabled by your school</string> <string name="error_feature_disabled">Feature disabled by your school</string>
<string name="error_feature_not_available">Feature not available. Login in a mode other than Mobile API</string> <string name="error_feature_not_available">Feature not available. Login in a mode other than Mobile API</string>
<string name="error_field_required">This field is required</string> <string name="error_field_required">This field is required</string>
<!-- Mute system -->
<string name="message_mute">Mute</string>
<string name="message_unmute">Unmute</string>
<string name="message_mute_success">You have muted this user</string>
<string name="message_unmute_success">You have unmuted this user</string>
</resources> </resources>

View File

@ -10,11 +10,17 @@ import io.github.wulkanowy.getSemesterEntity
import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.getStudentEntity
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.AutoRefreshHelper
import io.mockk.* import io.mockk.MockKAnnotations
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.MockK
import io.mockk.impl.annotations.SpyK import io.mockk.impl.annotations.SpyK
import io.mockk.just
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@ -61,26 +67,36 @@ class AttendanceRepositoryTest {
} }
@Test @Test
fun `force refresh without difference`() { fun `force refresh without difference`() = runTest {
// prepare // prepare
coEvery { sdk.getAttendance(startDate, endDate) } returns remoteList coEvery { sdk.getAttendance(startDate, endDate) } returns remoteList
coEvery { attendanceDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf( coEvery { attendanceDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf(
flowOf(remoteList.mapToEntities(semester, emptyList())), flowOf(remoteList.mapToEntities(semester, emptyList())),
flowOf(remoteList.mapToEntities(semester, emptyList())) flowOf(remoteList.mapToEntities(semester, emptyList()))
) )
coEvery { attendanceDb.insertAll(any()) } returns listOf(1, 2, 3) coEvery { attendanceDb.removeOldAndSaveNew(any(), any()) } just Runs
coEvery { attendanceDb.deleteAll(any()) } just Runs
// execute // execute
val res = runBlocking { attendanceRepository.getAttendance(student, semester, startDate, endDate, true).toFirstResult() } val res = attendanceRepository.getAttendance(
student = student,
semester = semester,
start = startDate,
end = endDate,
forceRefresh = true,
).toFirstResult()
// verify // verify
assertEquals(null, res.errorOrNull) assertEquals(null, res.errorOrNull)
assertEquals(2, res.dataOrNull?.size) assertEquals(2, res.dataOrNull?.size)
coVerify { sdk.getAttendance(startDate, endDate) } coVerify { sdk.getAttendance(startDate, endDate) }
coVerify { attendanceDb.loadAll(1, 1, startDate, endDate) } coVerify { attendanceDb.loadAll(1, 1, startDate, endDate) }
coVerify { attendanceDb.insertAll(match { it.isEmpty() }) } coVerify {
coVerify { attendanceDb.deleteAll(match { it.isEmpty() }) } attendanceDb.removeOldAndSaveNew(
oldItems = match { it.isEmpty() },
newItems = match { it.isEmpty() },
)
}
} }
@Test @Test
@ -89,14 +105,23 @@ class AttendanceRepositoryTest {
coEvery { sdk.getAttendance(startDate, endDate) } returns remoteList coEvery { sdk.getAttendance(startDate, endDate) } returns remoteList
coEvery { attendanceDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf( coEvery { attendanceDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf(
flowOf(remoteList.dropLast(1).mapToEntities(semester, emptyList())), flowOf(remoteList.dropLast(1).mapToEntities(semester, emptyList())),
flowOf(remoteList.dropLast(1).mapToEntities(semester, emptyList())), // after fetch end before save result flowOf(
remoteList.dropLast(1).mapToEntities(semester, emptyList())
), // after fetch end before save result
flowOf(remoteList.mapToEntities(semester, emptyList())) flowOf(remoteList.mapToEntities(semester, emptyList()))
) )
coEvery { attendanceDb.insertAll(any()) } returns listOf(1, 2, 3) coEvery { attendanceDb.removeOldAndSaveNew(any(), any()) } just Runs
coEvery { attendanceDb.deleteAll(any()) } just Runs
// execute // execute
val res = runBlocking { attendanceRepository.getAttendance(student, semester, startDate, endDate, true).toFirstResult() } val res = runBlocking {
attendanceRepository.getAttendance(
student,
semester,
startDate,
endDate,
true
).toFirstResult()
}
// verify // verify
assertEquals(null, res.errorOrNull) assertEquals(null, res.errorOrNull)
@ -104,11 +129,13 @@ class AttendanceRepositoryTest {
coVerify { sdk.getAttendance(startDate, endDate) } coVerify { sdk.getAttendance(startDate, endDate) }
coVerify { attendanceDb.loadAll(1, 1, startDate, endDate) } coVerify { attendanceDb.loadAll(1, 1, startDate, endDate) }
coVerify { coVerify {
attendanceDb.insertAll(match { attendanceDb.removeOldAndSaveNew(
it.size == 1 && it[0] == remoteList.mapToEntities(semester, emptyList())[1] oldItems = match { it.isEmpty() },
}) newItems = match {
it.size == 1 && it[0] == remoteList.mapToEntities(semester, emptyList())[1]
},
)
} }
coVerify { attendanceDb.deleteAll(match { it.isEmpty() }) }
} }
@Test @Test
@ -117,25 +144,39 @@ class AttendanceRepositoryTest {
coEvery { sdk.getAttendance(startDate, endDate) } returns remoteList.dropLast(1) coEvery { sdk.getAttendance(startDate, endDate) } returns remoteList.dropLast(1)
coEvery { attendanceDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf( coEvery { attendanceDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf(
flowOf(remoteList.mapToEntities(semester, emptyList())), flowOf(remoteList.mapToEntities(semester, emptyList())),
flowOf(remoteList.mapToEntities(semester, emptyList())), // after fetch end before save result flowOf(
remoteList.mapToEntities(
semester,
emptyList()
)
), // after fetch end before save result
flowOf(remoteList.dropLast(1).mapToEntities(semester, emptyList())) flowOf(remoteList.dropLast(1).mapToEntities(semester, emptyList()))
) )
coEvery { attendanceDb.insertAll(any()) } returns listOf(1, 2, 3) coEvery { attendanceDb.removeOldAndSaveNew(any(), any()) } just Runs
coEvery { attendanceDb.deleteAll(any()) } just Runs
// execute // execute
val res = runBlocking { attendanceRepository.getAttendance(student, semester, startDate, endDate, true).toFirstResult() } val res = runBlocking {
attendanceRepository.getAttendance(
student,
semester,
startDate,
endDate,
true
).toFirstResult()
}
// verify // verify
assertEquals(null, res.errorOrNull) assertEquals(null, res.errorOrNull)
assertEquals(1, res.dataOrNull?.size) assertEquals(1, res.dataOrNull?.size)
coVerify { sdk.getAttendance(startDate, endDate) } coVerify { sdk.getAttendance(startDate, endDate) }
coVerify { attendanceDb.loadAll(1, 1, startDate, endDate) } coVerify { attendanceDb.loadAll(1, 1, startDate, endDate) }
coVerify { attendanceDb.insertAll(match { it.isEmpty() }) }
coVerify { coVerify {
attendanceDb.deleteAll(match { attendanceDb.removeOldAndSaveNew(
it.size == 1 && it[0] == remoteList.mapToEntities(semester, emptyList())[1] oldItems = match {
}) it.size == 1 && it[0] == remoteList.mapToEntities(semester, emptyList())[1]
},
newItems = emptyList(),
)
} }
} }

View File

@ -9,11 +9,16 @@ import io.github.wulkanowy.getSemesterEntity
import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.getStudentEntity
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.AutoRefreshHelper
import io.mockk.* import io.mockk.MockKAnnotations
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.MockK
import io.mockk.impl.annotations.SpyK import io.mockk.impl.annotations.SpyK
import io.mockk.just
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@ -52,46 +57,28 @@ class CompletedLessonsRepositoryTest {
MockKAnnotations.init(this) MockKAnnotations.init(this)
every { refreshHelper.shouldBeRefreshed(any()) } returns false every { refreshHelper.shouldBeRefreshed(any()) } returns false
completedLessonRepository = CompletedLessonsRepository(completedLessonDb, sdk, refreshHelper) completedLessonRepository =
CompletedLessonsRepository(completedLessonDb, sdk, refreshHelper)
} }
@Test @Test
fun `force refresh without difference`() { fun `force refresh without difference`() = runTest {
// prepare // prepare
coEvery { sdk.getCompletedLessons(startDate, endDate) } returns remoteList coEvery { sdk.getCompletedLessons(startDate, endDate) } returns remoteList
coEvery { completedLessonDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf( coEvery { completedLessonDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf(
flowOf(remoteList.mapToEntities(semester)), flowOf(remoteList.mapToEntities(semester)),
flowOf(remoteList.mapToEntities(semester)) flowOf(remoteList.mapToEntities(semester))
) )
coEvery { completedLessonDb.insertAll(any()) } returns listOf(1, 2, 3) coEvery { completedLessonDb.removeOldAndSaveNew(any(), any()) } just Runs
coEvery { completedLessonDb.deleteAll(any()) } just Runs
// execute // execute
val res = runBlocking { completedLessonRepository.getCompletedLessons(student, semester, startDate, endDate, true).toFirstResult() } val res = completedLessonRepository.getCompletedLessons(
student = student,
// verify semester = semester,
assertEquals(null, res.errorOrNull) start = startDate,
assertEquals(2, res.dataOrNull?.size) end = endDate,
coVerify { sdk.getCompletedLessons(startDate, endDate) } forceRefresh = true,
coVerify { completedLessonDb.loadAll(1, 1, startDate, endDate) } ).toFirstResult()
coVerify { completedLessonDb.insertAll(match { it.isEmpty() }) }
coVerify { completedLessonDb.deleteAll(match { it.isEmpty() }) }
}
@Test
fun `force refresh with more items in remote`() {
// prepare
coEvery { sdk.getCompletedLessons(startDate, endDate) } returns remoteList
coEvery { completedLessonDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf(
flowOf(remoteList.dropLast(1).mapToEntities(semester)),
flowOf(remoteList.dropLast(1).mapToEntities(semester)), // after fetch end before save result
flowOf(remoteList.mapToEntities(semester))
)
coEvery { completedLessonDb.insertAll(any()) } returns listOf(1, 2, 3)
coEvery { completedLessonDb.deleteAll(any()) } just Runs
// execute
val res = runBlocking { completedLessonRepository.getCompletedLessons(student, semester, startDate, endDate, true).toFirstResult() }
// verify // verify
assertEquals(null, res.errorOrNull) assertEquals(null, res.errorOrNull)
@ -99,15 +86,52 @@ class CompletedLessonsRepositoryTest {
coVerify { sdk.getCompletedLessons(startDate, endDate) } coVerify { sdk.getCompletedLessons(startDate, endDate) }
coVerify { completedLessonDb.loadAll(1, 1, startDate, endDate) } coVerify { completedLessonDb.loadAll(1, 1, startDate, endDate) }
coVerify { coVerify {
completedLessonDb.insertAll(match { completedLessonDb.removeOldAndSaveNew(
it.size == 1 && it[0] == remoteList.mapToEntities(semester)[1] oldItems = match { it.isEmpty() },
}) newItems = match { it.isEmpty() },
)
} }
coVerify { completedLessonDb.deleteAll(match { it.isEmpty() }) }
} }
@Test @Test
fun `force refresh with more items in local`() { fun `force refresh with more items in remote`() = runTest {
// prepare
coEvery { sdk.getCompletedLessons(startDate, endDate) } returns remoteList
coEvery { completedLessonDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf(
flowOf(remoteList.dropLast(1).mapToEntities(semester)),
flowOf(
remoteList.dropLast(1).mapToEntities(semester)
), // after fetch end before save result
flowOf(remoteList.mapToEntities(semester))
)
coEvery { completedLessonDb.removeOldAndSaveNew(any(), any()) } just Runs
// execute
val res = completedLessonRepository.getCompletedLessons(
student = student,
semester = semester,
start = startDate,
end = endDate,
forceRefresh = true
).toFirstResult()
// verify
assertEquals(null, res.errorOrNull)
assertEquals(2, res.dataOrNull?.size)
coVerify { sdk.getCompletedLessons(startDate, endDate) }
coVerify { completedLessonDb.loadAll(1, 1, startDate, endDate) }
coVerify {
completedLessonDb.removeOldAndSaveNew(
oldItems = match { it.isEmpty() },
newItems = match {
it.size == 1 && it[0] == remoteList.mapToEntities(semester)[1]
}
)
}
}
@Test
fun `force refresh with more items in local`() = runTest {
// prepare // prepare
coEvery { sdk.getCompletedLessons(startDate, endDate) } returns remoteList.dropLast(1) coEvery { sdk.getCompletedLessons(startDate, endDate) } returns remoteList.dropLast(1)
coEvery { completedLessonDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf( coEvery { completedLessonDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf(
@ -115,22 +139,29 @@ class CompletedLessonsRepositoryTest {
flowOf(remoteList.mapToEntities(semester)), // after fetch end before save result flowOf(remoteList.mapToEntities(semester)), // after fetch end before save result
flowOf(remoteList.dropLast(1).mapToEntities(semester)) flowOf(remoteList.dropLast(1).mapToEntities(semester))
) )
coEvery { completedLessonDb.insertAll(any()) } returns listOf(1, 2, 3) coEvery { completedLessonDb.removeOldAndSaveNew(any(), any()) } just Runs
coEvery { completedLessonDb.deleteAll(any()) } just Runs
// execute // execute
val res = runBlocking { completedLessonRepository.getCompletedLessons(student, semester, startDate, endDate, true).toFirstResult() } val res = completedLessonRepository.getCompletedLessons(
student = student,
semester = semester,
start = startDate,
end = endDate,
forceRefresh = true,
).toFirstResult()
// verify // verify
assertEquals(null, res.errorOrNull) assertEquals(null, res.errorOrNull)
assertEquals(1, res.dataOrNull?.size) assertEquals(1, res.dataOrNull?.size)
coVerify { sdk.getCompletedLessons(startDate, endDate) } coVerify { sdk.getCompletedLessons(startDate, endDate) }
coVerify { completedLessonDb.loadAll(1, 1, startDate, endDate) } coVerify { completedLessonDb.loadAll(1, 1, startDate, endDate) }
coVerify { completedLessonDb.insertAll(match { it.isEmpty() }) }
coVerify { coVerify {
completedLessonDb.deleteAll(match { completedLessonDb.removeOldAndSaveNew(
it.size == 1 && it[0] == remoteList.mapToEntities(semester)[1] oldItems = match {
}) it.size == 1 && it[0] == remoteList.mapToEntities(semester)[1]
},
newItems = match { it.isEmpty() },
)
} }
} }

View File

@ -9,11 +9,17 @@ import io.github.wulkanowy.getSemesterEntity
import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.getStudentEntity
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.AutoRefreshHelper
import io.mockk.* import io.mockk.MockKAnnotations
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.MockK
import io.mockk.impl.annotations.SpyK import io.mockk.impl.annotations.SpyK
import io.mockk.just
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@ -64,35 +70,42 @@ class ExamRemoteTest {
flowOf(remoteList.mapToEntities(semester)), flowOf(remoteList.mapToEntities(semester)),
flowOf(remoteList.mapToEntities(semester)) flowOf(remoteList.mapToEntities(semester))
) )
coEvery { examDb.insertAll(any()) } returns listOf(1, 2, 3) coEvery { examDb.removeOldAndSaveNew(any(), any()) } just Runs
coEvery { examDb.deleteAll(any()) } just Runs
// execute // execute
val res = runBlocking { examRepository.getExams(student, semester, startDate, endDate, true).toFirstResult() } val res = runBlocking {
examRepository.getExams(student, semester, startDate, endDate, true).toFirstResult()
}
// verify // verify
assertEquals(null, res.errorOrNull) assertEquals(null, res.errorOrNull)
assertEquals(2, res.dataOrNull?.size) assertEquals(2, res.dataOrNull?.size)
coVerify { sdk.getExams(startDate, realEndDate) } coVerify { sdk.getExams(startDate, realEndDate) }
coVerify { examDb.loadAll(1, 1, startDate, realEndDate) } coVerify { examDb.loadAll(1, 1, startDate, realEndDate) }
coVerify { examDb.insertAll(match { it.isEmpty() }) } coVerify { examDb.removeOldAndSaveNew(emptyList(), emptyList()) }
coVerify { examDb.deleteAll(match { it.isEmpty() }) }
} }
@Test @Test
fun `force refresh with more items in remote`() { fun `force refresh with more items in remote`() = runTest {
// prepare // prepare
coEvery { sdk.getExams(startDate, realEndDate) } returns remoteList coEvery { sdk.getExams(startDate, realEndDate) } returns remoteList
coEvery { examDb.loadAll(1, 1, startDate, realEndDate) } returnsMany listOf( coEvery { examDb.loadAll(1, 1, startDate, realEndDate) } returnsMany listOf(
flowOf(remoteList.dropLast(1).mapToEntities(semester)), flowOf(remoteList.dropLast(1).mapToEntities(semester)),
flowOf(remoteList.dropLast(1).mapToEntities(semester)), // after fetch end before save result flowOf(
remoteList.dropLast(1).mapToEntities(semester)
), // after fetch end before save result
flowOf(remoteList.mapToEntities(semester)) flowOf(remoteList.mapToEntities(semester))
) )
coEvery { examDb.insertAll(any()) } returns listOf(1, 2, 3) coEvery { examDb.removeOldAndSaveNew(any(), any()) } just Runs
coEvery { examDb.deleteAll(any()) } just Runs
// execute // execute
val res = runBlocking { examRepository.getExams(student, semester, startDate, endDate, true).toFirstResult() } val res = examRepository.getExams(
student = student,
semester = semester,
start = startDate,
end = endDate,
forceRefresh = true,
).toFirstResult()
// verify // verify
assertEquals(null, res.errorOrNull) assertEquals(null, res.errorOrNull)
@ -100,15 +113,17 @@ class ExamRemoteTest {
coVerify { sdk.getExams(startDate, realEndDate) } coVerify { sdk.getExams(startDate, realEndDate) }
coVerify { examDb.loadAll(1, 1, startDate, realEndDate) } coVerify { examDb.loadAll(1, 1, startDate, realEndDate) }
coVerify { coVerify {
examDb.insertAll(match { examDb.removeOldAndSaveNew(
it.size == 1 && it[0] == remoteList.mapToEntities(semester)[1] oldItems = emptyList(),
}) newItems = match {
it.size == 1 && it[0] == remoteList.mapToEntities(semester)[1]
},
)
} }
coVerify { examDb.deleteAll(match { it.isEmpty() }) }
} }
@Test @Test
fun `force refresh with more items in local`() { fun `force refresh with more items in local`() = runTest {
// prepare // prepare
coEvery { sdk.getExams(startDate, realEndDate) } returns remoteList.dropLast(1) coEvery { sdk.getExams(startDate, realEndDate) } returns remoteList.dropLast(1)
coEvery { examDb.loadAll(1, 1, startDate, realEndDate) } returnsMany listOf( coEvery { examDb.loadAll(1, 1, startDate, realEndDate) } returnsMany listOf(
@ -116,22 +131,27 @@ class ExamRemoteTest {
flowOf(remoteList.mapToEntities(semester)), // after fetch end before save result flowOf(remoteList.mapToEntities(semester)), // after fetch end before save result
flowOf(remoteList.dropLast(1).mapToEntities(semester)) flowOf(remoteList.dropLast(1).mapToEntities(semester))
) )
coEvery { examDb.insertAll(any()) } returns listOf(1, 2, 3) coEvery { examDb.removeOldAndSaveNew(any(), any()) } just Runs
coEvery { examDb.deleteAll(any()) } just Runs
// execute // execute
val res = runBlocking { examRepository.getExams(student, semester, startDate, endDate, true).toFirstResult() } val res = examRepository.getExams(
student = student,
semester = semester,
start = startDate,
end = endDate,
forceRefresh = true,
).toFirstResult()
// verify // verify
assertEquals(null, res.errorOrNull) assertEquals(null, res.errorOrNull)
assertEquals(1, res.dataOrNull?.size) assertEquals(1, res.dataOrNull?.size)
coVerify { sdk.getExams(startDate, realEndDate) } coVerify { sdk.getExams(startDate, realEndDate) }
coVerify { examDb.loadAll(1, 1, startDate, realEndDate) } coVerify { examDb.loadAll(1, 1, startDate, realEndDate) }
coVerify { examDb.insertAll(match { it.isEmpty() }) }
coVerify { coVerify {
examDb.deleteAll(match { examDb.removeOldAndSaveNew(
it.size == 1 && it[0] == remoteList.mapToEntities(semester)[1] oldItems = match { it.size == 1 && it[0] == remoteList.mapToEntities(semester)[1] },
}) newItems = emptyList()
)
} }
} }

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