From c04752ed39e847009bc4ab1997a4aef43a545ca0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Fri, 1 Mar 2024 10:32:55 +0100 Subject: [PATCH 01/43] Fix timetable items layout (#2446) --- app/src/main/res/layout/item_timetable.xml | 11 +++++++---- .../main/res/layout/subitem_dashboard_small_grade.xml | 1 + 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/layout/item_timetable.xml b/app/src/main/res/layout/item_timetable.xml index b9966c12..d1310522 100644 --- a/app/src/main/res/layout/item_timetable.xml +++ b/app/src/main/res/layout/item_timetable.xml @@ -24,6 +24,7 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0" tools:text="5" /> @@ -179,7 +182,7 @@ android:visibility="gone" app:backgroundTint="?colorPrimary" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="parent" + app:layout_constraintTop_toTopOf="@id/timetableItemTimeStart" tools:text="jeszcze 15 min" tools:visibility="visible" /> diff --git a/app/src/main/res/layout/subitem_dashboard_small_grade.xml b/app/src/main/res/layout/subitem_dashboard_small_grade.xml index 3684c267..5d48313a 100644 --- a/app/src/main/res/layout/subitem_dashboard_small_grade.xml +++ b/app/src/main/res/layout/subitem_dashboard_small_grade.xml @@ -11,6 +11,7 @@ android:gravity="center" android:maxLength="5" android:minWidth="20dp" + android:padding="1dp" android:textColor="@android:color/white" android:textSize="12sp" android:textStyle="bold" From ea28fc783cf2c24e25606f5ba13dff120d1101ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Fri, 1 Mar 2024 21:14:43 +0100 Subject: [PATCH 02/43] Add message from trash restoring (#2438) --- .../wulkanowy/data/enums/MessageFolder.kt | 7 ++- .../data/repositories/MessageRepository.kt | 50 ++++++++++----- .../message/preview/MessagePreviewFragment.kt | 27 ++++---- .../preview/MessagePreviewPresenter.kt | 61 +++++++++++++++---- .../message/preview/MessagePreviewView.kt | 8 +-- .../message/send/SendMessagePresenter.kt | 2 +- .../modules/message/tab/MessageTabFragment.kt | 16 +++-- .../message/tab/MessageTabPresenter.kt | 23 ++++++- .../ic_menu_message_delete_forever.xml | 9 +++ .../res/drawable/ic_menu_message_restore.xml | 9 +++ .../res/menu/action_menu_message_preview.xml | 14 +++++ .../res/menu/context_menu_message_tab.xml | 14 +++++ app/src/main/res/values/strings.xml | 3 + 13 files changed, 192 insertions(+), 51 deletions(-) create mode 100644 app/src/main/res/drawable/ic_menu_message_delete_forever.xml create mode 100644 app/src/main/res/drawable/ic_menu_message_restore.xml diff --git a/app/src/main/java/io/github/wulkanowy/data/enums/MessageFolder.kt b/app/src/main/java/io/github/wulkanowy/data/enums/MessageFolder.kt index 899ba908..7cb4202a 100644 --- a/app/src/main/java/io/github/wulkanowy/data/enums/MessageFolder.kt +++ b/app/src/main/java/io/github/wulkanowy/data/enums/MessageFolder.kt @@ -3,5 +3,10 @@ package io.github.wulkanowy.data.enums enum class MessageFolder(val id: Int = 1) { RECEIVED(1), SENT(2), - TRASHED(3) + TRASHED(3), + ; + + companion object { + fun byId(id: Int) = entries.first { it.id == id } + } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt index 6d591c5b..96f04870 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt @@ -18,6 +18,7 @@ import io.github.wulkanowy.data.db.entities.Recipient import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.enums.MessageFolder 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.mappers.mapFromEntities import io.github.wulkanowy.data.mappers.mapToEntities @@ -25,6 +26,7 @@ import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.data.onResourceError import io.github.wulkanowy.data.onResourceSuccess import io.github.wulkanowy.data.pojos.MessageDraft +import io.github.wulkanowy.data.toFirstResult import io.github.wulkanowy.data.waitForResult import io.github.wulkanowy.domain.messages.GetMailboxByStudentUseCase import io.github.wulkanowy.sdk.Sdk @@ -34,7 +36,6 @@ import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first import kotlinx.coroutines.sync.Mutex import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -155,17 +156,30 @@ class MessageRepository @Inject constructor( subject: String, content: String, recipients: List, - mailboxId: String, + mailbox: Mailbox, ) { sdk.init(student).sendMessage( subject = subject, content = content, recipients = recipients.mapFromEntities(), - mailboxId = mailboxId, + mailboxId = mailbox.globalKey, ) + refreshFolders(student, mailbox, listOf(SENT)) } - suspend fun deleteMessages(student: Student, mailbox: Mailbox?, messages: List) { + suspend fun restoreMessages(student: Student, mailbox: Mailbox?, messages: List) { + 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) { val firstMessage = messages.first() sdk.init(student).deleteMessages( messages = messages.map { it.messageGlobalKey }, @@ -184,18 +198,24 @@ class MessageRepository @Inject constructor( } messagesDb.updateAll(deletedMessages) - } else messagesDb.deleteAll(messages) - - getMessages( - student = student, - mailbox = mailbox, - folder = TRASHED, - forceRefresh = true, - ).first() + } else { + messagesDb.deleteAll(messages) + } } - suspend fun deleteMessage(student: Student, mailbox: Mailbox?, message: Message) { - deleteMessages(student, mailbox, listOf(message)) + private suspend fun refreshFolders( + student: Student, + mailbox: Mailbox?, + folders: List = MessageFolder.entries + ) { + folders.forEach { + getMessages( + student = student, + mailbox = mailbox, + folder = it, + forceRefresh = true, + ).toFirstResult() + } } suspend fun getMailboxes(student: Student, forceRefresh: Boolean) = networkBoundResource( @@ -240,7 +260,7 @@ class MessageRepository @Inject constructor( value?.let { json.encodeToString(it) } ) - suspend fun isMuted(author: String): Boolean { + private suspend fun isMuted(author: String): Boolean { return mutedMessageSendersDao.checkMute(author) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt index 3b33bb51..75778bac 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt @@ -44,8 +44,12 @@ class MessagePreviewFragment : private var menuForwardButton: MenuItem? = null + private var menuRestoreButton: MenuItem? = null + private var menuDeleteButton: MenuItem? = null + private var menuDeleteForeverButton: MenuItem? = null + private var menuShareButton: MenuItem? = null private var menuPrintButton: MenuItem? = null @@ -64,6 +68,9 @@ class MessagePreviewFragment : 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 get() = getString(R.string.message_no_subject) @@ -111,7 +118,9 @@ class MessagePreviewFragment : inflater.inflate(R.menu.action_menu_message_preview, menu) menuReplyButton = menu.findItem(R.id.messagePreviewMenuReply) menuForwardButton = menu.findItem(R.id.messagePreviewMenuForward) + menuRestoreButton = menu.findItem(R.id.messagePreviewMenuRestore) menuDeleteButton = menu.findItem(R.id.messagePreviewMenuDelete) + menuDeleteForeverButton = menu.findItem(R.id.messagePreviewMenuDeleteForever) menuShareButton = menu.findItem(R.id.messagePreviewMenuShare) menuPrintButton = menu.findItem(R.id.messagePreviewMenuPrint) menuMuteButton = menu.findItem(R.id.messagePreviewMenuMute) @@ -124,7 +133,9 @@ class MessagePreviewFragment : return when (item.itemId) { R.id.messagePreviewMenuReply -> presenter.onReply() R.id.messagePreviewMenuForward -> presenter.onForward() + R.id.messagePreviewMenuRestore -> presenter.onMessageRestore() R.id.messagePreviewMenuDelete -> presenter.onMessageDelete() + R.id.messagePreviewMenuDeleteForever -> presenter.onMessageDelete() R.id.messagePreviewMenuShare -> presenter.onShare() R.id.messagePreviewMenuPrint -> presenter.onPrint() R.id.messagePreviewMenuMute -> presenter.onMute() @@ -152,23 +163,17 @@ class MessagePreviewFragment : binding.messagePreviewRecycler.visibility = if (show) VISIBLE else GONE } - override fun showOptions(show: Boolean, isReplayable: Boolean) { - menuReplyButton?.isVisible = isReplayable + override fun showOptions(show: Boolean, isReplayable: Boolean, isRestorable: Boolean) { + menuReplyButton?.isVisible = show && isReplayable menuForwardButton?.isVisible = show - menuDeleteButton?.isVisible = show + menuRestoreButton?.isVisible = show && isRestorable + menuDeleteButton?.isVisible = show && !isRestorable + menuDeleteForeverButton?.isVisible = show && isRestorable menuShareButton?.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) { binding.messagePreviewError.visibility = if (show) VISIBLE else GONE } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt index 2eff245f..9bb0d32a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt @@ -14,9 +14,11 @@ import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.toFormattedString +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds class MessagePreviewPresenter @Inject constructor( errorHandler: ErrorHandler, @@ -74,6 +76,7 @@ class MessagePreviewPresenter @Inject constructor( } } } else { + delay(1.seconds) view?.run { showMessage(messageNotExists) popView() @@ -172,13 +175,51 @@ class MessagePreviewPresenter @Inject constructor( return true } + private fun restoreMessage() { + val message = messageWithAttachments?.message ?: return + + view?.run { + showContent(false) + showProgress(true) + showOptions( + show = false, + isReplayable = false, + isRestorable = false, + ) + showErrorView(false) + } + Timber.i("Restore message ${message.messageGlobalKey}") + presenterScope.launch { + runCatching { + val student = studentRepository.getCurrentStudent(decryptPass = true) + val mailbox = messageRepository.getMailboxByStudent(student) + messageRepository.restoreMessages(student, mailbox, listOfNotNull(message)) + } + .onFailure { + retryCallback = { onMessageRestore() } + errorHandler.dispatch(it) + } + .onSuccess { + view?.run { + showMessage(restoreMessageSuccessString) + popView() + } + } + view?.showProgress(false) + } + } + private fun deleteMessage() { messageWithAttachments?.message ?: return view?.run { showContent(false) showProgress(true) - showOptions(show = false, isReplayable = false) + showOptions( + show = false, + isReplayable = false, + isRestorable = false, + ) showErrorView(false) } @@ -187,8 +228,7 @@ class MessagePreviewPresenter @Inject constructor( presenterScope.launch { runCatching { val student = studentRepository.getCurrentStudent(decryptPass = true) - val mailbox = messageRepository.getMailboxByStudent(student) - messageRepository.deleteMessage(student, mailbox, messageWithAttachments?.message!!) + messageRepository.deleteMessage(student, messageWithAttachments?.message!!) }.onFailure { retryCallback = { onMessageDelete() } errorHandler.dispatch(it) @@ -213,6 +253,11 @@ class MessagePreviewPresenter @Inject constructor( } } + fun onMessageRestore(): Boolean { + restoreMessage() + return true + } + fun onMessageDelete(): Boolean { deleteMessage() return true @@ -222,15 +267,9 @@ class MessagePreviewPresenter @Inject constructor( view?.apply { showOptions( show = messageWithAttachments?.message != null, - isReplayable = messageWithAttachments?.message?.folderId != MessageFolder.SENT.id, + isReplayable = messageWithAttachments?.message?.folderId == MessageFolder.RECEIVED.id, + isRestorable = messageWithAttachments?.message?.folderId == MessageFolder.TRASHED.id, ) - messageWithAttachments?.message?.let { - when (it.folderId == MessageFolder.TRASHED.id) { - true -> setDeletedOptionsLabels() - false -> setNotDeletedOptionsLabels() - } - } - } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt index cbe1c3cb..ee0b6ce0 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt @@ -13,6 +13,8 @@ interface MessagePreviewView : BaseView { val unmuteMessageSuccessString: String + val restoreMessageSuccessString: String + val messageNoSubjectString: String val printHTML: String @@ -35,11 +37,7 @@ interface MessagePreviewView : BaseView { fun setErrorRetryCallback(callback: () -> Unit) - fun showOptions(show: Boolean, isReplayable: Boolean) - - fun setDeletedOptionsLabels() - - fun setNotDeletedOptionsLabels() + fun showOptions(show: Boolean, isReplayable: Boolean, isRestorable: Boolean) fun openMessageReply(message: Message?) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt index e776e994..6155baea 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt @@ -203,7 +203,7 @@ class SendMessagePresenter @Inject constructor( subject = subject, content = content, recipients = recipients, - mailboxId = mailbox.globalKey, + mailbox = mailbox, ) }.logResourceStatus("sending message").onEach { when (it) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt index 4364e868..12f9d323 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt @@ -5,7 +5,9 @@ import android.view.Menu import android.view.MenuInflater import android.view.MenuItem 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 androidx.annotation.StringRes import androidx.appcompat.view.ActionMode @@ -64,10 +66,12 @@ class MessageTabFragment : BaseFragment(R.layout.frag } override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { - if (presenter.folder == MessageFolder.TRASHED) { - val menuItem = menu.findItem(R.id.messageTabContextMenuDelete) - menuItem.setTitle(R.string.message_delete_forever) - } + val isTrashFolder = presenter.folder == MessageFolder.TRASHED + + menu.findItem(R.id.messageTabContextMenuDelete).setVisible(!isTrashFolder) + menu.findItem(R.id.messageTabContextMenuDeleteForever).setVisible(isTrashFolder) + menu.findItem(R.id.messageTabContextMenuRestore).setVisible(isTrashFolder) + return presenter.onPrepareActionMode() } @@ -79,6 +83,8 @@ class MessageTabFragment : BaseFragment(R.layout.frag override fun onActionItemClicked(mode: ActionMode, menu: MenuItem): Boolean { when (menu.itemId) { R.id.messageTabContextMenuDelete -> presenter.onActionModeSelectDelete() + R.id.messageTabContextMenuRestore -> presenter.onActionModeSelectRestore() + R.id.messageTabContextMenuDeleteForever -> presenter.onActionModeSelectDelete() R.id.messageTabContextMenuSelectAll -> presenter.onActionModeSelectCheckAll() } return true diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt index f8283721..cda0b32b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt @@ -121,8 +121,27 @@ class MessageTabPresenter @Inject constructor( 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() { - Timber.i("Delete ${messagesToDelete.size} messages)") + Timber.i("Delete ${messagesToDelete.size} messages") val messageList = messagesToDelete.toList() presenterScope.launch { @@ -134,7 +153,7 @@ class MessageTabPresenter @Inject constructor( runCatching { val student = studentRepository.getCurrentStudent(true) - messageRepository.deleteMessages(student, selectedMailbox, messageList) + messageRepository.deleteMessages(student, messageList) } .onFailure(errorHandler::dispatch) .onSuccess { view?.showMessage(R.string.message_messages_deleted) } diff --git a/app/src/main/res/drawable/ic_menu_message_delete_forever.xml b/app/src/main/res/drawable/ic_menu_message_delete_forever.xml new file mode 100644 index 00000000..a7b5ac53 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_message_delete_forever.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_menu_message_restore.xml b/app/src/main/res/drawable/ic_menu_message_restore.xml new file mode 100644 index 00000000..5c8544f2 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_message_restore.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/menu/action_menu_message_preview.xml b/app/src/main/res/menu/action_menu_message_preview.xml index 22478867..04af8671 100644 --- a/app/src/main/res/menu/action_menu_message_preview.xml +++ b/app/src/main/res/menu/action_menu_message_preview.xml @@ -29,6 +29,13 @@ android:title="@string/message_forward" app:iconTint="@color/material_on_surface_emphasis_medium" app:showAsAction="ifRoom" /> + + + + Forward Select all Unselect all + Restore from trash Move to trash Delete permanently + Message restored successfully Message deleted successfully student parent @@ -364,6 +366,7 @@ %1$d selected Messages deleted + Messages restored Choose mailbox Incognito mode is on Thanks to incognito mode sender is not notified when you read the message From a7238e3f23703bdcc0af20b06fc09e904fee4ffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Fri, 1 Mar 2024 22:16:56 +0100 Subject: [PATCH 03/43] New Crowdin updates (#2447) --- app/src/main/res/values-cs/strings.xml | 3 +++ app/src/main/res/values-de/strings.xml | 3 +++ app/src/main/res/values-pl/strings.xml | 3 +++ app/src/main/res/values-ru/strings.xml | 3 +++ app/src/main/res/values-sk/strings.xml | 3 +++ app/src/main/res/values-uk/strings.xml | 3 +++ 6 files changed, 18 insertions(+) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 5c4c52da..fbc92e46 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -336,8 +336,10 @@ Poslat dále Vybrat vše Odznačit vše + Restore from trash Přesunout do koše Odstranit natrvalo + Message restored successfully Zpráva byla úspěšně odstraněna žák rodič @@ -383,6 +385,7 @@ %1$d vybraných Zprávy odstraněné + Messages restored Vyberte poštovní schránku Anonymní režim je zapnutý Díky anonymnímu režimu není odesílatel upozorněn, když si zprávu přečtete diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index a346bbd2..1f124600 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -296,8 +296,10 @@ Weiterleiten Alle auswählen Alle abwählen + Restore from trash In Papierkorb verschieben Dauerhaft löschen + Message restored successfully Nachricht erfolgreich gelöscht schüler Eltern @@ -335,6 +337,7 @@ %1$d ausgewählt Nachrichten gelöscht + Messages restored Postfach auswählen Incognito mode is on Thanks to incognito mode sender is not notified when you read the message diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 56a85ea2..597d843d 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -336,8 +336,10 @@ Prześlij dalej Zaznacz wszystkie Odznacz wszystkie + Przywróć z kosza Przenieś do kosza Usuń trwale + Wiadomość przywrócona pomyślnie Wiadomość usunięta pomyślnie uczeń rodzic @@ -383,6 +385,7 @@ %1$d wybranych Wiadomości zostały usunięte + Wiadomości przywrócone Wybierz skrzynkę Tryb incognito jest włączony Dzięki trybowi incognito nadawca nie zobaczy, że przeczytałeś tę wiadomość diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index f7469675..46a19c71 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -336,8 +336,10 @@ Переслать Выбрать все Отменить выбор + Restore from trash Перенести в корзину Удалить навсегда + Message restored successfully Сообщение успешно удалено ученик родитель @@ -383,6 +385,7 @@ %1$d выбрано Сообщение удалено + Messages restored Выбрать почтовый ящик Incognito mode is on Thanks to incognito mode sender is not notified when you read the message diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 56238c10..b63c07c6 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -336,8 +336,10 @@ Poslať ďalej Vybrať všetko Odznačiť všetko + Restore from trash Presunúť do koša Odstrániť natrvalo + Message restored successfully Správa bola úspešne odstránená žiak rodič @@ -383,6 +385,7 @@ %1$d vybraných Správy odstránené + Messages restored Vyberte poštovú schránku Režim inkognito je zapnutý Vďaka inkognito režimu nie je odosielateľ upozornený, keď si správu prečítate diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index a8202747..8116c7e4 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -336,8 +336,10 @@ Переслати Вибрати всі Відмінити вибір + Restore from trash Перемістити до кошика Видалити назавжди + Message restored successfully Лист було успішно видалено учень родич @@ -383,6 +385,7 @@ %1$d вибрано Листи видалено + Messages restored Вибрати поштову скриньку Режим анонімності включено Завдяки режиму анонімності, відправник не буде сповіщений коли ви прочитаєте повідомлення From ccba31f2e81d2aa36e3ff64912ad57a5c7a92102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sat, 2 Mar 2024 16:55:54 +0100 Subject: [PATCH 04/43] Add last announcements to school announcements (#2452) --- .../61.json | 2533 +++++++++++++++++ .../github/wulkanowy/data/db/AppDatabase.kt | 3 +- .../data/db/entities/SchoolAnnouncement.kt | 4 +- .../data/mappers/DirectorInformationMapper.kt | 14 + .../SchoolAnnouncementRepository.kt | 7 +- .../SchoolAnnouncementAdapter.kt | 5 + .../res/layout/item_school_announcement.xml | 33 +- 7 files changed, 2585 insertions(+), 14 deletions(-) create mode 100644 app/schemas/io.github.wulkanowy.data.db.AppDatabase/61.json diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/61.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/61.json new file mode 100644 index 00000000..e36dcc8a --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/61.json @@ -0,0 +1,2533 @@ +{ + "formatVersion": 1, + "database": { + "version": 61, + "identityHash": "41fbd2ff00aba10b2ef0a079e6037c87", + "entities": [ + { + "tableName": "Students", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scrapper_base_url` TEXT NOT NULL, `scrapper_domain_suffix` TEXT NOT NULL DEFAULT '', `mobile_base_url` TEXT NOT NULL, `login_type` TEXT NOT NULL, `login_mode` TEXT NOT NULL, `certificate_key` TEXT NOT NULL, `private_key` TEXT NOT NULL, `is_parent` INTEGER NOT NULL, `email` TEXT NOT NULL, `password` TEXT NOT NULL, `symbol` TEXT NOT NULL, `student_id` INTEGER NOT NULL, `user_login_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `student_name` TEXT NOT NULL, `school_id` TEXT NOT NULL, `school_short` TEXT NOT NULL, `school_name` TEXT NOT NULL, `class_name` TEXT NOT NULL, `class_id` INTEGER NOT NULL, `is_current` INTEGER NOT NULL, `registration_date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `nick` TEXT NOT NULL, `avatar_color` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "scrapperBaseUrl", + "columnName": "scrapper_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scrapperDomainSuffix", + "columnName": "scrapper_domain_suffix", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "mobileBaseUrl", + "columnName": "mobile_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginType", + "columnName": "login_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginMode", + "columnName": "login_mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "certificateKey", + "columnName": "certificate_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "private_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isParent", + "columnName": "is_parent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "user_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "student_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolSymbol", + "columnName": "school_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "school_short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolName", + "columnName": "school_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "className", + "columnName": "class_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isCurrent", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registrationDate", + "columnName": "registration_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nick", + "columnName": "nick", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarColor", + "columnName": "avatar_color", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Students_email_symbol_student_id_school_id_class_id", + "unique": true, + "columnNames": [ + "email", + "symbol", + "student_id", + "school_id", + "class_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Semesters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `kindergarten_diary_id` INTEGER NOT NULL DEFAULT 0, `diary_name` TEXT NOT NULL, `school_year` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `semester_name` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_current` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "kindergartenDiaryId", + "columnName": "kindergarten_diary_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "diaryName", + "columnName": "diary_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolYear", + "columnName": "school_year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterName", + "columnName": "semester_name", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "kindergarten_diary_id", + "semester_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `diary_id`, `kindergarten_diary_id`, `semester_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Exams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `group` TEXT NOT NULL, `type` TEXT NOT NULL, `description` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Timetable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `number` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `subjectOld` TEXT NOT NULL, `group` TEXT NOT NULL, `room` TEXT NOT NULL, `roomOld` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacherOld` TEXT NOT NULL, `info` TEXT NOT NULL, `student_plan` INTEGER NOT NULL, `changes` INTEGER NOT NULL, `canceled` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subjectOld", + "columnName": "subjectOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "room", + "columnName": "room", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roomOld", + "columnName": "roomOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherOld", + "columnName": "teacherOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "info", + "columnName": "info", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isStudentPlan", + "columnName": "student_plan", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "changes", + "columnName": "changes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canceled", + "columnName": "canceled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Attendance", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `time_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `excusable` INTEGER NOT NULL, `excuse_status` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeId", + "columnName": "time_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excusable", + "columnName": "excusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excuseStatus", + "columnName": "excuse_status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AttendanceSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `subject_id` INTEGER NOT NULL, `month` INTEGER NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `absence_excused` INTEGER NOT NULL, `absence_for_school_reasons` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `lateness_excused` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subject_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "month", + "columnName": "month", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceExcused", + "columnName": "absence_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceForSchoolReasons", + "columnName": "absence_for_school_reasons", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latenessExcused", + "columnName": "lateness_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Grades", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `entry` TEXT NOT NULL, `value` REAL NOT NULL, `modifier` REAL NOT NULL, `comment` TEXT NOT NULL, `color` TEXT NOT NULL, `grade_symbol` TEXT NOT NULL, `description` TEXT NOT NULL, `weight` TEXT NOT NULL, `weightValue` REAL NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entry", + "columnName": "entry", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "modifier", + "columnName": "modifier", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gradeSymbol", + "columnName": "grade_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weightValue", + "columnName": "weightValue", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `subject` TEXT NOT NULL, `predicted_grade` TEXT NOT NULL, `final_grade` TEXT NOT NULL, `proposed_points` TEXT NOT NULL, `final_points` TEXT NOT NULL, `points_sum` TEXT NOT NULL, `average` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_predicted_grade_notified` INTEGER NOT NULL, `is_final_grade_notified` INTEGER NOT NULL, `predicted_grade_last_change` INTEGER NOT NULL, `final_grade_last_change` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "predictedGrade", + "columnName": "predicted_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalGrade", + "columnName": "final_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "proposedPoints", + "columnName": "proposed_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalPoints", + "columnName": "final_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pointsSum", + "columnName": "points_sum", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "average", + "columnName": "average", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPredictedGradeNotified", + "columnName": "is_predicted_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFinalGradeNotified", + "columnName": "is_final_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "predictedGradeLastChange", + "columnName": "predicted_grade_last_change", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "finalGradeLastChange", + "columnName": "final_grade_last_change", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradePartialStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `class_average` TEXT NOT NULL, `student_average` TEXT NOT NULL, `class_amounts` TEXT NOT NULL, `student_amounts` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAverage", + "columnName": "class_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAverage", + "columnName": "student_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAmounts", + "columnName": "class_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAmounts", + "columnName": "student_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesPointsStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `others` REAL NOT NULL, `student` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "others", + "columnName": "others", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "student", + "columnName": "student", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradeSemesterStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `amounts` TEXT NOT NULL, `student_grade` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amounts", + "columnName": "amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentGrade", + "columnName": "student_grade", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `message_global_key` TEXT NOT NULL, `mailbox_key` TEXT NOT NULL, `message_id` INTEGER NOT NULL, `correspondents` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `read_by` INTEGER, `unread_by` INTEGER, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `content` TEXT NOT NULL, `sender` TEXT, `recipients` TEXT)", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "messageGlobalKey", + "columnName": "message_global_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mailboxKey", + "columnName": "mailbox_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "correspondents", + "columnName": "correspondents", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasAttachments", + "columnName": "has_attachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recipients", + "columnName": "recipients", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageAttachments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`message_global_key` TEXT NOT NULL, `url` TEXT NOT NULL, `filename` TEXT NOT NULL, PRIMARY KEY(`message_global_key`, `url`, `filename`))", + "fields": [ + { + "fieldPath": "messageGlobalKey", + "columnName": "message_global_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "message_global_key", + "url", + "filename" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `category` TEXT NOT NULL, `category_type` INTEGER NOT NULL, `is_points_show` INTEGER NOT NULL, `points` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryType", + "columnName": "category_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPointsShow", + "columnName": "is_points_show", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Homework", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `attachments` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_done` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL, `is_added_by_user` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDone", + "columnName": "is_done", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Subjects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "LuckyNumbers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `lucky_number` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "luckyNumber", + "columnName": "lucky_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CompletedLesson", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `topic` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `substitution` TEXT NOT NULL, `absence` TEXT NOT NULL, `resources` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "topic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "substitution", + "columnName": "substitution", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "resources", + "columnName": "resources", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Mailboxes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`globalKey` TEXT NOT NULL, `email` TEXT NOT NULL, `symbol` TEXT NOT NULL, `schoolId` TEXT NOT NULL, `fullName` TEXT NOT NULL, `userName` TEXT NOT NULL, `studentName` TEXT NOT NULL, `schoolNameShort` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`globalKey`))", + "fields": [ + { + "fieldPath": "globalKey", + "columnName": "globalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolId", + "columnName": "schoolId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "studentName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolNameShort", + "columnName": "schoolNameShort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "globalKey" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Recipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mailboxGlobalKey` TEXT NOT NULL, `studentMailboxGlobalKey` TEXT NOT NULL, `fullName` TEXT NOT NULL, `userName` TEXT NOT NULL, `schoolShortName` TEXT NOT NULL, `type` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "mailboxGlobalKey", + "columnName": "mailboxGlobalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentMailboxGlobalKey", + "columnName": "studentMailboxGlobalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "schoolShortName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MobileDevices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_login_id` INTEGER NOT NULL, `device_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Teachers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `short_name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "School", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL, `contact` TEXT NOT NULL, `headmaster` TEXT NOT NULL, `pedagogue` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contact", + "columnName": "contact", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "headmaster", + "columnName": "headmaster", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pedagogue", + "columnName": "pedagogue", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Conferences", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `subject` TEXT NOT NULL, `agenda` TEXT NOT NULL, `present_on_conference` TEXT NOT NULL, `conference_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agenda", + "columnName": "agenda", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presentOnConference", + "columnName": "present_on_conference", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conference_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableAdditional", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `repeat_id` BLOB DEFAULT NULL, `is_added_by_user` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatId", + "columnName": "repeat_id", + "affinity": "BLOB", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "StudentInfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `full_name` TEXT NOT NULL, `first_name` TEXT NOT NULL, `second_name` TEXT NOT NULL, `surname` TEXT NOT NULL, `birth_date` INTEGER NOT NULL, `birth_place` TEXT NOT NULL, `gender` TEXT NOT NULL, `has_polish_citizenship` INTEGER NOT NULL, `family_name` TEXT NOT NULL, `parents_names` TEXT NOT NULL, `address` TEXT NOT NULL, `registered_address` TEXT NOT NULL, `correspondence_address` TEXT NOT NULL, `phone_number` TEXT NOT NULL, `cell_phone_number` TEXT NOT NULL, `email` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `first_guardian_full_name` TEXT, `first_guardian_kinship` TEXT, `first_guardian_address` TEXT, `first_guardian_phones` TEXT, `first_guardian_email` TEXT, `second_guardian_full_name` TEXT, `second_guardian_kinship` TEXT, `second_guardian_address` TEXT, `second_guardian_phones` TEXT, `second_guardian_email` TEXT)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "full_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "first_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondName", + "columnName": "second_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "surname", + "columnName": "surname", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "birthDate", + "columnName": "birth_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthPlace", + "columnName": "birth_place", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gender", + "columnName": "gender", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPolishCitizenship", + "columnName": "has_polish_citizenship", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "familyName", + "columnName": "family_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentsNames", + "columnName": "parents_names", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "registeredAddress", + "columnName": "registered_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "correspondenceAddress", + "columnName": "correspondence_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phoneNumber", + "columnName": "phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cellPhoneNumber", + "columnName": "cell_phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "firstGuardian.fullName", + "columnName": "first_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.kinship", + "columnName": "first_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.address", + "columnName": "first_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.phones", + "columnName": "first_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.email", + "columnName": "first_guardian_email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.fullName", + "columnName": "second_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.kinship", + "columnName": "second_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.address", + "columnName": "second_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.phones", + "columnName": "second_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.email", + "columnName": "second_guardian_email", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableHeaders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SchoolAnnouncements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_login_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `author` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `type` TEXT NOT NULL, `destination` TEXT NOT NULL DEFAULT '{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}', `date` INTEGER NOT NULL, `data` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "destination", + "columnName": "destination", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}'" + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AdminMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `version_name` INTEGER, `version_max` INTEGER, `target_register_host` TEXT, `target_flavor` TEXT, `destination_url` TEXT, `priority` TEXT NOT NULL, `types` TEXT NOT NULL DEFAULT '[]', `is_ok_visible` INTEGER NOT NULL DEFAULT 0, `is_x_visible` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "versionMin", + "columnName": "version_name", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMax", + "columnName": "version_max", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "targetRegisterHost", + "columnName": "target_register_host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "targetFlavor", + "columnName": "target_flavor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "destinationUrl", + "columnName": "destination_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "priority", + "columnName": "priority", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "types", + "columnName": "types", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'[]'" + }, + { + "fieldPath": "isOkVisible", + "columnName": "is_ok_visible", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isXVisible", + "columnName": "is_x_visible", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MutedMessageSenders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`author` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesDescriptive", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `description` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '41fbd2ff00aba10b2ef0a079e6037c87')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt index 21a6e3f3..208daf75 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt @@ -173,6 +173,7 @@ import javax.inject.Singleton AutoMigration(from = 57, to = 58, spec = Migration58::class), AutoMigration(from = 58, to = 59), AutoMigration(from = 59, to = 60), + AutoMigration(from = 60, to = 61), ], version = AppDatabase.VERSION_SCHEMA, exportSchema = true @@ -181,7 +182,7 @@ import javax.inject.Singleton abstract class AppDatabase : RoomDatabase() { companion object { - const val VERSION_SCHEMA = 60 + const val VERSION_SCHEMA = 61 fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( Migration2(), diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/SchoolAnnouncement.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/SchoolAnnouncement.kt index 25e27ef1..ac096b02 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/SchoolAnnouncement.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/SchoolAnnouncement.kt @@ -16,7 +16,9 @@ data class SchoolAnnouncement( val subject: String, - val content: String + val content: String, + + val author: String? = null, ) : Serializable { @PrimaryKey(autoGenerate = true) diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/DirectorInformationMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/DirectorInformationMapper.kt index 16f1bbac..85b37afc 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/DirectorInformationMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/DirectorInformationMapper.kt @@ -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.Student import io.github.wulkanowy.sdk.pojo.DirectorInformation as SdkDirectorInformation +import io.github.wulkanowy.sdk.pojo.LastAnnouncement as SdkLastAnnouncement +@JvmName("mapDirectorInformationToEntities") fun List.mapToEntities(student: Student) = map { SchoolAnnouncement( userLoginId = student.userLoginId, date = it.date, subject = it.subject, content = it.content, + author = null, + ) +} + +@JvmName("mapLastAnnouncementsToEntities") +fun List.mapToEntities(student: Student) = map { + SchoolAnnouncement( + userLoginId = student.userLoginId, + date = it.date, + subject = it.subject, + content = it.content, + author = it.author, ) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolAnnouncementRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolAnnouncementRepository.kt index 4c42d092..8537fbc3 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolAnnouncementRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolAnnouncementRepository.kt @@ -41,9 +41,10 @@ class SchoolAnnouncementRepository @Inject constructor( schoolAnnouncementDb.loadAll(student.userLoginId) }, fetch = { - sdk.init(student) - .getDirectorInformation() - .mapToEntities(student) + val sdk = sdk.init(student) + val lastAnnouncements = sdk.getLastAnnouncements().mapToEntities(student) + val directorInformation = sdk.getDirectorInformation().mapToEntities(student) + lastAnnouncements + directorInformation }, saveFetchResult = { old, new -> val schoolAnnouncementsToSave = (new uniqueSubtract old).onEach { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementAdapter.kt index 46999599..731488a9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementAdapter.kt @@ -2,6 +2,7 @@ package io.github.wulkanowy.ui.modules.schoolannouncement import android.view.LayoutInflater import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import io.github.wulkanowy.databinding.ItemSchoolAnnouncementBinding @@ -29,6 +30,10 @@ class SchoolAnnouncementAdapter @Inject constructor() : schoolAnnouncementItemDate.text = item.date.toFormattedString() schoolAnnouncementItemType.text = item.subject schoolAnnouncementItemContent.text = item.content.parseUonetHtml() + with(schoolAnnouncementItemAuthor) { + text = item.author + isVisible = !item.author.isNullOrBlank() + } root.setOnClickListener { onItemClickListener(item) } } diff --git a/app/src/main/res/layout/item_school_announcement.xml b/app/src/main/res/layout/item_school_announcement.xml index bb0cffd1..1197c66c 100644 --- a/app/src/main/res/layout/item_school_announcement.xml +++ b/app/src/main/res/layout/item_school_announcement.xml @@ -11,27 +11,41 @@ android:id="@+id/schoolAnnouncementItemDate" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="15dp" + android:layout_marginHorizontal="15dp" android:layout_marginTop="10dp" - android:layout_marginEnd="10dp" android:textColor="?android:textColorSecondary" android:textSize="15sp" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toStartOf="@id/schoolAnnouncementItemAuthor" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:text="@tools:sample/date/ddmmyy" /> + + @@ -40,6 +54,7 @@ android:id="@+id/schoolAnnouncementItemContent" android:layout_width="0dp" android:layout_height="wrap_content" + android:layout_marginHorizontal="15dp" android:layout_marginTop="5dp" android:layout_marginBottom="15dp" android:ellipsize="end" @@ -47,8 +62,8 @@ android:maxLines="2" android:textSize="14sp" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="@+id/schoolAnnouncementItemType" - app:layout_constraintStart_toStartOf="@id/schoolAnnouncementItemDate" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/schoolAnnouncementItemType" tools:text="@tools:sample/lorem/random" /> From f2d26453ed330e49590194ceda5bc3c5f3b8e822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sat, 2 Mar 2024 17:01:12 +0100 Subject: [PATCH 05/43] Fix calculating average with optional arithmetic average on and no grade with average in second semester (#2448) --- .../ui/modules/grade/GradeAverageProvider.kt | 27 ++++++--- .../modules/grade/GradeAverageProviderTest.kt | 56 +++++++++++++++++-- 2 files changed, 71 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt index e8a5fa25..8da59eaf 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt @@ -159,7 +159,7 @@ class GradeAverageProvider @Inject constructor( ?.updateModifiers(student, config).orEmpty() (updatedSecondSemesterGrades + updatedFirstSemesterGrades).calcAverage( - config.isOptionalArithmeticAverage + isOptionalArithmeticAverage = config.isOptionalArithmeticAverage, ) } else { secondSemesterSubject.average @@ -173,13 +173,21 @@ class GradeAverageProvider @Inject constructor( config: AverageCalcParams, ): Double { 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 .updateModifiers(student, config) - .calcAverage(config.isOptionalArithmeticAverage) + .calcAverage(isOptionalArithmeticAverage = config.isOptionalArithmeticAverage) val firstSemesterAverage = firstSemesterSubject?.grades ?.updateModifiers(student, config) - ?.calcAverage(config.isOptionalArithmeticAverage) ?: secondSemesterAverage + ?.calcAverage(isOptionalArithmeticAverage = config.isOptionalArithmeticAverage) + ?: secondSemesterAverage (secondSemesterAverage + firstSemesterAverage) / divider } else { @@ -225,7 +233,7 @@ class GradeAverageProvider @Inject constructor( subject = summary.subject, average = if (!isAnyAverage || params.forceAverageCalc) { grades.updateModifiers(student, params) - .calcAverage(params.isOptionalArithmeticAverage) + .calcAverage(isOptionalArithmeticAverage = params.isOptionalArithmeticAverage) } else summary.average, points = summary.pointsSum, summary = summary, @@ -286,8 +294,13 @@ class GradeAverageProvider @Inject constructor( proposedPoints = "", finalPoints = "", pointsSum = "", - average = if (calcAverage) details.updateModifiers(student, params) - .calcAverage(params.isOptionalArithmeticAverage) else .0 + average = when { + calcAverage -> details + .updateModifiers(student, params) + .calcAverage(isOptionalArithmeticAverage = params.isOptionalArithmeticAverage) + + else -> .0 + } ) } } diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt index 6a717f6f..4f0f80fe 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test @@ -112,8 +113,8 @@ class GradeAverageProviderTest { private val secondGradeWithModifier = listOf( // avg: 3.375 - getGrade(24, "Język polski", 3.0, -0.50), - getGrade(24, "Język polski", 4.0, 0.25) + getGrade(24, "Język polski", 3.0, -0.50, entry = "3-"), + getGrade(24, "Język polski", 4.0, 0.25, entry = "4+") ) private val secondSummariesWithModifier = listOf( @@ -122,8 +123,8 @@ class GradeAverageProviderTest { private val noWeightGrades = listOf( // standard: 0.0, arithmetic: 4.0 - getGrade(22, "Matematyka", 5.0, 0.0, 0.0), - getGrade(22, "Matematyka", 3.0, 0.0, 0.0), + getGrade(22, "Matematyka", 5.0, 0.0, 0.0, "5"), + getGrade(22, "Matematyka", 3.0, 0.0, 0.0, "3"), getGrade(22, "Matematyka", 1.0, 0.0, 0.0, "np.") ) @@ -132,7 +133,7 @@ class GradeAverageProviderTest { ) private val noWeightGradesArithmeticSummary = listOf( - getSummary(23, "Matematyka", 4.0) + getSummary(23, "Matematyka", .0) ) @Before @@ -211,6 +212,51 @@ class GradeAverageProviderTest { ) // from summary: 4,0 } + @Test + fun `calc current semester arithmetic average with no weights in second semester`() = runTest { + every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(false) + every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(true) + every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.BOTH_SEMESTERS) + coEvery { + gradeRepository.getGrades( + student = student, + semester = semesters[1], + forceRefresh = true, + ) + } returns resourceFlow { + Triple( + first = noWeightGrades, + second = noWeightGradesArithmeticSummary, + third = emptyList(), + ) + } + coEvery { + gradeRepository.getGrades( + student = student, + semester = semesters[2], + forceRefresh = true, + ) + } returns resourceFlow { + Triple( + first = noWeightGrades, + second = noWeightGradesArithmeticSummary, + third = emptyList(), + ) + } + + val items = gradeAverageProvider.getGradesDetailsWithAverage( + student = student, + semesterId = semesters[2].semesterId, + forceRefresh = true + ).getResult() + + assertEquals( + 4.0, + items.single { it.subject == "Matematyka" }.average, + .0 + ) // from summary: 4,0 + } + @Test fun `calc current semester average with load from cache sequence`() { every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) From 3564366a8f04aafc5ba1a91b875ba39f62eda798 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Mar 2024 16:08:32 +0000 Subject: [PATCH 06/43] Bump com.google.firebase:firebase-bom from 32.7.2 to 32.7.3 (#2453) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 07efeb2f..f4ead943 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -252,7 +252,7 @@ dependencies { implementation 'com.fredporciuncula:flow-preferences:1.9.1' 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-messaging' playImplementation 'com.google.firebase:firebase-crashlytics:' From 05bda598fc5e60ff1c9ea88efda13f27a5007fbe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Mar 2024 16:10:19 +0000 Subject: [PATCH 07/43] Bump mockk from 1.13.9 to 1.13.10 (#2455) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index f4ead943..f01b9917 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -190,7 +190,7 @@ ext { android_hilt = "1.2.0" room = "2.6.1" chucker = "4.0.0" - mockk = "1.13.9" + mockk = "1.13.10" coroutines = "1.8.0" } From e9d64de0cbe88fec1a228a00a934d0b9b300dc81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sat, 2 Mar 2024 17:10:38 +0100 Subject: [PATCH 08/43] Improve invalid password message (#2451) --- .../github/wulkanowy/ui/base/BaseActivity.kt | 25 +++++++++++++++++++ .../wulkanowy/utils/ExceptionExtension.kt | 2 ++ app/src/main/res/values/strings.xml | 5 ++-- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt index 29996db7..10735dab 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt @@ -17,6 +17,8 @@ import io.github.wulkanowy.utils.FragmentLifecycleLogger import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.lifecycleAwareVariable import io.github.wulkanowy.utils.openInternetBrowser +import timber.log.Timber +import java.time.Instant import javax.inject.Inject abstract class BaseActivity, VB : ViewBinding> : @@ -36,6 +38,8 @@ abstract class BaseActivity, VB : ViewBinding> : abstract var presenter: T + private var lastDialogOpenTime = mutableMapOf() + override fun onCreate(savedInstanceState: Bundle?) { inject() themeManager.applyActivityTheme(this) @@ -70,6 +74,8 @@ abstract class BaseActivity, VB : ViewBinding> : } override fun showExpiredCredentialsDialog() { + if (!shouldShowDialog(DIALOG_ERROR_BAD_CREDENTIALS)) return + MaterialAlertDialogBuilder(this) .setTitle(R.string.main_expired_credentials_title) .setMessage(R.string.main_expired_credentials_description) @@ -83,6 +89,8 @@ abstract class BaseActivity, VB : ViewBinding> : } override fun showDecryptionFailedDialog() { + if (!shouldShowDialog(DIALOG_ERROR_DECRYPTION_FAILED)) return + MaterialAlertDialogBuilder(this) .setTitle(R.string.main_session_expired) .setMessage(R.string.main_session_relogin) @@ -119,4 +127,21 @@ abstract class BaseActivity, VB : ViewBinding> : protected open fun inject() { 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" + } } diff --git a/app/src/main/java/io/github/wulkanowy/utils/ExceptionExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/ExceptionExtension.kt index 1c229051..d541c0a7 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/ExceptionExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/ExceptionExtension.kt @@ -9,6 +9,7 @@ import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException import io.github.wulkanowy.sdk.scrapper.exception.ScrapperException import io.github.wulkanowy.sdk.scrapper.exception.ServiceUnavailableException 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.PasswordChangeRequiredException import okhttp3.internal.http2.StreamResetException @@ -34,6 +35,7 @@ fun Resources.getErrorString(error: Throwable): String = when (error) { is ServiceUnavailableException -> R.string.error_service_unavailable is FeatureDisabledException -> R.string.error_feature_disabled 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 ScrapperException -> R.string.error_unknown_app diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6843f1aa..266c3522 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -109,8 +109,8 @@ Log in Session expired Session expired, log in again - Your account password has been changed. You need to log in to Wulkanowy again - Password changed + Password has expired or been changed + Your account password has expired or been changed. You will need to log in to Wulkanowy again Application support Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time Enable ads @@ -858,6 +858,7 @@ This account is inactive. Try logging in again Connection to register failed. Servers can be overloaded. Please try again later Loading data failed. Please try again later + Your password has expired or been changed. Please log in again Register password change required Maintenance underway UONET + register. Try again later Unknown UONET + register error. Try again later From dc9af29a441991610e79e25bd8de529d14b3f019 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Mar 2024 16:11:13 +0000 Subject: [PATCH 09/43] Bump hilt_version from 2.50 to 2.51 (#2456) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f7f3d209..a23f2191 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { kotlin_version = '1.9.22' about_libraries = '10.10.0' - hilt_version = '2.50' + hilt_version = '2.51' } repositories { mavenCentral() From fb240938ed69801da29040a16245079c5989ac1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Mar 2024 16:21:55 +0000 Subject: [PATCH 10/43] Bump about_libraries from 10.10.0 to 11.1.0 (#2454) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a23f2191..ec19ee49 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { kotlin_version = '1.9.22' - about_libraries = '10.10.0' + about_libraries = '11.1.0' hilt_version = '2.51' } repositories { From 333306e7ba57c278bafe33e0310cc7e88eb5574b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sat, 2 Mar 2024 17:25:27 +0100 Subject: [PATCH 11/43] Wrap delete and save operations in database transactions (#2450) --- .../wulkanowy/data/db/dao/AdminMessageDao.kt | 14 +-- .../github/wulkanowy/data/db/dao/BaseDao.kt | 7 ++ .../data/repositories/AttendanceRepository.kt | 7 +- .../AttendanceSummaryRepository.kt | 9 +- .../CompletedLessonsRepository.kt | 14 ++- .../data/repositories/ConferenceRepository.kt | 12 +- .../data/repositories/ExamRepository.kt | 12 +- .../data/repositories/GradeRepository.kt | 87 +++++++------ .../repositories/GradeStatisticsRepository.kt | 22 ++-- .../data/repositories/HomeworkRepository.kt | 12 +- .../repositories/LuckyNumberRepository.kt | 11 +- .../data/repositories/MessageRepository.kt | 13 +- .../repositories/MobileDeviceRepository.kt | 7 +- .../data/repositories/NoteRepository.kt | 17 ++- .../data/repositories/RecipientRepository.kt | 12 +- .../SchoolAnnouncementRepository.kt | 12 +- .../data/repositories/SchoolRepository.kt | 8 +- .../data/repositories/SemesterRepository.kt | 15 ++- .../repositories/StudentInfoRepository.kt | 10 +- .../data/repositories/SubjectRepository.kt | 7 +- .../data/repositories/TeacherRepository.kt | 7 +- .../data/repositories/TimetableRepository.kt | 18 ++- .../repositories/AttendanceRepositoryTest.kt | 87 +++++++++---- .../CompletedLessonsRepositoryTest.kt | 117 +++++++++++------- .../data/repositories/ExamRemoteTest.kt | 66 ++++++---- .../data/repositories/GradeRepositoryTest.kt | 81 +++++++----- .../GradeStatisticsRepositoryTest.kt | 12 +- .../repositories/LuckyNumberRemoteTest.kt | 51 +++++--- .../repositories/MessageRepositoryTest.kt | 17 +-- .../MobileDeviceRepositoryTest.kt | 101 +++++++++------ .../data/repositories/RecipientLocalTest.kt | 20 ++- .../repositories/SemesterRepositoryTest.kt | 51 +++++--- .../repositories/TimetableRepositoryTest.kt | 17 +-- 33 files changed, 587 insertions(+), 366 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt index 2b4cb597..6c8d7e47 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt @@ -2,24 +2,14 @@ package io.github.wulkanowy.data.db.dao import androidx.room.Dao import androidx.room.Query -import androidx.room.Transaction import io.github.wulkanowy.data.db.entities.AdminMessage import kotlinx.coroutines.flow.Flow import javax.inject.Singleton @Singleton @Dao -abstract class AdminMessageDao : BaseDao { +interface AdminMessageDao : BaseDao { @Query("SELECT * FROM AdminMessages") - abstract fun loadAll(): Flow> - - @Transaction - open suspend fun removeOldAndSaveNew( - oldMessages: List, - newMessages: List - ) { - deleteAll(oldMessages) - insertAll(newMessages) - } + fun loadAll(): Flow> } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/BaseDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/BaseDao.kt index 056a5cbd..937e9824 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/BaseDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/BaseDao.kt @@ -3,6 +3,7 @@ package io.github.wulkanowy.data.db.dao import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy +import androidx.room.Transaction import androidx.room.Update interface BaseDao { @@ -15,4 +16,10 @@ interface BaseDao { @Delete suspend fun deleteAll(items: List) + + @Transaction + suspend fun removeOldAndSaveNew(oldItems: List, newItems: List) { + deleteAll(oldItems) + insertAll(newItems) + } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt index bbf627de..46ea29f8 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt @@ -65,12 +65,13 @@ class AttendanceRepository @Inject constructor( .mapToEntities(semester, lessons) }, saveFetchResult = { old, new -> - attendanceDb.deleteAll(old uniqueSubtract new) val attendanceToAdd = (new uniqueSubtract old).map { newAttendance -> newAttendance.apply { if (notify) isNotified = false } } - attendanceDb.insertAll(attendanceToAdd) - + attendanceDb.removeOldAndSaveNew( + oldItems = old uniqueSubtract new, + newItems = attendanceToAdd, + ) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end)) }, filterResult = { it.filter { item -> item.date in start..end } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt index 6bdcf9d7..c6cfc2f6 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt @@ -1,5 +1,7 @@ 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.entities.Semester import io.github.wulkanowy.data.db.entities.Student @@ -20,6 +22,7 @@ class AttendanceSummaryRepository @Inject constructor( private val attendanceDb: AttendanceSummaryDao, private val sdk: Sdk, private val refreshHelper: AutoRefreshHelper, + private val appDatabase: AppDatabase, ) { private val saveFetchResultMutex = Mutex() @@ -46,8 +49,10 @@ class AttendanceSummaryRepository @Inject constructor( .mapToEntities(semester, subjectId) }, saveFetchResult = { old, new -> - attendanceDb.deleteAll(old uniqueSubtract new) - attendanceDb.insertAll(new uniqueSubtract old) + appDatabase.withTransaction { + attendanceDb.deleteAll(old uniqueSubtract new) + attendanceDb.insertAll(new uniqueSubtract old) + } refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester)) } ) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt index 1579ae62..f7f86b23 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt @@ -6,7 +6,13 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.networkBoundResource 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 java.time.LocalDate import javax.inject.Inject @@ -53,8 +59,10 @@ class CompletedLessonsRepository @Inject constructor( .mapToEntities(semester) }, saveFetchResult = { old, new -> - completedLessonsDb.deleteAll(old uniqueSubtract new) - completedLessonsDb.insertAll(new uniqueSubtract old) + completedLessonsDb.removeOldAndSaveNew( + oldItems = old uniqueSubtract new, + newItems = new uniqueSubtract old, + ) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end)) }, filterResult = { it.filter { item -> item.date in start..end } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt index 7eb37f0b..fbe57860 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt @@ -53,12 +53,12 @@ class ConferenceRepository @Inject constructor( .filter { it.date >= startDate } }, saveFetchResult = { old, new -> - val conferencesToSave = (new uniqueSubtract old).onEach { - if (notify) it.isNotified = false - } - - conferenceDb.deleteAll(old uniqueSubtract new) - conferenceDb.insertAll(conferencesToSave) + conferenceDb.removeOldAndSaveNew( + oldItems = old uniqueSubtract new, + newItems = (new uniqueSubtract old).onEach { + if (notify) it.isNotified = false + }, + ) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester)) } ) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt index 96026a55..9b8dd02e 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt @@ -62,12 +62,12 @@ class ExamRepository @Inject constructor( .mapToEntities(semester) }, saveFetchResult = { old, new -> - val examsToSave = (new uniqueSubtract old).onEach { - if (notify) it.isNotified = false - } - - examDb.deleteAll(old uniqueSubtract new) - examDb.insertAll(examsToSave) + examDb.removeOldAndSaveNew( + oldItems = old uniqueSubtract new, + newItems = (new uniqueSubtract old).onEach { + if (notify) it.isNotified = false + }, + ) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end)) }, filterResult = { it.filter { item -> item.date in start..end } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt index 1e2ea935..ac1ef541 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt @@ -87,10 +87,12 @@ class GradeRepository @Inject constructor( new: List, notify: Boolean ) { - gradeDescriptiveDb.deleteAll(old uniqueSubtract new) - gradeDescriptiveDb.insertAll((new uniqueSubtract old).onEach { - if (notify) it.isNotified = false - }) + gradeDescriptiveDb.removeOldAndSaveNew( + oldItems = old uniqueSubtract new, + newItems = (new uniqueSubtract old).onEach { + if (notify) it.isNotified = false + }, + ) } private suspend fun refreshGradeDetails( @@ -101,13 +103,16 @@ class GradeRepository @Inject constructor( ) { val notifyBreakDate = oldGrades.maxByOrNull { it.date }?.date ?: student.registrationDate.toLocalDate() - gradeDb.deleteAll(oldGrades uniqueSubtract newDetails) - gradeDb.insertAll((newDetails uniqueSubtract oldGrades).onEach { - if (it.date >= notifyBreakDate) it.apply { - isRead = false - if (notify) isNotified = false - } - }) + + gradeDb.removeOldAndSaveNew( + oldItems = oldGrades uniqueSubtract newDetails, + newItems = (newDetails uniqueSubtract oldGrades).onEach { + if (it.date >= notifyBreakDate) it.apply { + isRead = false + if (notify) isNotified = false + } + }, + ) } private suspend fun refreshGradeSummaries( @@ -115,31 +120,43 @@ class GradeRepository @Inject constructor( newSummary: List, notify: Boolean ) { - gradeSummaryDb.deleteAll(oldSummaries uniqueSubtract newSummary) - gradeSummaryDb.insertAll((newSummary uniqueSubtract oldSummaries).onEach { summary -> - val oldSummary = oldSummaries.find { old -> old.subject == summary.subject } - summary.isPredictedGradeNotified = when { - summary.predictedGrade.isEmpty() -> true - notify && oldSummary?.predictedGrade != summary.predictedGrade -> false - else -> true - } - summary.isFinalGradeNotified = when { - summary.finalGrade.isEmpty() -> true - notify && oldSummary?.finalGrade != summary.finalGrade -> false - else -> true - } + gradeSummaryDb.removeOldAndSaveNew( + oldItems = oldSummaries uniqueSubtract newSummary, + newItems = (newSummary uniqueSubtract oldSummaries).onEach { summary -> + getGradeSummaryWithUpdatedNotificationState( + summary = summary, + oldSummary = oldSummaries.find { it.subject == summary.subject }, + notify = notify, + ) + }, + ) + } - 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 - } - }) + private fun getGradeSummaryWithUpdatedNotificationState( + summary: GradeSummary, + oldSummary: GradeSummary?, + notify: Boolean, + ) { + summary.isPredictedGradeNotified = when { + summary.predictedGrade.isEmpty() -> true + notify && oldSummary?.predictedGrade != summary.predictedGrade -> false + else -> true + } + summary.isFinalGradeNotified = when { + summary.finalGrade.isEmpty() -> true + notify && oldSummary?.finalGrade != summary.finalGrade -> false + else -> true + } + summary.predictedGradeLastChange = when { + oldSummary == null -> 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> { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt index 23d7b858..809f92d3 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt @@ -19,7 +19,7 @@ import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.switchSemester import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.sync.Mutex -import java.util.* +import java.util.Locale import javax.inject.Inject import javax.inject.Singleton @@ -62,8 +62,10 @@ class GradeStatisticsRepository @Inject constructor( .mapToEntities(semester) }, saveFetchResult = { old, new -> - gradePartialStatisticsDb.deleteAll(old uniqueSubtract new) - gradePartialStatisticsDb.insertAll(new uniqueSubtract old) + gradePartialStatisticsDb.removeOldAndSaveNew( + oldItems = old uniqueSubtract new, + newItems = new uniqueSubtract old, + ) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(partialCacheKey, semester)) }, mapResult = { items -> @@ -80,6 +82,7 @@ class GradeStatisticsRepository @Inject constructor( ) listOf(summaryItem) + items } + else -> items.filter { it.subject == subjectName } }.mapPartialToStatisticItems() } @@ -107,8 +110,10 @@ class GradeStatisticsRepository @Inject constructor( .mapToEntities(semester) }, saveFetchResult = { old, new -> - gradeSemesterStatisticsDb.deleteAll(old uniqueSubtract new) - gradeSemesterStatisticsDb.insertAll(new uniqueSubtract old) + gradeSemesterStatisticsDb.removeOldAndSaveNew( + oldItems = old uniqueSubtract new, + newItems = new uniqueSubtract old, + ) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(semesterCacheKey, semester)) }, mapResult = { items -> @@ -138,6 +143,7 @@ class GradeStatisticsRepository @Inject constructor( } listOf(summaryItem) + itemsWithAverage } + else -> itemsWithAverage.filter { it.subject == subjectName } }.mapSemesterToStatisticItems() } @@ -163,8 +169,10 @@ class GradeStatisticsRepository @Inject constructor( .mapToEntities(semester) }, saveFetchResult = { old, new -> - gradePointsStatisticsDb.deleteAll(old uniqueSubtract new) - gradePointsStatisticsDb.insertAll(new uniqueSubtract old) + gradePointsStatisticsDb.removeOldAndSaveNew( + oldItems = old uniqueSubtract new, + newItems = new uniqueSubtract old, + ) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(pointsCacheKey, semester)) }, mapResult = { items -> diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt index 010cf845..1a9c7ffa 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt @@ -61,14 +61,14 @@ class HomeworkRepository @Inject constructor( .mapToEntities(semester) }, saveFetchResult = { old, new -> - val homeWorkToSave = (new uniqueSubtract old).onEach { - if (notify) it.isNotified = false - } val filteredOld = old.filterNot { it.isAddedByUser } - homeworkDb.deleteAll(filteredOld uniqueSubtract new) - homeworkDb.insertAll(homeWorkToSave) - + homeworkDb.removeOldAndSaveNew( + oldItems = filteredOld uniqueSubtract new, + newItems = (new uniqueSubtract old).onEach { + if (notify) it.isNotified = false + }, + ) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end)) } ) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt index 4ff4517d..45b7f6e2 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt @@ -18,7 +18,7 @@ import javax.inject.Singleton @Singleton class LuckyNumberRepository @Inject constructor( private val luckyNumberDb: LuckyNumberDao, - private val sdk: Sdk + private val sdk: Sdk, ) { private val saveFetchResultMutex = Mutex() @@ -39,11 +39,10 @@ class LuckyNumberRepository @Inject constructor( newLuckyNumber ?: return@networkBoundResource if (newLuckyNumber != oldLuckyNumber) { - val updatedLuckNumberList = - listOf(newLuckyNumber.apply { if (notify) isNotified = false }) - - oldLuckyNumber?.let { luckyNumberDb.deleteAll(listOfNotNull(it)) } - luckyNumberDb.insertAll(updatedLuckNumberList) + luckyNumberDb.removeOldAndSaveNew( + oldItems = listOfNotNull(oldLuckyNumber), + newItems = listOf(newLuckyNumber.apply { if (notify) isNotified = false }), + ) } } ) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt index 96f04870..a4517760 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt @@ -89,12 +89,13 @@ class MessageRepository @Inject constructor( }, saveFetchResult = { oldWithAuthors, new -> val old = oldWithAuthors.map { it.message } - messagesDb.deleteAll(old uniqueSubtract new) - messagesDb.insertAll((new uniqueSubtract old).onEach { - val muted = isMuted(it.correspondents) - it.isNotified = !notify || muted - }) - + messagesDb.removeOldAndSaveNew( + oldItems = old uniqueSubtract new, + newItems = (new uniqueSubtract old).onEach { + val muted = isMuted(it.correspondents) + it.isNotified = !notify || muted + }, + ) refreshHelper.updateLastRefreshTimestamp( getRefreshKey(messagesCacheKey, mailbox, folder) ) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt index 412f9e7f..48b4fc28 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt @@ -48,9 +48,10 @@ class MobileDeviceRepository @Inject constructor( .mapToEntities(student) }, saveFetchResult = { old, new -> - mobileDb.deleteAll(old uniqueSubtract new) - mobileDb.insertAll(new uniqueSubtract old) - + mobileDb.removeOldAndSaveNew( + oldItems = old uniqueSubtract new, + newItems = new uniqueSubtract old, + ) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student)) } ) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt index eeb1d53e..feb92c15 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt @@ -7,7 +7,12 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.networkBoundResource 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.sync.Mutex import javax.inject.Inject @@ -46,14 +51,16 @@ class NoteRepository @Inject constructor( .mapToEntities(semester) }, saveFetchResult = { old, new -> - noteDb.deleteAll(old uniqueSubtract new) - noteDb.insertAll((new uniqueSubtract old).onEach { + val notesToAdd = (new uniqueSubtract old).onEach { if (it.date >= student.registrationDate.toLocalDate()) it.apply { isRead = false if (notify) isNotified = false } - }) - + } + noteDb.removeOldAndSaveNew( + oldItems = old uniqueSubtract new, + newItems = notesToAdd, + ) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester)) } ) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/RecipientRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/RecipientRepository.kt index 79984ce6..4a1474ce 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/RecipientRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/RecipientRepository.kt @@ -1,7 +1,11 @@ package io.github.wulkanowy.data.repositories 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.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper @@ -25,8 +29,10 @@ class RecipientRepository @Inject constructor( .mapToEntities(mailbox.globalKey) val old = recipientDb.loadAll(type, mailbox.globalKey) - recipientDb.deleteAll(old uniqueSubtract new) - recipientDb.insertAll(new uniqueSubtract old) + recipientDb.removeOldAndSaveNew( + oldItems = old uniqueSubtract new, + newItems = new uniqueSubtract old, + ) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student)) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolAnnouncementRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolAnnouncementRepository.kt index 8537fbc3..f09a46aa 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolAnnouncementRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolAnnouncementRepository.kt @@ -47,12 +47,12 @@ class SchoolAnnouncementRepository @Inject constructor( lastAnnouncements + directorInformation }, saveFetchResult = { old, new -> - val schoolAnnouncementsToSave = (new uniqueSubtract old).onEach { - if (notify) it.isNotified = false - } - - schoolAnnouncementDb.deleteAll(old uniqueSubtract new) - schoolAnnouncementDb.insertAll(schoolAnnouncementsToSave) + schoolAnnouncementDb.removeOldAndSaveNew( + oldItems = old uniqueSubtract new, + newItems = (new uniqueSubtract old).onEach { + if (notify) it.isNotified = false + }, + ) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student)) } ) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt index f757ef04..b42b4d57 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt @@ -47,10 +47,10 @@ class SchoolRepository @Inject constructor( }, saveFetchResult = { old, new -> if (old != null && new != old) { - with(schoolDb) { - deleteAll(listOf(old)) - insertAll(listOf(new)) - } + schoolDb.removeOldAndSaveNew( + oldItems = listOf(old), + newItems = listOf(new) + ) } else if (old == null) { schoolDb.insertAll(listOf(new)) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt index dd44df70..9ae22bab 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt @@ -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.mappers.mapToEntities 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 timber.log.Timber import javax.inject.Inject @@ -15,7 +19,7 @@ import javax.inject.Singleton class SemesterRepository @Inject constructor( private val semesterDb: SemesterDao, private val sdk: Sdk, - private val dispatchers: DispatchersProvider + private val dispatchers: DispatchersProvider, ) { suspend fun getSemesters( @@ -45,6 +49,7 @@ class SemesterRepository @Inject constructor( 0 == it.diaryId && 0 == it.kindergartenDiaryId } == true } + else -> false } @@ -59,8 +64,10 @@ class SemesterRepository @Inject constructor( if (new.isEmpty()) return Timber.i("Empty semester list!") val old = semesterDb.loadAll(student.studentId, student.classId) - semesterDb.deleteAll(old.uniqueSubtract(new)) - semesterDb.insertSemesters(new.uniqueSubtract(old)) + semesterDb.removeOldAndSaveNew( + oldItems = old uniqueSubtract new, + newItems = new uniqueSubtract old, + ) } suspend fun getCurrentSemester(student: Student, forceRefresh: Boolean = false) = diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt index d6cd25c8..d42be180 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt @@ -15,7 +15,7 @@ import javax.inject.Singleton @Singleton class StudentInfoRepository @Inject constructor( private val studentInfoDao: StudentInfoDao, - private val sdk: Sdk + private val sdk: Sdk, ) { private val saveFetchResultMutex = Mutex() @@ -36,10 +36,10 @@ class StudentInfoRepository @Inject constructor( }, saveFetchResult = { old, new -> if (old != null && new != old) { - with(studentInfoDao) { - deleteAll(listOf(old)) - insertAll(listOf(new)) - } + studentInfoDao.removeOldAndSaveNew( + oldItems = listOf(old), + newItems = listOf(new), + ) } else if (old == null) { studentInfoDao.insertAll(listOf(new)) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt index 98cb181a..cf7f86c2 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt @@ -45,9 +45,10 @@ class SubjectRepository @Inject constructor( .mapToEntities(semester) }, saveFetchResult = { old, new -> - subjectDao.deleteAll(old uniqueSubtract new) - subjectDao.insertAll(new uniqueSubtract old) - + subjectDao.removeOldAndSaveNew( + oldItems = old uniqueSubtract new, + newItems = new uniqueSubtract old + ) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester)) } ) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt index 42698f92..5a488b27 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt @@ -45,9 +45,10 @@ class TeacherRepository @Inject constructor( .mapToEntities(semester) }, saveFetchResult = { old, new -> - teacherDb.deleteAll(old uniqueSubtract new) - teacherDb.insertAll(new uniqueSubtract old) - + teacherDb.removeOldAndSaveNew( + oldItems = old uniqueSubtract new, + newItems = new uniqueSubtract old, + ) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester)) } ) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt index acbd02d1..0d208c1f 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt @@ -154,8 +154,10 @@ class TimetableRepository @Inject constructor( new.apply { if (notify) isNotified = false } } - timetableDb.deleteAll(lessonsToRemove) - timetableDb.insertAll(lessonsToAdd) + timetableDb.removeOldAndSaveNew( + oldItems = lessonsToRemove, + newItems = lessonsToAdd, + ) schedulerHelper.cancelScheduled(lessonsToRemove, student) schedulerHelper.scheduleNotifications(lessonsToAdd, student) @@ -166,13 +168,17 @@ class TimetableRepository @Inject constructor( new: List ) { val oldFiltered = old.filter { !it.isAddedByUser } - timetableAdditionalDb.deleteAll(oldFiltered uniqueSubtract new) - timetableAdditionalDb.insertAll(new uniqueSubtract old) + timetableAdditionalDb.removeOldAndSaveNew( + oldItems = oldFiltered uniqueSubtract new, + newItems = new uniqueSubtract old, + ) } private suspend fun refreshDayHeaders(old: List, new: List) { - timetableHeaderDb.deleteAll(old uniqueSubtract new) - timetableHeaderDb.insertAll(new uniqueSubtract old) + timetableHeaderDb.removeOldAndSaveNew( + oldItems = old uniqueSubtract new, + newItems = new uniqueSubtract old, + ) } fun getLastRefreshTimestamp(semester: Semester, start: LocalDate, end: LocalDate): Instant { diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt index d0e500f1..e64144c2 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt @@ -10,11 +10,17 @@ import io.github.wulkanowy.getSemesterEntity import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.sdk.Sdk 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.SpyK +import io.mockk.just import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test @@ -61,26 +67,36 @@ class AttendanceRepositoryTest { } @Test - fun `force refresh without difference`() { + fun `force refresh without difference`() = runTest { // prepare coEvery { sdk.getAttendance(startDate, endDate) } returns remoteList coEvery { attendanceDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf( flowOf(remoteList.mapToEntities(semester, emptyList())), flowOf(remoteList.mapToEntities(semester, emptyList())) ) - coEvery { attendanceDb.insertAll(any()) } returns listOf(1, 2, 3) - coEvery { attendanceDb.deleteAll(any()) } just Runs + coEvery { attendanceDb.removeOldAndSaveNew(any(), any()) } just Runs // 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 assertEquals(null, res.errorOrNull) assertEquals(2, res.dataOrNull?.size) coVerify { sdk.getAttendance(startDate, endDate) } coVerify { attendanceDb.loadAll(1, 1, startDate, endDate) } - coVerify { attendanceDb.insertAll(match { it.isEmpty() }) } - coVerify { attendanceDb.deleteAll(match { it.isEmpty() }) } + coVerify { + attendanceDb.removeOldAndSaveNew( + oldItems = match { it.isEmpty() }, + newItems = match { it.isEmpty() }, + ) + } } @Test @@ -89,14 +105,23 @@ class AttendanceRepositoryTest { coEvery { sdk.getAttendance(startDate, endDate) } returns remoteList coEvery { attendanceDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf( 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())) ) - coEvery { attendanceDb.insertAll(any()) } returns listOf(1, 2, 3) - coEvery { attendanceDb.deleteAll(any()) } just Runs + coEvery { attendanceDb.removeOldAndSaveNew(any(), any()) } just Runs // execute - val res = runBlocking { attendanceRepository.getAttendance(student, semester, startDate, endDate, true).toFirstResult() } + val res = runBlocking { + attendanceRepository.getAttendance( + student, + semester, + startDate, + endDate, + true + ).toFirstResult() + } // verify assertEquals(null, res.errorOrNull) @@ -104,11 +129,13 @@ class AttendanceRepositoryTest { coVerify { sdk.getAttendance(startDate, endDate) } coVerify { attendanceDb.loadAll(1, 1, startDate, endDate) } coVerify { - attendanceDb.insertAll(match { - it.size == 1 && it[0] == remoteList.mapToEntities(semester, emptyList())[1] - }) + attendanceDb.removeOldAndSaveNew( + oldItems = match { it.isEmpty() }, + newItems = match { + it.size == 1 && it[0] == remoteList.mapToEntities(semester, emptyList())[1] + }, + ) } - coVerify { attendanceDb.deleteAll(match { it.isEmpty() }) } } @Test @@ -117,25 +144,39 @@ class AttendanceRepositoryTest { coEvery { sdk.getAttendance(startDate, endDate) } returns remoteList.dropLast(1) coEvery { attendanceDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf( 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())) ) - coEvery { attendanceDb.insertAll(any()) } returns listOf(1, 2, 3) - coEvery { attendanceDb.deleteAll(any()) } just Runs + coEvery { attendanceDb.removeOldAndSaveNew(any(), any()) } just Runs // execute - val res = runBlocking { attendanceRepository.getAttendance(student, semester, startDate, endDate, true).toFirstResult() } + val res = runBlocking { + attendanceRepository.getAttendance( + student, + semester, + startDate, + endDate, + true + ).toFirstResult() + } // verify assertEquals(null, res.errorOrNull) assertEquals(1, res.dataOrNull?.size) coVerify { sdk.getAttendance(startDate, endDate) } coVerify { attendanceDb.loadAll(1, 1, startDate, endDate) } - coVerify { attendanceDb.insertAll(match { it.isEmpty() }) } coVerify { - attendanceDb.deleteAll(match { - it.size == 1 && it[0] == remoteList.mapToEntities(semester, emptyList())[1] - }) + attendanceDb.removeOldAndSaveNew( + oldItems = match { + it.size == 1 && it[0] == remoteList.mapToEntities(semester, emptyList())[1] + }, + newItems = emptyList(), + ) } } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepositoryTest.kt index c28ea304..f8f68850 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepositoryTest.kt @@ -9,11 +9,16 @@ import io.github.wulkanowy.getSemesterEntity import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.sdk.Sdk 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.SpyK +import io.mockk.just import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test @@ -52,46 +57,28 @@ class CompletedLessonsRepositoryTest { MockKAnnotations.init(this) every { refreshHelper.shouldBeRefreshed(any()) } returns false - completedLessonRepository = CompletedLessonsRepository(completedLessonDb, sdk, refreshHelper) + completedLessonRepository = + CompletedLessonsRepository(completedLessonDb, sdk, refreshHelper) } @Test - fun `force refresh without difference`() { + fun `force refresh without difference`() = runTest { // prepare coEvery { sdk.getCompletedLessons(startDate, endDate) } returns remoteList coEvery { completedLessonDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf( flowOf(remoteList.mapToEntities(semester)), flowOf(remoteList.mapToEntities(semester)) ) - coEvery { completedLessonDb.insertAll(any()) } returns listOf(1, 2, 3) - coEvery { completedLessonDb.deleteAll(any()) } just Runs + coEvery { completedLessonDb.removeOldAndSaveNew(any(), any()) } just Runs // execute - val res = runBlocking { completedLessonRepository.getCompletedLessons(student, semester, startDate, endDate, 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.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() } + val res = completedLessonRepository.getCompletedLessons( + student = student, + semester = semester, + start = startDate, + end = endDate, + forceRefresh = true, + ).toFirstResult() // verify assertEquals(null, res.errorOrNull) @@ -99,15 +86,52 @@ class CompletedLessonsRepositoryTest { coVerify { sdk.getCompletedLessons(startDate, endDate) } coVerify { completedLessonDb.loadAll(1, 1, startDate, endDate) } coVerify { - completedLessonDb.insertAll(match { - it.size == 1 && it[0] == remoteList.mapToEntities(semester)[1] - }) + completedLessonDb.removeOldAndSaveNew( + oldItems = match { it.isEmpty() }, + newItems = match { it.isEmpty() }, + ) } - coVerify { completedLessonDb.deleteAll(match { it.isEmpty() }) } } @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 coEvery { sdk.getCompletedLessons(startDate, endDate) } returns remoteList.dropLast(1) 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.dropLast(1).mapToEntities(semester)) ) - coEvery { completedLessonDb.insertAll(any()) } returns listOf(1, 2, 3) - coEvery { completedLessonDb.deleteAll(any()) } just Runs + coEvery { completedLessonDb.removeOldAndSaveNew(any(), any()) } just Runs // 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 assertEquals(null, res.errorOrNull) assertEquals(1, res.dataOrNull?.size) coVerify { sdk.getCompletedLessons(startDate, endDate) } coVerify { completedLessonDb.loadAll(1, 1, startDate, endDate) } - coVerify { completedLessonDb.insertAll(match { it.isEmpty() }) } coVerify { - completedLessonDb.deleteAll(match { - it.size == 1 && it[0] == remoteList.mapToEntities(semester)[1] - }) + completedLessonDb.removeOldAndSaveNew( + oldItems = match { + it.size == 1 && it[0] == remoteList.mapToEntities(semester)[1] + }, + newItems = match { it.isEmpty() }, + ) } } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt index fb037a87..d1ed9ca3 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt @@ -9,11 +9,17 @@ import io.github.wulkanowy.getSemesterEntity import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.sdk.Sdk 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.SpyK +import io.mockk.just import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test @@ -64,35 +70,42 @@ class ExamRemoteTest { flowOf(remoteList.mapToEntities(semester)), flowOf(remoteList.mapToEntities(semester)) ) - coEvery { examDb.insertAll(any()) } returns listOf(1, 2, 3) - coEvery { examDb.deleteAll(any()) } just Runs + coEvery { examDb.removeOldAndSaveNew(any(), any()) } just Runs // execute - val res = runBlocking { examRepository.getExams(student, semester, startDate, endDate, true).toFirstResult() } + val res = runBlocking { + examRepository.getExams(student, semester, startDate, endDate, true).toFirstResult() + } // verify assertEquals(null, res.errorOrNull) assertEquals(2, res.dataOrNull?.size) coVerify { sdk.getExams(startDate, realEndDate) } coVerify { examDb.loadAll(1, 1, startDate, realEndDate) } - coVerify { examDb.insertAll(match { it.isEmpty() }) } - coVerify { examDb.deleteAll(match { it.isEmpty() }) } + coVerify { examDb.removeOldAndSaveNew(emptyList(), emptyList()) } } @Test - fun `force refresh with more items in remote`() { + fun `force refresh with more items in remote`() = runTest { // prepare coEvery { sdk.getExams(startDate, realEndDate) } returns remoteList coEvery { examDb.loadAll(1, 1, startDate, realEndDate) } returnsMany listOf( 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)) ) - coEvery { examDb.insertAll(any()) } returns listOf(1, 2, 3) - coEvery { examDb.deleteAll(any()) } just Runs + coEvery { examDb.removeOldAndSaveNew(any(), any()) } just Runs // 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 assertEquals(null, res.errorOrNull) @@ -100,15 +113,17 @@ class ExamRemoteTest { coVerify { sdk.getExams(startDate, realEndDate) } coVerify { examDb.loadAll(1, 1, startDate, realEndDate) } coVerify { - examDb.insertAll(match { - it.size == 1 && it[0] == remoteList.mapToEntities(semester)[1] - }) + examDb.removeOldAndSaveNew( + oldItems = emptyList(), + newItems = match { + it.size == 1 && it[0] == remoteList.mapToEntities(semester)[1] + }, + ) } - coVerify { examDb.deleteAll(match { it.isEmpty() }) } } @Test - fun `force refresh with more items in local`() { + fun `force refresh with more items in local`() = runTest { // prepare coEvery { sdk.getExams(startDate, realEndDate) } returns remoteList.dropLast(1) 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.dropLast(1).mapToEntities(semester)) ) - coEvery { examDb.insertAll(any()) } returns listOf(1, 2, 3) - coEvery { examDb.deleteAll(any()) } just Runs + coEvery { examDb.removeOldAndSaveNew(any(), any()) } just Runs // 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 assertEquals(null, res.errorOrNull) assertEquals(1, res.dataOrNull?.size) coVerify { sdk.getExams(startDate, realEndDate) } coVerify { examDb.loadAll(1, 1, startDate, realEndDate) } - coVerify { examDb.insertAll(match { it.isEmpty() }) } coVerify { - examDb.deleteAll(match { - it.size == 1 && it[0] == remoteList.mapToEntities(semester)[1] - }) + examDb.removeOldAndSaveNew( + oldItems = match { it.size == 1 && it[0] == remoteList.mapToEntities(semester)[1] }, + newItems = emptyList() + ) } } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt index 515b0d66..0ea5d3fa 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt @@ -22,6 +22,7 @@ import io.mockk.impl.annotations.SpyK import io.mockk.just import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -60,26 +61,27 @@ class GradeRepositoryTest { MockKAnnotations.init(this) every { refreshHelper.shouldBeRefreshed(any()) } returns false - gradeRepository = - GradeRepository(gradeDb, gradeSummaryDb, gradeDescriptiveDb, sdk, refreshHelper) + gradeRepository = GradeRepository( + gradeDb = gradeDb, + gradeSummaryDb = gradeSummaryDb, + gradeDescriptiveDb = gradeDescriptiveDb, + sdk = sdk, + refreshHelper = refreshHelper, + ) - coEvery { gradeDb.deleteAll(any()) } just Runs - coEvery { gradeDb.insertAll(any()) } returns listOf() + coEvery { gradeDb.removeOldAndSaveNew(any(), any()) } just Runs + coEvery { gradeSummaryDb.removeOldAndSaveNew(any(), any()) } just Runs coEvery { gradeSummaryDb.loadAll(1, 1) } returnsMany listOf( flowOf(listOf()), flowOf(listOf()), flowOf(listOf()) ) - coEvery { gradeSummaryDb.deleteAll(any()) } just Runs - coEvery { gradeSummaryDb.insertAll(any()) } returns listOf() + coEvery { gradeDescriptiveDb.removeOldAndSaveNew(any(), any()) } just Runs coEvery { gradeDescriptiveDb.loadAll(any(), any()) } returnsMany listOf( flowOf(listOf()), ) - - coEvery { gradeDescriptiveDb.deleteAll(any()) } just Runs - coEvery { gradeDescriptiveDb.insertAll(any()) } returns listOf() } @Test @@ -113,13 +115,16 @@ class GradeRepositoryTest { assertEquals(null, res.errorOrNull) assertEquals(4, res.dataOrNull?.first?.size) coVerify { - gradeDb.insertAll(withArg { - assertEquals(4, it.size) - assertTrue(it[0].isRead) - assertTrue(it[1].isRead) - assertFalse(it[2].isRead) - assertFalse(it[3].isRead) - }) + gradeDb.removeOldAndSaveNew( + oldItems = emptyList(), + newItems = withArg { + assertEquals(4, it.size) + assertTrue(it[0].isRead) + assertTrue(it[1].isRead) + assertFalse(it[2].isRead) + assertFalse(it[3].isRead) + }, + ) } } @@ -167,23 +172,23 @@ class GradeRepositoryTest { assertEquals(null, res.errorOrNull) assertEquals(4, res.dataOrNull?.first?.size) coVerify { - gradeDb.insertAll(withArg { - assertEquals(3, it.size) - assertTrue(it[0].isRead) - assertTrue(it[1].isRead) - assertFalse(it[2].isRead) - assertEquals(remoteList.mapToEntities(semester).last(), it[2]) - }) - } - coVerify { - gradeDb.deleteAll(withArg { - assertEquals(2, it.size) - }) + gradeDb.removeOldAndSaveNew( + oldItems = withArg { + assertEquals(2, it.size) + }, + newItems = withArg { + assertEquals(3, it.size) + assertTrue(it[0].isRead) + assertTrue(it[1].isRead) + assertFalse(it[2].isRead) + assertEquals(remoteList.mapToEntities(semester).last(), it[2]) + } + ) } } @Test - fun `force refresh when local contains duplicated grades`() { + fun `force refresh when local contains duplicated grades`() = runTest { // prepare val remoteList = listOf( createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"), @@ -203,13 +208,17 @@ class GradeRepositoryTest { ) // execute - val res = runBlocking { gradeRepository.getGrades(student, semester, true).toFirstResult() } + val res = gradeRepository.getGrades(student, semester, true).toFirstResult() // verify assertEquals(null, res.errorOrNull) assertEquals(2, res.dataOrNull?.first?.size) - coVerify { gradeDb.insertAll(match { it.isEmpty() }) } - coVerify { gradeDb.deleteAll(match { it.size == 1 }) } // ... here + coVerify { + gradeDb.removeOldAndSaveNew( + oldItems = match { it.size == 1 }, // ... here + newItems = emptyList() + ) + } } @Test @@ -238,8 +247,12 @@ class GradeRepositoryTest { // verify assertEquals(null, res.errorOrNull) assertEquals(3, res.dataOrNull?.first?.size) - coVerify { gradeDb.insertAll(match { it.size == 1 }) } // ... here - coVerify { gradeDb.deleteAll(match { it.isEmpty() }) } + coVerify { + gradeDb.removeOldAndSaveNew( + oldItems = emptyList(), + newItems = match { it.size == 1 }, // ... here + ) + } } @Test diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepositoryTest.kt index 8e2f7c6e..dfd36ee1 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepositoryTest.kt @@ -71,8 +71,7 @@ class GradeStatisticsRepositoryTest { flowOf(remotePartialList.mapToEntities(semester)), flowOf(remotePartialList.mapToEntities(semester)) ) - coEvery { gradePartialStatisticsDb.insertAll(any()) } returns listOf(1, 2, 3) - coEvery { gradePartialStatisticsDb.deleteAll(any()) } just Runs + coEvery { gradePartialStatisticsDb.removeOldAndSaveNew(any(), any()) } just Runs // execute val res = runBlocking { @@ -93,8 +92,7 @@ class GradeStatisticsRepositoryTest { assertEquals("", items[2].partial?.studentAverage) coVerify { sdk.getGradesPartialStatistics(1) } coVerify { gradePartialStatisticsDb.loadAll(1, 1) } - coVerify { gradePartialStatisticsDb.insertAll(match { it.isEmpty() }) } - coVerify { gradePartialStatisticsDb.deleteAll(match { it.isEmpty() }) } + coVerify { gradePartialStatisticsDb.removeOldAndSaveNew(emptyList(), emptyList()) } } @Test @@ -109,8 +107,7 @@ class GradeStatisticsRepositoryTest { flowOf(remotePartialList.mapToEntities(semester)), flowOf(remotePartialList.mapToEntities(semester)) ) - coEvery { gradePartialStatisticsDb.insertAll(any()) } returns listOf(1, 2, 3) - coEvery { gradePartialStatisticsDb.deleteAll(any()) } just Runs + coEvery { gradePartialStatisticsDb.removeOldAndSaveNew(any(), any()) } just Runs // execute val res = runBlocking { @@ -131,8 +128,7 @@ class GradeStatisticsRepositoryTest { assertEquals("5.0", items[2].partial?.studentAverage) coVerify { sdk.getGradesPartialStatistics(1) } coVerify { gradePartialStatisticsDb.loadAll(1, 1) } - coVerify { gradePartialStatisticsDb.insertAll(match { it.isEmpty() }) } - coVerify { gradePartialStatisticsDb.deleteAll(match { it.isEmpty() }) } + coVerify { gradePartialStatisticsDb.removeOldAndSaveNew(emptyList(), emptyList()) } } private fun getGradeStatisticsPartialSubject( diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/LuckyNumberRemoteTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/LuckyNumberRemoteTest.kt index 3225c3bd..fa78b1bd 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/LuckyNumberRemoteTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/LuckyNumberRemoteTest.kt @@ -7,11 +7,16 @@ import io.github.wulkanowy.data.mappers.mapToEntity import io.github.wulkanowy.data.toFirstResult import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.sdk.Sdk -import io.mockk.* +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerify import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.SpyK +import io.mockk.just import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test @@ -53,7 +58,8 @@ class LuckyNumberRemoteTest { coEvery { luckyNumberDb.deleteAll(any()) } just Runs // execute - val res = runBlocking { luckyNumberRepository.getLuckyNumber(student, true).toFirstResult() } + val res = + runBlocking { luckyNumberRepository.getLuckyNumber(student, true).toFirstResult() } // verify assertEquals(null, res.errorOrNull) @@ -65,19 +71,19 @@ class LuckyNumberRemoteTest { } @Test - fun `force refresh with different item on remote`() { + fun `force refresh with different item on remote`() = runTest { // prepare coEvery { sdk.getLuckyNumber(student.schoolShortName) } returns luckyNumber coEvery { luckyNumberDb.load(1, date) } returnsMany listOf( flowOf(luckyNumber.mapToEntity(student).copy(luckyNumber = 6666)), - flowOf(luckyNumber.mapToEntity(student).copy(luckyNumber = 6666)), // after fetch end before save result + // after fetch end before save result + flowOf(luckyNumber.mapToEntity(student).copy(luckyNumber = 6666)), flowOf(luckyNumber.mapToEntity(student)) ) - coEvery { luckyNumberDb.insertAll(any()) } returns listOf(1, 2, 3) - coEvery { luckyNumberDb.deleteAll(any()) } just Runs + coEvery { luckyNumberDb.removeOldAndSaveNew(any(), any()) } just Runs // execute - val res = runBlocking { luckyNumberRepository.getLuckyNumber(student, true).toFirstResult() } + val res = luckyNumberRepository.getLuckyNumber(student, true).toFirstResult() // verify assertEquals(null, res.errorOrNull) @@ -85,13 +91,16 @@ class LuckyNumberRemoteTest { coVerify { sdk.getLuckyNumber(student.schoolShortName) } coVerify { luckyNumberDb.load(1, date) } coVerify { - luckyNumberDb.insertAll(match { - it.size == 1 && it[0] == luckyNumber.mapToEntity(student) - }) + luckyNumberDb.removeOldAndSaveNew( + oldItems = match { + it.size == 1 && it[0] == luckyNumber.mapToEntity(student) + .copy(luckyNumber = 6666) + }, + newItems = match { + it.size == 1 && it[0] == luckyNumber.mapToEntity(student) + } + ) } - coVerify { luckyNumberDb.deleteAll(match { - it.size == 1 && it[0] == luckyNumber.mapToEntity(student).copy(luckyNumber = 6666) - }) } } @Test @@ -103,11 +112,11 @@ class LuckyNumberRemoteTest { flowOf(null), // after fetch end before save result flowOf(luckyNumber.mapToEntity(student)) ) - coEvery { luckyNumberDb.insertAll(any()) } returns listOf(1, 2, 3) - coEvery { luckyNumberDb.deleteAll(any()) } just Runs + coEvery { luckyNumberDb.removeOldAndSaveNew(any(), any()) } just Runs // execute - val res = runBlocking { luckyNumberRepository.getLuckyNumber(student, true).toFirstResult() } + val res = + runBlocking { luckyNumberRepository.getLuckyNumber(student, true).toFirstResult() } // verify assertEquals(null, res.errorOrNull) @@ -115,10 +124,12 @@ class LuckyNumberRemoteTest { coVerify { sdk.getLuckyNumber(student.schoolShortName) } coVerify { luckyNumberDb.load(1, date) } coVerify { - luckyNumberDb.insertAll(match { - it.size == 1 && it[0] == luckyNumber.mapToEntity(student) - }) + luckyNumberDb.removeOldAndSaveNew( + oldItems = emptyList(), + newItems = match { + it.size == 1 && it[0] == luckyNumber.mapToEntity(student) + } + ) } - coVerify(exactly = 0) { luckyNumberDb.deleteAll(any()) } } } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt index 58937e77..fbbe4934 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt @@ -113,7 +113,7 @@ class MessageRepositoryTest { } @Test - fun `get messages when fetched completely new message without notify`() = runBlocking { + fun `get messages when fetched completely new message without notify`() = runTest { coEvery { mailboxDao.loadAll(any()) } returns listOf(mailbox) every { messageDb.loadAll(mailbox.globalKey, any()) } returns flowOf(emptyList()) coEvery { sdk.getMessages(Folder.RECEIVED, any()) } returns listOf( @@ -122,8 +122,7 @@ class MessageRepositoryTest { readBy = 10, ) ) - coEvery { messageDb.deleteAll(any()) } just Runs - coEvery { messageDb.insertAll(any()) } returns listOf() + coEvery { messageDb.removeOldAndSaveNew(any(), any()) } just Runs val res = repository.getMessages( student = student, @@ -134,12 +133,14 @@ class MessageRepositoryTest { ).toFirstResult() assertEquals(null, res.errorOrNull) - coVerify(exactly = 1) { messageDb.deleteAll(withArg { checkEquals(emptyList()) }) } coVerify { - messageDb.insertAll(withArg { - assertEquals(4, it.single().messageId) - assertTrue(it.single().isNotified) - }) + messageDb.removeOldAndSaveNew( + oldItems = withArg { checkEquals(emptyList()) }, + newItems = withArg { + assertEquals(4, it.single().messageId) + assertTrue(it.single().isNotified) + }, + ) } } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt index 1a3f9679..aa93a5e6 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt @@ -19,7 +19,7 @@ import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.SpyK import io.mockk.just import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest import org.junit.Assert import org.junit.Before import org.junit.Test @@ -57,42 +57,21 @@ class MobileDeviceRepositoryTest { } @Test - fun `force refresh without difference`() { + fun `force refresh without difference`() = runTest { // prepare coEvery { sdk.getRegisteredDevices() } returns remoteList coEvery { mobileDeviceDb.loadAll(student.studentId) } returnsMany listOf( flowOf(remoteList.mapToEntities(student)), flowOf(remoteList.mapToEntities(student)) ) - coEvery { mobileDeviceDb.insertAll(any()) } returns listOf(1, 2, 3) - coEvery { mobileDeviceDb.deleteAll(any()) } just Runs + coEvery { mobileDeviceDb.removeOldAndSaveNew(any(), any()) } just Runs // execute - val res = runBlocking { mobileDeviceRepository.getDevices(student, semester, true).toFirstResult() } - - // verify - Assert.assertEquals(null, res.errorOrNull) - Assert.assertEquals(2, res.dataOrNull?.size) - coVerify { sdk.getRegisteredDevices() } - coVerify { mobileDeviceDb.loadAll(1) } - coVerify { mobileDeviceDb.insertAll(match { it.isEmpty() }) } - coVerify { mobileDeviceDb.deleteAll(match { it.isEmpty() }) } - } - - @Test - fun `force refresh with more items in remote`() { - // prepare - coEvery { sdk.getRegisteredDevices() } returns remoteList - coEvery { mobileDeviceDb.loadAll(1) } returnsMany listOf( - flowOf(remoteList.dropLast(1).mapToEntities(student)), - flowOf(remoteList.dropLast(1).mapToEntities(student)), // after fetch end before save result - flowOf(remoteList.mapToEntities(student)) - ) - coEvery { mobileDeviceDb.insertAll(any()) } returns listOf(1, 2, 3) - coEvery { mobileDeviceDb.deleteAll(any()) } just Runs - - // execute - val res = runBlocking { mobileDeviceRepository.getDevices(student, semester, true).toFirstResult() } + val res = mobileDeviceRepository.getDevices( + student = student, + semester = semester, + forceRefresh = true, + ).toFirstResult() // verify Assert.assertEquals(null, res.errorOrNull) @@ -100,15 +79,50 @@ class MobileDeviceRepositoryTest { coVerify { sdk.getRegisteredDevices() } coVerify { mobileDeviceDb.loadAll(1) } coVerify { - mobileDeviceDb.insertAll(match { - it.size == 1 && it[0] == remoteList.mapToEntities(student)[1] - }) + mobileDeviceDb.removeOldAndSaveNew( + oldItems = match { it.isEmpty() }, + newItems = match { it.isEmpty() }, + ) } - coVerify { mobileDeviceDb.deleteAll(match { it.isEmpty() }) } } @Test - fun `force refresh with more items in local`() { + fun `force refresh with more items in remote`() = runTest { + // prepare + coEvery { sdk.getRegisteredDevices() } returns remoteList + coEvery { mobileDeviceDb.loadAll(1) } returnsMany listOf( + flowOf(remoteList.dropLast(1).mapToEntities(student)), + flowOf( + remoteList.dropLast(1).mapToEntities(student) + ), // after fetch end before save result + flowOf(remoteList.mapToEntities(student)) + ) + coEvery { mobileDeviceDb.removeOldAndSaveNew(any(), any()) } just Runs + + // execute + val res = mobileDeviceRepository.getDevices( + student = student, + semester = semester, + forceRefresh = true, + ).toFirstResult() + + // verify + Assert.assertEquals(null, res.errorOrNull) + Assert.assertEquals(2, res.dataOrNull?.size) + coVerify { sdk.getRegisteredDevices() } + coVerify { mobileDeviceDb.loadAll(1) } + coVerify { + mobileDeviceDb.removeOldAndSaveNew( + oldItems = match { it.isEmpty() }, + newItems = match { + it.size == 1 && it[0] == remoteList.mapToEntities(student)[1] + }, + ) + } + } + + @Test + fun `force refresh with more items in local`() = runTest { // prepare coEvery { sdk.getRegisteredDevices() } returns remoteList.dropLast(1) coEvery { mobileDeviceDb.loadAll(1) } returnsMany listOf( @@ -116,22 +130,27 @@ class MobileDeviceRepositoryTest { flowOf(remoteList.mapToEntities(student)), // after fetch end before save result flowOf(remoteList.dropLast(1).mapToEntities(student)) ) - coEvery { mobileDeviceDb.insertAll(any()) } returns listOf(1, 2, 3) - coEvery { mobileDeviceDb.deleteAll(any()) } just Runs + coEvery { mobileDeviceDb.removeOldAndSaveNew(any(), any()) } just Runs // execute - val res = runBlocking { mobileDeviceRepository.getDevices(student, semester, true).toFirstResult() } + val res = mobileDeviceRepository.getDevices( + student = student, + semester = semester, + forceRefresh = true, + ).toFirstResult() // verify Assert.assertEquals(null, res.errorOrNull) Assert.assertEquals(1, res.dataOrNull?.size) coVerify { sdk.getRegisteredDevices() } coVerify { mobileDeviceDb.loadAll(1) } - coVerify { mobileDeviceDb.insertAll(match { it.isEmpty() }) } coVerify { - mobileDeviceDb.deleteAll(match { - it.size == 1 && it[0] == remoteList.mapToEntities(student)[1] - }) + mobileDeviceDb.removeOldAndSaveNew( + oldItems = match { + it.size == 1 && it[0] == remoteList.mapToEntities(student)[1] + }, + newItems = match { it.isEmpty() }, + ) } } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/RecipientLocalTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/RecipientLocalTest.kt index ae73a795..e608cafb 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/RecipientLocalTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/RecipientLocalTest.kt @@ -69,7 +69,12 @@ class RecipientLocalTest { @Test fun `load recipients when items already in database`() { // prepare - coEvery { recipientDb.loadAll(io.github.wulkanowy.data.db.entities.MailboxType.UNKNOWN, "v4") } returnsMany listOf( + coEvery { + recipientDb.loadAll( + io.github.wulkanowy.data.db.entities.MailboxType.UNKNOWN, + "v4" + ) + } returnsMany listOf( remoteList.mapToEntities("v4"), remoteList.mapToEntities("v4") ) @@ -108,8 +113,7 @@ class RecipientLocalTest { emptyList(), remoteList.mapToEntities("v4") ) - coEvery { recipientDb.insertAll(any()) } returns listOf(1, 2, 3) - coEvery { recipientDb.deleteAll(any()) } just Runs + coEvery { recipientDb.removeOldAndSaveNew(any(), any()) } just Runs // execute val res = runBlocking { @@ -123,8 +127,12 @@ class RecipientLocalTest { // verify assertEquals(3, res.size) coVerify { sdk.getRecipients("v4") } - coVerify { recipientDb.loadAll(io.github.wulkanowy.data.db.entities.MailboxType.UNKNOWN, "v4") } - coVerify { recipientDb.insertAll(match { it.isEmpty() }) } - coVerify { recipientDb.deleteAll(match { it.isEmpty() }) } + coVerify { + recipientDb.loadAll( + io.github.wulkanowy.data.db.entities.MailboxType.UNKNOWN, + "v4" + ) + } + coVerify { recipientDb.removeOldAndSaveNew(match { it.isEmpty() }, match { it.isEmpty() }) } } } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/SemesterRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/SemesterRepositoryTest.kt index 31098d2e..96db8a79 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/SemesterRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/SemesterRepositoryTest.kt @@ -50,13 +50,16 @@ class SemesterRepositoryTest { coEvery { semesterDb.loadAll(student.studentId, student.classId) } returns emptyList() coEvery { sdk.getSemesters() } returns semesters - coEvery { semesterDb.deleteAll(any()) } just Runs - coEvery { semesterDb.insertSemesters(any()) } returns emptyList() + coEvery { semesterDb.removeOldAndSaveNew(any(), any()) } just Runs runBlocking { semesterRepository.getSemesters(student) } - coVerify { semesterDb.insertSemesters(semesters.mapToEntities(student.studentId)) } - coVerify { semesterDb.deleteAll(emptyList()) } + coVerify { + semesterDb.removeOldAndSaveNew( + oldItems = emptyList(), + newItems = semesters.mapToEntities(student.studentId), + ) + } } @Test @@ -71,12 +74,17 @@ class SemesterRepositoryTest { getSemesterPojo(123, 2, now().minusMonths(3), now()) ) - coEvery { semesterDb.loadAll(student.studentId, student.classId) } returns badSemesters.mapToEntities(student.studentId) + coEvery { + semesterDb.loadAll( + student.studentId, + student.classId + ) + } returns badSemesters.mapToEntities(student.studentId) coEvery { sdk.getSemesters() } returns goodSemesters - coEvery { semesterDb.deleteAll(any()) } just Runs - coEvery { semesterDb.insertSemesters(any()) } returns listOf() + coEvery { semesterDb.removeOldAndSaveNew(any(), any()) } just Runs - val items = runBlocking { semesterRepository.getSemesters(student.copy(loginMode = Sdk.Mode.HEBE.name)) } + val items = + runBlocking { semesterRepository.getSemesters(student.copy(loginMode = Sdk.Mode.HEBE.name)) } assertEquals(2, items.size) assertEquals(0, items[0].diaryId) } @@ -99,8 +107,7 @@ class SemesterRepositoryTest { goodSemesters.mapToEntities(student.studentId) ) coEvery { sdk.getSemesters() } returns goodSemesters - coEvery { semesterDb.deleteAll(any()) } just Runs - coEvery { semesterDb.insertSemesters(any()) } returns listOf() + coEvery { semesterDb.removeOldAndSaveNew(any(), any()) } just Runs val items = semesterRepository.getSemesters( student = student.copy(loginMode = Sdk.Mode.SCRAPPER.name) @@ -157,13 +164,16 @@ class SemesterRepositoryTest { coEvery { semesterDb.loadAll(student.studentId, student.classId) } returns emptyList() coEvery { sdk.getSemesters() } returns semesters - coEvery { semesterDb.deleteAll(any()) } just Runs - coEvery { semesterDb.insertSemesters(any()) } returns listOf() + coEvery { semesterDb.removeOldAndSaveNew(any(), any()) } just Runs runBlocking { semesterRepository.getSemesters(student, refreshOnNoCurrent = true) } - coVerify { semesterDb.deleteAll(emptyList()) } - coVerify { semesterDb.insertSemesters(semesters.mapToEntities(student.studentId)) } + coVerify { + semesterDb.removeOldAndSaveNew( + oldItems = emptyList(), + newItems = semesters.mapToEntities(student.studentId), + ) + } } @Test @@ -181,12 +191,17 @@ class SemesterRepositoryTest { getSemesterPojo(2, 2, now().plusMonths(5), now().plusMonths(11)), ) - coEvery { semesterDb.loadAll(student.studentId, student.classId) } returns semestersWithNoCurrent + coEvery { + semesterDb.loadAll( + student.studentId, + student.classId + ) + } returns semestersWithNoCurrent coEvery { sdk.getSemesters() } returns newSemesters - coEvery { semesterDb.deleteAll(any()) } just Runs - coEvery { semesterDb.insertSemesters(any()) } returns listOf() + coEvery { semesterDb.removeOldAndSaveNew(any(), any()) } just Runs - val items = runBlocking { semesterRepository.getSemesters(student, refreshOnNoCurrent = true) } + val items = + runBlocking { semesterRepository.getSemesters(student, refreshOnNoCurrent = true) } assertEquals(2, items.size) } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt index 92ad01b1..2a61f99c 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt @@ -108,8 +108,7 @@ class TimetableRepositoryTest { flowOf(remoteList.mapToEntities(semester)), flowOf(remoteList.mapToEntities(semester)) ) - coEvery { timetableDb.insertAll(any()) } returns listOf(1, 2, 3) - coEvery { timetableDb.deleteAll(any()) } just Runs + coEvery { timetableDb.removeOldAndSaveNew(any(), any()) } just Runs coEvery { timetableAdditionalDao.loadAll( @@ -119,12 +118,10 @@ class TimetableRepositoryTest { end = endDate ) } returns flowOf(listOf()) - coEvery { timetableAdditionalDao.deleteAll(emptyList()) } just Runs - coEvery { timetableAdditionalDao.insertAll(emptyList()) } returns listOf(1, 2, 3) + coEvery { timetableAdditionalDao.removeOldAndSaveNew(any(), any()) } just Runs coEvery { timetableHeaderDao.loadAll(1, 1, startDate, endDate) } returns flowOf(listOf()) - coEvery { timetableHeaderDao.insertAll(emptyList()) } returns listOf(1, 2, 3) - coEvery { timetableHeaderDao.deleteAll(emptyList()) } just Runs + coEvery { timetableHeaderDao.removeOldAndSaveNew(any(), any()) } just Runs // execute val res = runBlocking { @@ -142,8 +139,12 @@ class TimetableRepositoryTest { assertEquals(2, res.dataOrNull!!.lessons.size) coVerify { sdk.getTimetable(startDate, endDate) } coVerify { timetableDb.loadAll(1, 1, startDate, endDate) } - coVerify { timetableDb.insertAll(match { it.isEmpty() }) } - coVerify { timetableDb.deleteAll(match { it.isEmpty() }) } + coVerify { + timetableDb.removeOldAndSaveNew( + oldItems = match { it.isEmpty() }, + newItems = match { it.isEmpty() }, + ) + } } private fun createTimetableRemote( From b319bb03cd722dedd68682d9d7fae04784358bea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sat, 2 Mar 2024 17:31:44 +0100 Subject: [PATCH 12/43] New Crowdin updates (#2458) --- app/src/main/res/values-cs/strings.xml | 5 +++-- app/src/main/res/values-de/strings.xml | 5 +++-- app/src/main/res/values-pl/strings.xml | 5 +++-- app/src/main/res/values-ru/strings.xml | 5 +++-- app/src/main/res/values-sk/strings.xml | 5 +++-- app/src/main/res/values-uk/strings.xml | 5 +++-- 6 files changed, 18 insertions(+), 12 deletions(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index fbc92e46..48b43ae4 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -98,8 +98,8 @@ Přihlásit se Relace vypršela Relace vypršela. Přihlaste se prosím znovu - Heslo k vašemu účtu bylo změněno. Musíte se znovu přihlásit do Wulkanového - Heslo bylo změněno + Password has expired or been changed + Your account password has expired or been changed. You will need to log in to Wulkanowy again Podpora aplikace Líbí se Vám tato aplikace? Podpořte její vývoj tím, že povolíte neinvazivní reklamy, které můžete kdykoliv vypnout Zapnout reklamy @@ -860,6 +860,7 @@ This account is inactive. Try logging in again Nelze se připojit ke deníku. Servery mohou být přetíženy. Prosím zkuste to znovu později Načítání dat se nezdařilo. Prosím zkuste to znovu později + Your password has expired or been changed. Please log in again Je vyžadována změna hesla pro deník Probíhá údržba deníku UONET+. Zkuste to později znovu Neznámá chyba deniku UONET+. Prosím zkuste to znovu později diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 1f124600..ce3ab0d9 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -98,8 +98,8 @@ Anmelden Die Sitzung ist abgelaufen Die Sitzung ist abgelaufen, bitte loggen Sie sich erneut ein - Your account password has been changed. You need to log in to Wulkanowy again - Password changed + Password has expired or been changed + Your account password has expired or been changed. You will need to log in to Wulkanowy again Anwendungsunterstützung Gefällt Ihnen diese App? Unterstützen Sie ihre Entwicklung, indem Sie nicht-invasive Werbung aktivieren, die Sie jederzeit deaktivieren können Werbung aktivieren @@ -766,6 +766,7 @@ This account is inactive. Try logging in again Registrierungsverbindung fehlgeschlagen. Server können überlastet sein. Bitte versuchen Sie es später noch einmal Das Laden der Daten ist fehlgeschlagen. Bitte versuchen Sie es später noch einmal + Your password has expired or been changed. Please log in again Passwortänderung für Registrierung erforderlich Wartung im Gange UONET + Klassenbuch. Versuchen Sie es später noch einmal Unbekannter UONET + Registerfehler. Versuchen Sie es später erneut diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 597d843d..a193da1b 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -98,8 +98,8 @@ Zaloguj się Sesja wygasła Sesja wygasła, zaloguj się ponownie - Hasło do Twojego konta zostało zmienione. Musisz zalogować się ponownie do Wulkanowego - Hasło zostało zmienione + Hasło wygasło lub zostało zmienione + Hasło do twojego konta wygasło lub zostało zmienione. Musisz zalogować się ponownie do Wulkanowego Wparcie aplikacji Podoba Ci się ta aplikacja? Wspieraj jej rozwój poprzez włączenie nieinwazyjnych reklam, które możesz wyłączyć w dowolnym momencie Włącz reklamy @@ -860,6 +860,7 @@ Konto jest nieaktywne. Spróbuj zalogować się ponownie Nie udało się połączyć z dziennikiem. Serwery mogą być przeciążone. Spróbuj ponownie później Ładowanie danych nie powiodło się. Spróbuj ponownie później + Twoje hasło wygasło lub zostało zmienione. Zaloguj się ponownie Wymagana zmiana hasła do dziennika Trwa przerwa techniczna dziennika UONET+. Spróbuj ponownie później Nieznany błąd dziennika UONET+. Spróbuj ponownie później diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 46a19c71..590dc13d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -98,8 +98,8 @@ Войти Сеанс истёк Сеанс истёк, авторизуйтесь снова - Your account password has been changed. You need to log in to Wulkanowy again - Password changed + Password has expired or been changed + Your account password has expired or been changed. You will need to log in to Wulkanowy again Поддержка приложения Вам нравится это приложение? Поддержите его разработку, включив неинвазивную рекламу, которую можно отключить в любое время Включить рекламу @@ -860,6 +860,7 @@ This account is inactive. Try logging in again Не удалось подключиться к дневнику. Возможно, сервера перегружены, повторите попытку позже Не удалось загрузить данные, повторите попытку позже + Your password has expired or been changed. Please log in again Необходимо изменить пароль дневника UONET+ проводит техническое обслуживание, повторите попытку позже Неизвестная ошибка дневника UONET+, повторите попытку позже diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index b63c07c6..93d7559a 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -98,8 +98,8 @@ Prihlásiť sa Relácia vypršala Relácia vypršala. Prihláste sa prosím znovu - Heslo k vášmu účtu bolo zmenené. Musíte sa znovu prihlásiť do Wulkanového - Heslo bolo zmenené + Password has expired or been changed + Your account password has expired or been changed. You will need to log in to Wulkanowy again Podpora aplikácie 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úť Zapnúť reklamy @@ -860,6 +860,7 @@ This account is inactive. Try logging in again Nedá sa pripojiť ku denníku. Servery môžu byť preťažené. Prosím skúste to znova neskôr Načítanie údajov zlyhalo. Skúste neskôr prosím + Your password has expired or been changed. Please log in again Je vyžadovaná zmena hesla pre denník Prebieha údržba denníka UONET+. Skúste to neskôr znova Neznáma chyba dennika UONET+. Prosím skúste to znova neskôr diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 8116c7e4..40fc96c1 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -98,8 +98,8 @@ Увійти Минув термін дії сесії Минув термін дії сесії, авторизуйтеся знову - Пароль вашого облікового запису був змінений. Ви повинні увійти в Wulkanowy знову - Пароль змінено + Password has expired or been changed + Your account password has expired or been changed. You will need to log in to Wulkanowy again Підтримка додатку Вам подобається цей додаток? Підтримайте його розвиток, увімкнувши неінвазивну рекламу, яку ви можете відключити в будь-який час Увімкнути рекламу @@ -860,6 +860,7 @@ This account is inactive. Try logging in again Помилка підключення до щоденнику. Сервери можуть бути перевантажені, спробуйте пізніше Помилка завантаження даних, спробуйте пізніше + Your password has expired or been changed. Please log in again Необхідна зміна пароля щоденника UONET+ проводить технічне осблуговування, спробуйте пізніше Невідома помилка щоденника UONET+, спробуйте пізніше From 3bab883a5692539e27d6addc45f3630a5b6f5770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sat, 2 Mar 2024 19:49:08 +0100 Subject: [PATCH 13/43] Add a message explaining the reason for the captcha to the captcha dialog (#2459) --- .../io/github/wulkanowy/data/DataModule.kt | 23 +++++++++------- .../ui/modules/captcha/CaptchaDialog.kt | 5 ++++ .../utils/WebkitCookieManagerProxy.kt | 10 ++++--- app/src/main/res/layout/dialog_captcha.xml | 27 ++++++++++++++++--- app/src/main/res/values/strings.xml | 3 ++- 5 files changed, 51 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/data/DataModule.kt b/app/src/main/java/io/github/wulkanowy/data/DataModule.kt index 6b6c9d32..50d6c8f9 100644 --- a/app/src/main/java/io/github/wulkanowy/data/DataModule.kt +++ b/app/src/main/java/io/github/wulkanowy/data/DataModule.kt @@ -38,17 +38,20 @@ internal class DataModule { @Singleton @Provides - fun provideSdk(chuckerInterceptor: ChuckerInterceptor, remoteConfig: RemoteConfigHelper) = - Sdk().apply { - androidVersion = android.os.Build.VERSION.RELEASE - buildTag = android.os.Build.MODEL - userAgentTemplate = remoteConfig.userAgentTemplate - setSimpleHttpLogger { Timber.d(it) } - setAdditionalCookieManager(WebkitCookieManagerProxy()) + fun provideSdk( + chuckerInterceptor: ChuckerInterceptor, + remoteConfig: RemoteConfigHelper, + webkitCookieManagerProxy: WebkitCookieManagerProxy, + ) = Sdk().apply { + androidVersion = android.os.Build.VERSION.RELEASE + buildTag = android.os.Build.MODEL + userAgentTemplate = remoteConfig.userAgentTemplate + setSimpleHttpLogger { Timber.d(it) } + setAdditionalCookieManager(webkitCookieManagerProxy) - // for debug only - addInterceptor(chuckerInterceptor, network = true) - } + // for debug only + addInterceptor(chuckerInterceptor, network = true) + } @Singleton @Provides diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/captcha/CaptchaDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/captcha/CaptchaDialog.kt index ed8293a9..98b4fda7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/captcha/CaptchaDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/captcha/CaptchaDialog.kt @@ -13,6 +13,7 @@ import io.github.wulkanowy.R import io.github.wulkanowy.databinding.DialogCaptchaBinding import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.ui.base.BaseDialogFragment +import io.github.wulkanowy.utils.WebkitCookieManagerProxy import timber.log.Timber import javax.inject.Inject @@ -22,6 +23,9 @@ class CaptchaDialog : BaseDialogFragment() { @Inject lateinit var sdk: Sdk + @Inject + lateinit var webkitCookieManagerProxy: WebkitCookieManagerProxy + private var webView: WebView? = null companion object { @@ -80,6 +84,7 @@ class CaptchaDialog : BaseDialogFragment() { } override fun onDestroy() { + webkitCookieManagerProxy.webkitCookieManager?.flush() webView?.destroy() super.onDestroy() } diff --git a/app/src/main/java/io/github/wulkanowy/utils/WebkitCookieManagerProxy.kt b/app/src/main/java/io/github/wulkanowy/utils/WebkitCookieManagerProxy.kt index 3d41c711..4d2dde78 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/WebkitCookieManagerProxy.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/WebkitCookieManagerProxy.kt @@ -5,17 +5,21 @@ import java.net.CookiePolicy import java.net.CookieStore import java.net.HttpCookie import java.net.URI +import javax.inject.Inject +import javax.inject.Singleton import android.webkit.CookieManager as WebkitCookieManager 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] */ - private fun getWebkitCookieManager(): WebkitCookieManager? { + private fun getCookieManager(): WebkitCookieManager? { return try { WebkitCookieManager.getInstance() } catch (e: AndroidRuntimeException) { diff --git a/app/src/main/res/layout/dialog_captcha.xml b/app/src/main/res/layout/dialog_captcha.xml index 539aa0cc..019d8932 100644 --- a/app/src/main/res/layout/dialog_captcha.xml +++ b/app/src/main/res/layout/dialog_captcha.xml @@ -7,15 +7,18 @@ tools:context=".ui.modules.captcha.CaptchaDialog"> + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0" /> + + + + + app:layout_constraintTop_toBottomOf="@id/captcha_webview" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 266c3522..2775365d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -848,7 +848,8 @@ - Verification is in progress. Wait… + VULCAN\'s website requires verification + Why am I seeing this?\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 Verified successfully From a0a0b8dea6e70b32e310158caeee317026326bb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sat, 2 Mar 2024 20:37:36 +0100 Subject: [PATCH 14/43] New Crowdin updates (#2460) --- app/src/main/res/values-cs/strings.xml | 27 +++++++++++++------------- app/src/main/res/values-de/strings.xml | 3 ++- app/src/main/res/values-pl/strings.xml | 3 ++- app/src/main/res/values-ru/strings.xml | 3 ++- app/src/main/res/values-sk/strings.xml | 27 +++++++++++++------------- app/src/main/res/values-uk/strings.xml | 27 +++++++++++++------------- 6 files changed, 48 insertions(+), 42 deletions(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 48b43ae4..c3c691c7 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -56,7 +56,7 @@ Neplatný e-mail Místo e-mailu použijte přiřazené přihlašovací údaje Použijte přiřazené přihlašovací nebo e-mail v @%1$s - Invalid domain suffix + Neplatná přípona domény Neplatný symbol. Pokud jej nemůžete najít, kontaktujte školu Nevymýšlejte si! Pokud symbol nemůžete najít, kontaktujte školu Žák nebyl nalezen. Zkontrolujte správnost symbolu a vybrané varianty deníku UONET+ @@ -98,8 +98,8 @@ Přihlásit se Relace vypršela Relace vypršela. Přihlaste se prosím znovu - Password has expired or been changed - Your account password has expired or been changed. You will need to log in to Wulkanowy again + Heslo vypršelo nebo bylo změněno + Platnost hesla k vašemu účtu vypršela nebo bylo změněno. Budete se muset znovu přihlásit do Wulkanového Podpora aplikace Líbí se Vám tato aplikace? Podpořte její vývoj tím, že povolíte neinvazivní reklamy, které můžete kdykoliv vypnout Zapnout reklamy @@ -336,10 +336,10 @@ Poslat dále Vybrat vše Odznačit vše - Restore from trash + Obnovit z koše Přesunout do koše Odstranit natrvalo - Message restored successfully + Zpráva úspěšně obnovena Zpráva byla úspěšně odstraněna žák rodič @@ -385,7 +385,7 @@ %1$d vybraných Zprávy odstraněné - Messages restored + Obnovené zprávy Vyberte poštovní schránku Anonymní režim je zapnutý Díky anonymnímu režimu není odesílatel upozorněn, když si zprávu přečtete @@ -852,15 +852,16 @@ Pro provoz aplikace potřebujeme potvrdit vaši identitu. Zadejte PESEL žáka <b>%1$s</b> v níže uvedeném poli Zatím přeskočit - Probíhá ověřování. Počkejte… + VULCAN\'s website requires verification + Why am I seeing this?\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 Úspěšně ověřeno Žádné internetové připojení Vyskytla se chyba. Zkontrolujte hodiny svého zařízení - This account is inactive. Try logging in again + Tento účet je neaktivní. Zkuste se znovu přihlásit Nelze se připojit ke deníku. Servery mohou být přetíženy. Prosím zkuste to znovu později Načítání dat se nezdařilo. Prosím zkuste to znovu později - Your password has expired or been changed. Please log in again + Vaše heslo vypršelo nebo bylo změněno. Přihlaste se znovu Je vyžadována změna hesla pro deník Probíhá údržba deníku UONET+. Zkuste to později znovu Neznámá chyba deniku UONET+. Prosím zkuste to znovu později @@ -871,8 +872,8 @@ Funkce není k dispozici. Přihlaste se v jiném režimu než Mobile API Toto pole je povinné - Mute - Unmute - You have muted this user - You have unmuted this user + Ztlumit + Zrušit ztlumení + Ztlumili jste tohoto uživatele + Zrušili jste ztlumení tohoto uživatele diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index ce3ab0d9..daabc7d8 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -758,7 +758,8 @@ To operate the application, we need to confirm your identity. Please enter the student\'s PESEL <b>%1$s</b> in the field below Skip for now - Verification is in progress. Wait… + VULCAN\'s website requires verification + Why am I seeing this?\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 Verified successfully Keine Internetverbindung diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index a193da1b..33b715d7 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -852,7 +852,8 @@ 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 <b>%1$s</b> w polu poniżej Na razie pomiń - Trwa weryfikacja. Czekaj… + Strona dziennika VULCAN wymaga weryfikacji + Dlaczego to widzę?\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ść Pomyślnie zweryfikowano Brak połączenia z internetem diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 590dc13d..8a5fcc40 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -852,7 +852,8 @@ Для работы приложения нам необходимо подтвердить вашу личность. Введите PESEL учащегося <b>%1$s</b> в поле ниже Пропустить сейчас - Verification is in progress. Wait… + VULCAN\'s website requires verification + Why am I seeing this?\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 Verified successfully Интернет-соединение отсутствует diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 93d7559a..829475d6 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -56,7 +56,7 @@ Neplatný e-mail Namiesto e-mailu použite priradené prihlasovacie údaje Použite priradené prihlasovacie alebo e-mail v @%1$s - Invalid domain suffix + Neplatná prípona domény Neplatný symbol. Pokiaľ ho nemôžete nájsť, kontaktujte školu Nevymýšľajte si! Pokiaľ symbol nemôžete nájsť, kontaktujte školu Žiak nebol nájdený. Skontrolujte správnosť symbolu a vybrané varianty denníka UONET+ @@ -98,8 +98,8 @@ Prihlásiť sa Relácia vypršala Relácia vypršala. Prihláste sa prosím znovu - Password has expired or been changed - Your account password has expired or been changed. You will need to log in to Wulkanowy again + Heslo vypršalo alebo bolo zmenené + Platnosť hesla k vášmu účtu vypršala alebo bolo zmenené. Budete sa musieť znova prihlásiť do Wulkanového Podpora aplikácie 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úť Zapnúť reklamy @@ -336,10 +336,10 @@ Poslať ďalej Vybrať všetko Odznačiť všetko - Restore from trash + Obnoviť z koša Presunúť do koša Odstrániť natrvalo - Message restored successfully + Správa úspešne obnovená Správa bola úspešne odstránená žiak rodič @@ -385,7 +385,7 @@ %1$d vybraných Správy odstránené - Messages restored + Obnovené správy Vyberte poštovú schránku Režim inkognito je zapnutý Vďaka inkognito režimu nie je odosielateľ upozornený, keď si správu prečítate @@ -852,15 +852,16 @@ Na prevádzku aplikácie potrebujeme potvrdiť vašu identitu. Zadajte PESEL žiaka <b>%1$s</b> v nižšie uvedenom poli Zatiaľ preskočiť - Overovanie prebieha. Počkajte… + VULCAN\'s website requires verification + Why am I seeing this?\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 Úspešne overené Žiadne internetové pripojenie Vyskytla sa chyba. Skontrolujte hodiny svojho zariadenia - This account is inactive. Try logging in again + Tento účet je neaktívny. Skúste sa znova prihlásiť Nedá sa pripojiť ku denníku. Servery môžu byť preťažené. Prosím skúste to znova neskôr Načítanie údajov zlyhalo. Skúste neskôr prosím - Your password has expired or been changed. Please log in again + Vaše heslo vypršalo alebo bolo zmenené. Prihláste sa znova Je vyžadovaná zmena hesla pre denník Prebieha údržba denníka UONET+. Skúste to neskôr znova Neznáma chyba dennika UONET+. Prosím skúste to znova neskôr @@ -871,8 +872,8 @@ Funkcia nie je k dispozícii. Prihláste sa v inom režime než Mobile API Toto pole je povinné - Mute - Unmute - You have muted this user - You have unmuted this user + Stlmiť + Zrušiť stlmenie + Stlmili ste tohto používateľa + Zrušili ste stlmenie tohto používateľa diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 40fc96c1..a0d4b6c0 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -56,7 +56,7 @@ Недійсна адреса e-mail Використовуйте призначений логін замість адреси e-mail Використовуйте призначений логін або адресу e-mail в @%1$s - Invalid domain suffix + Невірний суфікс домену Некоректний символ. Якщо ви не можете знайти його, будь ласка, зв\'яжіться зі школою Не вигадуйте! Якщо ви не можете знайти його, будь ласка, зв\'яжіться зі школою Студента не знайдено. Перевірте symbol та обраний тип щоденника UONET+ @@ -98,8 +98,8 @@ Увійти Минув термін дії сесії Минув термін дії сесії, авторизуйтеся знову - Password has expired or been changed - Your account password has expired or been changed. You will need to log in to Wulkanowy again + Термін дії пароля закінчився або його було змінено + Термін дії пароля для вашого облікового запису закінчився або було змінено. Необхідно зайти в Wulkanowy знову Підтримка додатку Вам подобається цей додаток? Підтримайте його розвиток, увімкнувши неінвазивну рекламу, яку ви можете відключити в будь-який час Увімкнути рекламу @@ -336,10 +336,10 @@ Переслати Вибрати всі Відмінити вибір - Restore from trash + Відновити зі смітника Перемістити до кошика Видалити назавжди - Message restored successfully + Повідомлення успішно відновлено Лист було успішно видалено учень родич @@ -385,7 +385,7 @@ %1$d вибрано Листи видалено - Messages restored + Повідомлення відновлені Вибрати поштову скриньку Режим анонімності включено Завдяки режиму анонімності, відправник не буде сповіщений коли ви прочитаєте повідомлення @@ -852,15 +852,16 @@ Для роботи програми нам потрібно підтвердити вашу особу. Будь ласка, введіть число PESEL <b>%1$s</b> студента в поле нижче Поки що пропустити - Верифікація в процесі. Чекайте… + Веб-сайт VULCAN потребує підтвердження + Чому я це бачу?\nСайт реєстру, з якого Wulkanowy завантажує дані, відображає той самий екран, що й вище, тому Wulkanowy також повинен показувати його, щоб мати змогу завантажувати дані з цього сайту. Це неможливо обійти Верифікація завершена Немає з\'єднання з інтернетом Сталася помилка. Перевірте годинник пристрою - This account is inactive. Try logging in again + Цей обліковий запис неактивний. Спробуйте увійти ще раз Помилка підключення до щоденнику. Сервери можуть бути перевантажені, спробуйте пізніше Помилка завантаження даних, спробуйте пізніше - Your password has expired or been changed. Please log in again + Термін дії вашого пароля минув або був змінений. Будь ласка увійдіть знову Необхідна зміна пароля щоденника UONET+ проводить технічне осблуговування, спробуйте пізніше Невідома помилка щоденника UONET+, спробуйте пізніше @@ -871,8 +872,8 @@ Функція недоступна в режимі Mobile API. Увійдіть в інший режим Це поле обовʼязкове - Mute - Unmute - You have muted this user - You have unmuted this user + Вимкнути сповіщення + Ввімкнути сповіщення + Ви ігноруєте цього користувача + Ви не ігноруєте цього користувача From 2bbc157d0361b0f23c1d37b174e7e60f0e3b3c07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sat, 2 Mar 2024 20:45:23 +0100 Subject: [PATCH 15/43] Add some new symbols to symbol autocomplete field (#2461) --- app/src/main/res/values/api_symbols.xml | 276 ++++++++++++++++++++++++ 1 file changed, 276 insertions(+) diff --git a/app/src/main/res/values/api_symbols.xml b/app/src/main/res/values/api_symbols.xml index 4b61db48..510995b9 100644 --- a/app/src/main/res/values/api_symbols.xml +++ b/app/src/main/res/values/api_symbols.xml @@ -1,6 +1,8 @@ + Adamów, powiat łukowski + Aleksandrów, powiat biłgorajski Andrychów Augustów Baranów Sandomierski @@ -8,6 +10,8 @@ Bełchatów Bełżyce Biała Podlaska + Biała, powiat prudnicki + Biała, powiat wielunski Biała Rawska Biały Bór Białystok @@ -23,11 +27,17 @@ Boguchwała Boguty-Pianki Bolesławiec + Bolesław, powiat dąbrowski Braniewo Brodnica + Brodnica, powiat śremski + Brody, powiat starachowicki + Brójce, powiat łódzki wschodni Brwinów Brzeg Brzeski + Powiat brzeski + Brzeźnica, powiat wadowicki Buk Bukowno Busko-Zdrój @@ -36,6 +46,7 @@ Bystrzyca Kłodzka Bytom Bytom Odrzański + CECH bialski Chełm Chełmno Chełmża @@ -44,18 +55,28 @@ Chojnice Chojnów Chorzów + Chrzanów, powiat janowski Ciechanów Cieszyn + Czarna, powiat bieszczadzki + Czarna, powiat dębicki Czarnków Czeladź + Czermin, powiat mielecki + Czermin, powiat pleszewski Czersk Częstochowa Człuchów + Dąbie, powiat krośnieński Dąbrowa Białostocka Dąbrowa Górnicza + Dabrowa, powiat opolski Dąbrowa Tarnowska Dębica Dębno + Dębowiec, powiat jasielski + Dobra, powiat lobeski + Dobre, powiat radziejowski Dobrzeń Wielki Dobrzeń Wielki 2 Dobrzyń Nad Wisłą @@ -67,6 +88,8 @@ Elbląg Ełk Frampol + Fundacja Elementarz + Fundacja Mozaika Garwolin Gdańsk Gdynia @@ -124,6 +147,7 @@ Gmina Brańszczyk Gmina Brąszewice Gmina Brenna + Gmina Brochów Gmina Brok Gmina Brzeg Dolny Gmina Brzeziny @@ -221,6 +245,7 @@ Gmina Działoszyce Gmina Dziemiany Gmina Dzierżoniów + Gmina Dziwnów Gmina Dzwola Gmina Elbląg Gmina Ełk @@ -296,6 +321,7 @@ Gmina Hrubieszów Gmina Huszlew Gmina Hyżne + Gmina Igołomia-Wawrzeńczyce Gmina Imielno Gmina Inowrocław Gmina Irządze @@ -339,6 +365,7 @@ Gmina Kamienica Gmina Kamiennik Gmina Kamionka + Gmina Kampinos Gmina Karczmiska Gmina Kargowa Gmina Karlino @@ -410,6 +437,7 @@ Gmina Krasocin Gmina Krempna Gmina Krokowa + Gmina Krościenko Gmina Krośnice Gmina Krupski Młyn Gmina Kruszwica @@ -477,6 +505,7 @@ Gmina Łopiennik Górny Gmina Łopuszno Gmina Łosice + Gmina Łososina Dolna Gmina Lubań Gmina Lubartów Gmina Lubasz @@ -524,6 +553,7 @@ Gmina Miejsce Piastowe Gmina Miękinia Gmina Mielec + Gmina Mieleszyn Gmina Mielno Gmina Mieszkowice Gmina Milanów @@ -575,6 +605,7 @@ Gmina Nowy Kawęczyn Gmina Nowy Korczyn Gmina Nowy Staw + Gmina Nowy Wiśnicz Gmina Nowy Targ Gmina Nowy Tomyśl Gmina Nozdrzec @@ -587,6 +618,7 @@ Gmina Olszyna Gmina Opatowiec Gmina Orneta + Gmina Orchowo Gmina Osieczna Gmina Osiek Gmina Osiek Jasielski @@ -629,9 +661,12 @@ Gmina Piątnica Gmina Piekoszów Gmina Pieniężno + Gmina Pietrowice Wielkie Gmina Pilchowice + Gmina Pielgrzymka Gmina Pińczów Gmina Pionki + Gmina Piszczac Gmina Płaska Gmina Platerówka Gmina Pleśna @@ -655,6 +690,7 @@ Gmina Popów Gmina Potęgowo Gmina Potok Wielki + Gmina Paradyż Gmina Praszka Gmina Prochowice Gmina Promna @@ -733,6 +769,7 @@ Gmina Sanok Gmina Sawin Gmina Ścinawa + Gmina Secemin Gmina Sędziejowice Gmina Sejny Gmina Sękowa @@ -758,6 +795,7 @@ Gmina Sitno Gmina Skarżysko Kościelne Gmina Skępe + Gmina Skierbieszów Gmina Skierniewice Gmina Skoczów Gmina Skoki @@ -779,6 +817,7 @@ Gmina Sobótka Gmina Sokółka Gmina Solina + Gmina Somonino Gmina Sośnicowice Gmina Sośnie Gmina Sośno @@ -800,11 +839,13 @@ Gmina Stoczek Łukowski Gmina Stopnica Gmina Strawczyn + Gmina Stromiec Gmina Stryków Gmina Stryszawa Gmina Stryszów Gmina Strzałkowo Gmina Strzelce Opolskie + Gmina Strzelce Opolskie 2 Gmina Strzelin Gmina Strzelno Gmina Strzyżewice @@ -846,6 +887,7 @@ Gmina Tarnów Gmina Tarnowiec Gmina Tarnów Opolski + Gmina Tarnów Opolski 2 Gmina Teresin Gmina Tereszpol Gmina Tłuchowo @@ -870,6 +912,7 @@ Gmina Tyrawa Wołoska Gmina Uchanie Gmina Ujazd + Gmina PSP Ujazd Gmina Ulan-Majorat Gmina Ulanów Gmina Ułęż @@ -898,6 +941,7 @@ Gmina Wielgomłyny Gmina Wieliszew Gmina Wielka Nieszawka + Gmina Gmina Wielopole Skrzyńskie Gmina Wieniawa Gmina Wieprz Gmina Wieruszów @@ -937,6 +981,7 @@ Gmina Wolsztyn Gmina Wręczyca Wielka Gmina Wronki + Gmina Wyryki Gmina Wyrzysk Gmina Wysokie Gmina Żabno @@ -980,6 +1025,7 @@ Gmina Żołynia Gmina Żukowice Gmina Żurawica + Gmina Żychlin Gmina Żyraków Gmina Żyrzyn Gmina Żytno @@ -992,9 +1038,11 @@ Górzno Gorzów Śląski Gorzów Wielkopolski + Gorzyce powiat tarnobrzeski Gostynin Grajewo Grodzisk Mazowiecki + Grodzisk Wielkopolski Grudziądz Grybów Gryfino @@ -1002,10 +1050,14 @@ Hel Hrubieszów Inowrocław + IR Tarnów Izbica Kujawska + Jabłonna, powiat legionowski + Jabłonna, powiat lubelski Jabłonowo Pomorskie Janowiec Wielkopolski Janów Lubelski + Janów, powiat sokolski Jarocin Jarosław Jasło @@ -1040,6 +1092,7 @@ Kołobrzeg Koniecpol Konin + Konopnica powiat lubelski Konstancin-Jeziorna Konstantynów Łódzki Koronowo @@ -1060,6 +1113,7 @@ Krosno Krotoszyce Krotoszyn + Krynica Krzeszowice Krzyż Wielkopolski Książ Wielkopolski @@ -1079,12 +1133,14 @@ Lędziny Legionowo Legnica + Leśnica opolska Leszno Lewin Brzeski Lewin Brzeski 2 Leżajsk Limanowa Lipno + Lipno, powiat lipnowski Łódź Łódzkie Łowicz @@ -1095,6 +1151,7 @@ Lubin Lublin Lubliniec + Lubnice, powiat staszowski Lubuskie Łuków Lwówecki @@ -1102,18 +1159,30 @@ Malbork Małopolskie Marki + Maszewo, powiat goleniowski Mazowieckie + MEN + Miasto Toruń Michałowice Miechów Międzyrzec Podlaski Miejska Górka Mielec Milanówek + Ministerstwo Rolnictwa Mińsk Mazowiecki + Ministerstwo Kultury i Dziedzictwa Narodowego Mniszków + Ministerstwo Nauki i Szkolnictwa + Ministerstwo Obrony Narodowej + Ministerstwo Środowiska Mosina + Moszczenica powiat gorlicki + Moszczenica powiat piotrkowski Mrągowo Mrągowski + Ministerstwo Sprawiedliwości + Ministerstwo Spraw Wewnętrznych Mszana Dolna Mszczonów Muszyna @@ -1137,9 +1206,17 @@ Nowy Żmigród Nysa Oborniki Śląskie + Oborniki Wielkopolskie Obrzycko + Oleśnica, powiat olesnicki + Oleśnica, powiat staszowski + Olesno powiat dąbrowski + Olesno powiat oleski Olkusz + Olszanka, powiat brzeski Olsztyn + Opatów, powiat kłobucki + Opatów, powiat opatowski Opinogóra Górna Opoczno Opole @@ -1148,8 +1225,10 @@ Orzesze Osieczna Osiecznica + Osiek, powiat starogardzki Ostróda Ostrołęka + Ostrowiec Świętokrzyski Ostrów Wielkopolski Oświęcim Otwock @@ -1166,6 +1245,7 @@ Pilzno Piotrków Trybunalski Pisz + Piwniczna Płock Płońsk Pniewy @@ -1177,6 +1257,8 @@ Pomorskie Poniec Poręba + Poświętne, powiat opoczyński + Poświętne, powiat wołomiński Powiat aleksandrowski Powiat augustowski Powiat będziński @@ -1217,6 +1299,7 @@ Powiat giżycki Powiat gliwicki Powiat głogowski + Powiat głubczycki Powiat gnieźnieński Powiat gołdapski Powiat goleniowski @@ -1226,6 +1309,8 @@ Powiat gorzowski Powiat gostyński Powiat grajewski + Powiat grodziski, mazowieckie + Powiat grodziski, wielkopolskie Powiat grójecki Powiat gryficki Powiat gryfiński @@ -1293,6 +1378,7 @@ Powiat makowski Powiat malborski Powiat miechowski + Powiat międzyrzecki Powiat mielecki Powiat mikołowski Powiat milicki @@ -1321,17 +1407,21 @@ Powiat olsztyński Powiat opatowski Powiat opoczyński + Powiat opole lubelskie Powiat opolski + Powiat opolski 2 Powiat ostródzki Powiat ostrowiecki Powiat ostrzeszowski Powiat oświęcimski + Powiat ostrowski, mazowieckie Powiat otwocki Powiat pabianicki Powiat piaseczyński Powiat pilski Powiat pińczowski Powiat piotrkowski + Powiat piski, warmińsko-mazurskie Powiat pleszewski Powiat płocki Powiat płoński @@ -1391,6 +1481,7 @@ Powiat suski Powiat świdnicki Powiat świdwiński + Powiat świdnicki w Świdniku Powiat świebodziński Powiat świecki Powiat szamotulski @@ -1403,6 +1494,7 @@ Powiat tatrzański Powiat tczewski Powiat tomaszowski + Powiat tomaszowski, lubelskie Powiat toruński Powiat trzebnicki Powiat tucholski @@ -1444,6 +1536,7 @@ Powiat żyrardowski Powiat żywiecki Poznań + prfrawamaz Proszowice Prudnik Pruszcz Gdański @@ -1461,6 +1554,7 @@ Rabka-Zdrój Raciąż Racibórz + Radków Kłodzki Radom Radomsko Radomyśl Wielki @@ -1468,12 +1562,17 @@ Radziejów Radzionków Radzyń Podlaski + Rakoniewice Rawa Mazowiecka Rawicz Reda + Rejowiec, powiat chełmski + Rogowo, powiat rypiński + Rogowo, powiat żniński Rogóźno Ropczyce Ruda Śląska + Rudnik, powiat raciborski Rumia Rybnik Rychwał @@ -1482,6 +1581,7 @@ Rypin Rzeszów Rzeszów projekt + Rzgów, powiat koniński Sandomierz Sanok Sędziszów Małopolski @@ -1503,14 +1603,19 @@ Sokołów Podlaski Sopot Sosnowiec + spmajkowskarzysko + spteodory Śrem Środa Śląska Środa Wielkopolska Starachowice Stargard Starogard Gdański + starostwokrosnienskie Stary Sącz Staszów + stezycapowiatrycki + stowarzyszenieintegracja Stronie Śląskie Strzyżów Sulejówek @@ -1518,9 +1623,12 @@ Sulmierzyce Swarzędz Świdnica + swidnicapowiatswidnicki + swidnicapowiatzielonogorski Świdnik Świdwin Świeradów-Zdrój + swietajnopowiatszczycienski Świętochłowice Świnoujście Syców @@ -1532,6 +1640,7 @@ Szprotawa Sztum Szubin + szydlowopowiatpilski Tarnobrzeg Tarnów Tarnowskie Góry @@ -1548,6 +1657,7 @@ Turawa Tuszyn Tychy + UG Gołcza Ujazd Ustka Ustroń @@ -1557,6 +1667,20 @@ Wałcz Warmińsko-Mazurskie Warszawa + Warszawa Bemowo + Warszawa Białołęka + Warszawa Bielany + Warszawa Mokotów + Warszawa Praga Południe + Warszawa Śródmiśscie + Warszawa Targówek + Warszawa Ursus + Warszawa Ursynow + Warszawa Wawer + Warszawa Wesoła + Warszawa Włochy + Warszawa Wola + Warszawa Żoliborz Wąsosz Węgrów Wejherowo @@ -1564,6 +1688,10 @@ Wieliczka Wielkopolskie Wieluń + Wierzbica, powiat chełmski + Wierzbica, powiat radomski + Wilków, powiat namysłowski + Wiśniowa, powiat myślenicki Władysławowo Włocławek Włodawa @@ -1580,24 +1708,36 @@ Żagań Zakliczyn Zakopane + Zakrzewo, powiat aleksandrowski Zambrów Zamość Żary Zawidów Zduńska Wola Zduny + ZDZ Warszawa Żelechów + Zespół Szkół PPC Kumarszew Zgierz Zgorzelec Zielona Góra Zielonka + ZKSO 1 Katowice Złotoryja Złotów Żory + ZS2 Lubin + ZSK Sieradz + ZSKZ Kwidzyn + ZSKZ Sochaczew + ZSP Stare Koźle + ZST powiat opoczyński Zwoleń Żyrardów + adamowpowiatlukowski + aleksandrowpowiatbilgorajski andrychow augustow baranowsandomierski @@ -1605,6 +1745,8 @@ belchatow belzyce bialapodlaska + bialapowiatprudnicki + bialapowiatwielunski bialarawska bialybor bialystok @@ -1620,11 +1762,17 @@ boguchwala bogutypianki boleslawiec + boleslawpowiatdabrowski braniewo brodnica + brodnicapowiatsremski + brodypowiatstarachowicki + brojcepowiatlodzkiwsch brwinow brzeg brzeski + brzeskipowiat + brzeznicapowiatwadowicki buk bukowno buskozdroj @@ -1633,6 +1781,7 @@ bystrzycaklodzka bytom bytomodrzanski + cechbialski chelm chelmno chelmza @@ -1641,18 +1790,28 @@ chojnice chojnow chorzow + chrzanowpowiatjanowski ciechanow cieszyn + czarnapowiatbieszczadzki + czarnapowiatdebicki czarnkow czeladz + czerminpowiatmielecki + czerminpowiatpleszewski czersk czestochowa czluchow + dabiepowiatkrosnienski dabrowabialostocka dabrowagornicza + dabrowapowiatopolski dabrowatarnowska debica debno + debowiecpowiatjasielski + dobrapowiatlobeski + dobrepowiatradziejowski dobrzenwielki dobrzenwielki2 dobrzynnadwisla @@ -1664,6 +1823,8 @@ elblag elk frampol + fundacjaelementarz + fundacjamozaika garwolin gdansk gdynia @@ -1721,6 +1882,7 @@ gminabranszczyk gminabraszewice gminabrenna + gminabrochow gminabrok gminabrzegdolny gminabrzeziny @@ -1818,6 +1980,7 @@ gminadzialoszyce gminadziemiany gminadzierzoniow + gminadziwnow gminadzwola gminaelblag gminaelk @@ -1893,6 +2056,7 @@ gminahrubieszow gminahuszlew gminahyzne + gminaiglomniawawrzenczyce gminaimielno gminainowroclaw gminairzadze @@ -1936,6 +2100,7 @@ gminakamienica gminakamiennik gminakamionka + gminakampinos gminakarczmiska gminakargowa gminakarlino @@ -2007,6 +2172,7 @@ gminakrasocin gminakrempna gminakrokowa + gminakroscienko gminakrosnice gminakrupskimlyn gminakruszwica @@ -2074,6 +2240,7 @@ gminalopiennikgorny gminalopuszno gminalosice + gminalososinadolna gminaluban gminalubartow gminalubasz @@ -2121,6 +2288,7 @@ gminamiejscepiastowe gminamiekinia gminamielec + gminamieleszyn gminamielno gminamieszkowice gminamilanow @@ -2172,6 +2340,7 @@ gminanowykaweczyn gminanowykorczyn gminanowystaw + gminanowyswisnicz gminanowytarg gminanowytomysl gminanozdrzec @@ -2184,6 +2353,7 @@ gminaolszyna gminaopatowiec gminaorneta + gminaorochowo gminaosieczna gminaosiek gminaosiekjasielski @@ -2226,9 +2396,12 @@ gminapiatnica gminapiekoszow gminapieniezno + gminapietrowicewlk gminapilchowice + gminapilelgrzymka gminapinczow gminapionki + gminapiszac gminaplaska gminaplaterowka gminaplesna @@ -2252,6 +2425,7 @@ gminapopow gminapotegowo gminapotokwielki + gminapradyz gminapraszka gminaprochowice gminapromna @@ -2330,6 +2504,7 @@ gminasanok gminasawin gminascinawa + gminasecemin gminasedziejowice gminasejny gminasekowa @@ -2355,6 +2530,7 @@ gminasitno gminaskarzyskokoscielne gminaskepe + gminaskierbieszow gminaskierniewice gminaskoczow gminaskoki @@ -2376,6 +2552,7 @@ gminasobotka gminasokolka gminasolina + gminasomonino gminasosnicowice gminasosnie gminasosno @@ -2397,11 +2574,13 @@ gminastoczeklukowski gminastopnica gminastrawczyn + gminastromiec gminastrykow gminastryszawa gminastryszow gminastrzalkowo gminastrzelceopolskie + gminastrzelceopolskie2 gminastrzelin gminastrzelno gminastrzyzewice @@ -2443,6 +2622,7 @@ gminatarnow gminatarnowiec gminatarnowopolski + gminatarnowopolski2 gminateresin gminatereszpol gminatluchowo @@ -2467,6 +2647,7 @@ gminatyrawawoloska gminauchanie gminaujazd + gminaujazdpsp gminaulanmajorat gminaulanow gminaulez @@ -2495,6 +2676,7 @@ gminawielgomlyny gminawieliszew gminawielkanieszawka + gminawielopoleskrzynskie gminawieniawa gminawieprz gminawieruszow @@ -2534,6 +2716,7 @@ gminawolsztyn gminawreczycawielka gminawronki + gminawyrki gminawyrzysk gminawysokie gminazabno @@ -2577,6 +2760,7 @@ gminazolynia gminazukowice gminazurawica + gminazychlin gminazyrakow gminazyrzyn gminazytno @@ -2589,9 +2773,11 @@ gorzno gorzowslaski gorzowwielkopolski + gorzycepowiattarnobrzeski gostynin grajewo grodziskmazowiecki + grodziskwielkopolski grudziadz grybow gryfino @@ -2599,10 +2785,14 @@ hel hrubieszow inowroclaw + irtarnow izbicakujawska + jablonnapowiatlegionowski + jablonnapowiatlubelski jablonowopomorskie janowiecwielkopolski janowlubelski + janowpowiatsokolski jarocin jaroslaw jaslo @@ -2637,6 +2827,7 @@ kolobrzeg koniecpol konin + konopnicapowiatlubelski konstancinjeziorna konstantynowlodzki koronowo @@ -2657,6 +2848,7 @@ krosno krotoszyce krotoszyn + krynica krzeszowice krzyzwielkopolski ksiazwielkopolski @@ -2676,12 +2868,14 @@ ledziny legionowo legnica + lesnicaopolska leszno lewinbrzeski lewinbrzeski2 lezajsk limanowa lipno + lipnopowiatlipnowski lodz lodzkie lowicz @@ -2692,6 +2886,7 @@ lubin lublin lubliniec + lubnicepowiatstaszowski lubuskie lukow lwowecki @@ -2699,18 +2894,30 @@ malbork malopolskie marki + maszewopowiatgoleniowski mazowieckie + men + miastotorun michalowice miechow miedzyrzecpodlaski miejskagorka mielec milanowek + minrol minskmazowiecki + mkdn mniszkow + mnsw + mon + mos mosina + moszczenicapowiatgorlicki + moszczenicapowiatpiotrkowski mragowo mragowski + ms + msw mszanadolna mszczonow muszyna @@ -2734,9 +2941,17 @@ nowyzmigrod nysa obornikislaskie + obornikiwielkopolskie obrzycko + olesnicapowiatolesnicki + olesnicapowiatstaszowski + olesnopowiatdabrowski + olesnopowiatoleski olkusz + olszankapowiatbrzeski olsztyn + opatowpowiatklobucki + opatowpowiatopatowski opinogoragorna opoczno opole @@ -2745,8 +2960,10 @@ orzesze osieczna osiecznica + osiekpowiatstarogardzki ostroda ostroleka + ostrowiecsw ostrowwielkopolski oswiecim otwock @@ -2763,6 +2980,7 @@ pilzno piotrkowtrybunalski pisz + piwniczna plock plonsk pniewy @@ -2774,6 +2992,8 @@ pomorskie poniec poreba + poswietnepowiatopoczynski + poswietnepowiatwolominski powiataleksandrowski powiataugustowski powiatbedzinski @@ -2814,6 +3034,7 @@ powiatgizycki powiatgliwicki powiatglogowski + powiatglubczycki powiatgnieznienski powiatgoldapski powiatgoleniowski @@ -2823,6 +3044,8 @@ powiatgorzowski powiatgostynski powiatgrajewski + powiatgrodziskimazowieckie + powiatgrodziskiwielkopolskie powiatgrojecki powiatgryficki powiatgryfinski @@ -2890,6 +3113,7 @@ powiatmakowski powiatmalborski powiatmiechowski + powiatmiedzyrzecki powiatmielecki powiatmikolowski powiatmilicki @@ -2918,17 +3142,21 @@ powiatolsztynski powiatopatowski powiatopoczynski + powiatopolelubelskie powiatopolski + powiatopolski2 powiatostrodzki powiatostrowiecki powiatostrzeszowski powiatoswiecimski + powiatostrowskimazowieckie powiatotwocki powiatpabianicki powiatpiaseczynski powiatpilski powiatpinczowski powiatpiotrkowski + powiatpiskiwarminskomazurskie powiatpleszewski powiatplocki powiatplonski @@ -2988,6 +3216,7 @@ powiatsuski powiatswidnicki powiatswidwinski + powiatswidnickiwswidniku powiatswiebodzinski powiatswiecki powiatszamotulski @@ -3000,6 +3229,7 @@ powiattatrzanski powiattczewski powiattomaszowski + powiattomaszowskilubelskie powiattorunski powiattrzebnicki powiattucholski @@ -3041,6 +3271,7 @@ powiatzyrardowski powiatzywiecki poznan + prfrawamaz proszowice prudnik pruszczgdanski @@ -3058,6 +3289,7 @@ rabkazdroj raciaz raciborz + radkowklodzki radom radomsko radomyslwielki @@ -3065,12 +3297,17 @@ radziejow radzionkow radzynpodlaski + rakoniewice rawamazowiecka rawicz reda + rejowiecpowiatchelmski + rogowopowiatrypinski + rogowopowiatzninski rogozno ropczyce rudaslaska + rudnikpowiatraciborski rumia rybnik rychwal @@ -3079,6 +3316,7 @@ rypin rzeszow rzeszowprojekt + rzgowpowiatkoninski sandomierz sanok sedziszowmalopolski @@ -3100,14 +3338,19 @@ sokolowpodlaski sopot sosnowiec + spmajkowskarzysko + spteodory srem srodaslaska srodawielkopolska starachowice stargard starogardgdanski + starostwokrosnienskie starysacz staszow + stezycapowiatrycki + stowarzyszenieintegracja stronieslaskie strzyzow sulejowek @@ -3115,9 +3358,12 @@ sulmierzyce swarzedz swidnica + swidnicapowiatswidnicki + swidnicapowiatzielonogorski swidnik swidwin swieradowzdroj + swietajnopowiatszczycienski swietochlowice swinoujscie sycow @@ -3129,6 +3375,7 @@ szprotawa sztum szubin + szydlowopowiatpilski tarnobrzeg tarnow tarnowskiegory @@ -3145,6 +3392,7 @@ turawa tuszyn tychy + uggolcza ujazd ustka ustron @@ -3154,6 +3402,20 @@ walcz warminskomazurskie warszawa + warszawabemowo + warszawabialoleka + warszawabielany + warszawamokotow + warszawapragapoludnie + warszawasrodmiescie + warszawatargowek + warszawaursus + warszawaursynow + warszawawawer + warszawawesola + warszawawlochy + warszawawola + warszawazoliborz wasosz wegrow wejherowo @@ -3161,6 +3423,10 @@ wieliczka wielkopolskie wielun + wierzbicapowiatchelmski + wierzbicapowiatradomski + wilkowpowiatnamyslowski + wisniowapowiatmyslenicki wladyslawowo wloclawek wlodawa @@ -3177,20 +3443,30 @@ zagan zakliczyn zakopane + zakrzewopowiataleksandrowski zambrow zamosc zary zawidow zdunskawola zduny + zdzwarszawa zelechow + zespolszkolppckumarszew zgierz zgorzelec zielonagora zielonka + zkso1katowice zlotoryja zlotow zory + zs2lubin + zsksieradz + zskzkwidzyn + zskzsochaczew + zspstarekozle + zstpowiatopoczynski zwolen zyrardow From f455064b9d4e2b219033b2500e02dedd08955040 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sat, 2 Mar 2024 21:18:02 +0100 Subject: [PATCH 16/43] Version 2.5.0 --- app/build.gradle | 10 +++++----- app/src/main/play/release-notes/pl-PL/default.txt | 12 ++++++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index f01b9917..6f63715b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,8 +27,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 34 - versionCode 148 - versionName "2.4.2" + versionCode 149 + versionName "2.5.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" @@ -164,8 +164,8 @@ play { defaultToAppBundles = false track = 'production' releaseStatus = ReleaseStatus.IN_PROGRESS - userFraction = 0.99d - updatePriority = 2 + userFraction = 0.20d + updatePriority = 1 enabled.set(false) } @@ -195,7 +195,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.4.2-SNAPSHOT' + implementation 'io.github.wulkanowy:sdk:2.5.0' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index ef6308b6..98c48e15 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -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 -- naprawiliśmy crash przy dodawaniu dodatkowych lekcji -- naprawiliśmy obsługę błędów widżetach +— dodaliśmy wyświetlanie ogłoszeń +— dodaliśmy opcję przywracania wiadomości z kosza +— 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 From 47d8513a7766da92013943a74cdecba65c202956 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sun, 3 Mar 2024 10:35:17 +0100 Subject: [PATCH 17/43] New Crowdin updates (#2464) --- app/src/main/res/values-cs/strings.xml | 4 ++-- app/src/main/res/values-sk/strings.xml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index c3c691c7..58c1d7d3 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -852,8 +852,8 @@ Pro provoz aplikace potřebujeme potvrdit vaši identitu. Zadejte PESEL žáka <b>%1$s</b> v níže uvedeném poli Zatím přeskočit - VULCAN\'s website requires verification - Why am I seeing this?\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 + Webová stránka deníku VULCAN vyžaduje ověření + Proč se mi to zobrazuje?\nWebová stránka deníku, ze které Wulkanowy stahuje data, zobrazuje stejnou obrazovku jako výše, takže Wulkanowy ji musí také zobrazit, aby bylo možné získávat data z této stránky. Nedá se to obejít Úspěšně ověřeno Žádné internetové připojení diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 829475d6..0f521566 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -852,8 +852,8 @@ Na prevádzku aplikácie potrebujeme potvrdiť vašu identitu. Zadajte PESEL žiaka <b>%1$s</b> v nižšie uvedenom poli Zatiaľ preskočiť - VULCAN\'s website requires verification - Why am I seeing this?\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 + Webová stránka denníka VULCAN vyžaduje overenie + Prečo sa mi to zobrazuje?\nWebová stránka denníka, z ktorej Wulkanowy sťahuje dáta, zobrazuje rovnakú obrazovku ako vyššie, takže Wulkanowy ju musí tiež zobraziť, aby bolo možné získavať dáta z tejto stránky. Nedá sa to obísť Úspešne overené Žiadne internetové pripojenie From 0a1f7270b4009244cf8d7e346594ae719c7448db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sun, 3 Mar 2024 11:15:11 +0100 Subject: [PATCH 18/43] Version 2.5.1 --- app/build.gradle | 8 ++++---- .../ui/modules/message/preview/MessagePreviewFragment.kt | 2 +- app/src/main/play/release-notes/pl-PL/default.txt | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 6f63715b..31de5104 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,8 +27,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 34 - versionCode 149 - versionName "2.5.0" + versionCode 150 + versionName "2.5.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" @@ -164,7 +164,7 @@ play { defaultToAppBundles = false track = 'production' releaseStatus = ReleaseStatus.IN_PROGRESS - userFraction = 0.20d + userFraction = 0.50d updatePriority = 1 enabled.set(false) } @@ -195,7 +195,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.5.0' + implementation 'io.github.wulkanowy:sdk:2.5.1' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt index 75778bac..ebdb96a4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt @@ -234,7 +234,7 @@ class MessagePreviewFragment : } override fun onSaveInstanceState(outState: Bundle) { - outState.putSerializable(MESSAGE_ID_KEY, presenter.messageWithAttachments) + outState.putSerializable(MESSAGE_ID_KEY, presenter.messageWithAttachments?.message) super.onSaveInstanceState(outState) } diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index 98c48e15..2a57977f 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,4 +1,4 @@ -Wersja 2.5.0 +Wersja 2.5.1 — dodaliśmy wyświetlanie ogłoszeń — dodaliśmy opcję przywracania wiadomości z kosza From 8f5a210ec752d353328a187b3ebb6541d5e3ec2d Mon Sep 17 00:00:00 2001 From: Michael <5672750+mibac138@users.noreply.github.com> Date: Fri, 8 Mar 2024 20:36:43 +0100 Subject: [PATCH 19/43] Add attendance calculator (#1597) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --------- Co-authored-by: Mikołaj Pich Co-authored-by: Faierbel --- .../java/io/github/wulkanowy/data/Resource.kt | 100 ++++++++++++- .../enums/AttendanceCalculatorSortingMode.kt | 13 ++ .../wulkanowy/data/pojos/AttendanceData.kt | 14 ++ .../AttendanceSummaryRepository.kt | 2 +- .../repositories/PreferencesRepository.kt | 13 ++ .../GetAttendanceCalculatorDataUseCase.kt | 103 +++++++++++++ .../modules/attendance/AttendanceFragment.kt | 6 + .../modules/attendance/AttendancePresenter.kt | 29 +++- .../ui/modules/attendance/AttendanceView.kt | 2 + .../calculator/AttendanceCalculatorAdapter.kt | 63 ++++++++ .../AttendanceCalculatorFragment.kt | 105 +++++++++++++ .../AttendanceCalculatorPresenter.kt | 84 +++++++++++ .../calculator/AttendanceCalculatorView.kt | 29 ++++ .../settings/appearance/AppearanceFragment.kt | 10 ++ .../wulkanowy/utils/AttendanceExtension.kt | 12 +- .../ic_menu_attendance_calculator.xml | 5 + .../layout/fragment_attendance_calculator.xml | 103 +++++++++++++ .../item_attendance_calculator_header.xml | 111 ++++++++++++++ .../res/layout/pref_target_attendance.xml | 140 ++++++++++++++++++ .../main/res/menu/action_menu_attendance.xml | 7 + app/src/main/res/values-pl/strings.xml | 3 + .../main/res/values/preferences_defaults.xml | 2 + app/src/main/res/values/preferences_keys.xml | 2 + .../main/res/values/preferences_values.xml | 11 ++ app/src/main/res/values/strings.xml | 7 + .../res/xml/scheme_preferences_appearance.xml | 19 +++ 26 files changed, 981 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/io/github/wulkanowy/data/enums/AttendanceCalculatorSortingMode.kt create mode 100644 app/src/main/java/io/github/wulkanowy/data/pojos/AttendanceData.kt create mode 100644 app/src/main/java/io/github/wulkanowy/domain/attendance/GetAttendanceCalculatorDataUseCase.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorAdapter.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorFragment.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorPresenter.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorView.kt create mode 100644 app/src/main/res/drawable/ic_menu_attendance_calculator.xml create mode 100644 app/src/main/res/layout/fragment_attendance_calculator.xml create mode 100644 app/src/main/res/layout/item_attendance_calculator_header.xml create mode 100644 app/src/main/res/layout/pref_target_attendance.xml diff --git a/app/src/main/java/io/github/wulkanowy/data/Resource.kt b/app/src/main/java/io/github/wulkanowy/data/Resource.kt index 108b0d58..c698c42d 100644 --- a/app/src/main/java/io/github/wulkanowy/data/Resource.kt +++ b/app/src/main/java/io/github/wulkanowy/data/Resource.kt @@ -1,11 +1,16 @@ package io.github.wulkanowy.data +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map @@ -14,8 +19,10 @@ import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import timber.log.Timber +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds -sealed class Resource { +sealed class Resource { open class Loading : Resource() @@ -64,6 +71,19 @@ fun Resource.mapData(block: (T) -> U) = when (this) { is Resource.Error -> Resource.Error(this.error) } +inline fun Flow>.combineWithResourceData( + flow: Flow, + crossinline block: suspend (T1, T2) -> R +): Flow> = + combine(flow) { resource, inject -> + when (resource) { + is Resource.Success -> Resource.Success(block(resource.data, inject)) + is Resource.Intermediate -> Resource.Intermediate(block(resource.data, inject)) + is Resource.Loading -> Resource.Loading() + is Resource.Error -> Resource.Error(resource.error) + } + } + fun Flow>.logResourceStatus(name: String, showData: Boolean = false) = onEach { val description = when (it) { is Resource.Intermediate -> "intermediate data received" + if (showData) " (data: `${it.data}`)" else "" @@ -74,8 +94,29 @@ fun Flow>.logResourceStatus(name: String, showData: Boolean = fa Timber.i("$name: $description") } -fun Flow>.mapResourceData(block: (T) -> U) = map { - it.mapData(block) +fun Flow>.mapResourceData(block: suspend (T) -> U) = map { + when (it) { + is Resource.Success -> Resource.Success(block(it.data)) + is Resource.Intermediate -> Resource.Intermediate(block(it.data)) + is Resource.Loading -> Resource.Loading() + is Resource.Error -> Resource.Error(it.error) + } +} + +@OptIn(ExperimentalCoroutinesApi::class) +fun Flow>.flatMapResourceData( + inheritIntermediate: Boolean = true, block: suspend (T) -> Flow> +) = flatMapLatest { + when (it) { + is Resource.Success -> block(it.data) + is Resource.Intermediate -> block(it.data).map { newRes -> + if (inheritIntermediate && newRes is Resource.Success) Resource.Intermediate(newRes.data) + else newRes + } + + is Resource.Loading -> flowOf(Resource.Loading()) + is Resource.Error -> flowOf(Resource.Error(it.error)) + } } fun Flow>.onResourceData(block: suspend (T) -> Unit) = onEach { @@ -105,13 +146,13 @@ fun Flow>.onResourceSuccess(block: suspend (T) -> Unit) = onEach } } -fun Flow>.onResourceError(block: (Throwable) -> Unit) = onEach { +fun Flow>.onResourceError(block: suspend (Throwable) -> Unit) = onEach { if (it is Resource.Error) { block(it.error) } } -fun Flow>.onResourceNotLoading(block: () -> Unit) = onEach { +fun Flow>.onResourceNotLoading(block: suspend () -> Unit) = onEach { if (it !is Resource.Loading) { block() } @@ -121,6 +162,55 @@ suspend fun Flow>.toFirstResult() = filter { it !is Resource.Loa suspend fun Flow>.waitForResult() = takeWhile { it is Resource.Loading }.collect() +// Can cause excessive amounts of `Resource.Intermediate` to be emitted. Unless that is desired, +// use `debounceIntermediates` to alleviate this behavior. +inline fun combineResourceFlows( + flows: Iterable>>, +): Flow>> = combine(flows) { items -> + var isIntermediate = false + val data = mutableListOf() + for (item in items) { + when (item) { + is Resource.Success -> data.add(item.data) + is Resource.Intermediate -> { + isIntermediate = true + data.add(item.data) + } + + is Resource.Loading -> return@combine Resource.Loading() + is Resource.Error -> continue + } + } + if (data.isEmpty()) { + // All items have to be errors for this to happen, so just return the first one. + // mapData is functionally useless and exists only to satisfy the type checker + items.first().mapData { listOf(it) } + } else if (isIntermediate) { + Resource.Intermediate(data) + } else { + Resource.Success(data) + } +} + +@OptIn(FlowPreview::class) +fun Flow>.debounceIntermediates(timeout: Duration = 5.seconds) = flow { + var wasIntermediate = false + + emitAll(this@debounceIntermediates.debounce { + if (it is Resource.Intermediate) { + if (!wasIntermediate) { + wasIntermediate = true + Duration.ZERO + } else { + timeout + } + } else { + wasIntermediate = false + Duration.ZERO + } + }) +} + inline fun networkBoundResource( mutex: Mutex = Mutex(), showSavedOnLoading: Boolean = true, diff --git a/app/src/main/java/io/github/wulkanowy/data/enums/AttendanceCalculatorSortingMode.kt b/app/src/main/java/io/github/wulkanowy/data/enums/AttendanceCalculatorSortingMode.kt new file mode 100644 index 00000000..77dd5fc4 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/enums/AttendanceCalculatorSortingMode.kt @@ -0,0 +1,13 @@ +package io.github.wulkanowy.data.enums + +enum class AttendanceCalculatorSortingMode(private val value: String) { + ALPHABETIC("alphabetic"), + ATTENDANCE("attendance_percentage"), + LESSON_BALANCE("lesson_balance"); + + companion object { + fun getByValue(value: String) = + AttendanceCalculatorSortingMode.values() + .find { it.value == value } ?: ALPHABETIC + } +} diff --git a/app/src/main/java/io/github/wulkanowy/data/pojos/AttendanceData.kt b/app/src/main/java/io/github/wulkanowy/data/pojos/AttendanceData.kt new file mode 100644 index 00000000..5810363c --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/pojos/AttendanceData.kt @@ -0,0 +1,14 @@ +package io.github.wulkanowy.data.pojos + +data class AttendanceData( + val subjectName: String, + val lessonBalance: Int, + val presences: Int, + val absences: Int, +) { + val total: Int + get() = presences + absences + + val presencePercentage: Double + get() = if (total == 0) 0.0 else (presences.toDouble() / total) * 100 +} diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt index c6cfc2f6..1129598a 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt @@ -1,12 +1,12 @@ package io.github.wulkanowy.data.repositories import androidx.room.withTransaction +import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.AppDatabase import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities -import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index 64e60a60..4735293c 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt @@ -10,6 +10,7 @@ import com.fredporciuncula.flow.preferences.Serializer import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.enums.AppTheme +import io.github.wulkanowy.data.enums.AttendanceCalculatorSortingMode import io.github.wulkanowy.data.enums.GradeColorTheme import io.github.wulkanowy.data.enums.GradeExpandMode import io.github.wulkanowy.data.enums.GradeSortingMode @@ -41,6 +42,18 @@ class PreferencesRepository @Inject constructor( R.bool.pref_default_attendance_present ) + val targetAttendanceFlow: Flow + get() = flowSharedPref.getInt( + context.getString(R.string.pref_key_attendance_target), + context.resources.getInteger(R.integer.pref_default_attendance_target) + ).asFlow() + + val attendanceCalculatorSortingModeFlow: Flow + get() = flowSharedPref.getString( + context.getString(R.string.pref_key_attendance_calculator_sorting_mode), + context.resources.getString(R.string.pref_default_attendance_calculator_sorting_mode) + ).asFlow().map(AttendanceCalculatorSortingMode::getByValue) + private val gradeAverageModePref: Preference get() = getObjectFlow( R.string.pref_key_grade_average_mode, diff --git a/app/src/main/java/io/github/wulkanowy/domain/attendance/GetAttendanceCalculatorDataUseCase.kt b/app/src/main/java/io/github/wulkanowy/domain/attendance/GetAttendanceCalculatorDataUseCase.kt new file mode 100644 index 00000000..ea68050d --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/domain/attendance/GetAttendanceCalculatorDataUseCase.kt @@ -0,0 +1,103 @@ +package io.github.wulkanowy.domain.attendance + +import io.github.wulkanowy.data.* +import io.github.wulkanowy.data.db.entities.AttendanceSummary +import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.db.entities.Subject +import io.github.wulkanowy.data.enums.AttendanceCalculatorSortingMode +import io.github.wulkanowy.data.enums.AttendanceCalculatorSortingMode.* +import io.github.wulkanowy.data.pojos.AttendanceData +import io.github.wulkanowy.data.repositories.AttendanceSummaryRepository +import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.data.repositories.SubjectRepository +import io.github.wulkanowy.utils.allAbsences +import io.github.wulkanowy.utils.allPresences +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject +import kotlin.math.ceil +import kotlin.math.floor + +class GetAttendanceCalculatorDataUseCase @Inject constructor( + private val subjectRepository: SubjectRepository, + private val attendanceSummaryRepository: AttendanceSummaryRepository, + private val preferencesRepository: PreferencesRepository, +) { + + operator fun invoke( + student: Student, + semester: Semester, + forceRefresh: Boolean, + ): Flow>> = + subjectRepository.getSubjects(student, semester, forceRefresh) + .mapResourceData { subjects -> subjects.sortedBy(Subject::name) } + .combineWithResourceData(preferencesRepository.targetAttendanceFlow, ::Pair) + .flatMapResourceData { (subjects, targetFreq) -> + combineResourceFlows(subjects.map { subject -> + attendanceSummaryRepository.getAttendanceSummary( + student = student, + semester = semester, + subjectId = subject.realId, + forceRefresh = forceRefresh + ).mapResourceData { summaries -> + summaries.toAttendanceData(subject.name, targetFreq) + } + }) + // Every individual combined flow causes separate network requests to update data. + // When there is N child flows, they can cause up to N-1 items to be emitted. Since all + // requests are usually completed in less than 5s, there is no need to emit multiple + // intermediates that will be visible for barely any time. + .debounceIntermediates() + } + .combineWithResourceData(preferencesRepository.attendanceCalculatorSortingModeFlow, List::sortedBy) +} + +private fun List.toAttendanceData(subjectName: String, targetFreq: Int): AttendanceData { + val presences = sumOf { it.allPresences } + val absences = sumOf { it.allAbsences } + return AttendanceData( + subjectName = subjectName, + lessonBalance = calcLessonBalance( + targetFreq.toDouble() / 100, presences, absences + ), + presences = presences, + absences = absences, + ) +} + +private fun calcLessonBalance(targetFreq: Double, presences: Int, absences: Int): Int { + val total = presences + absences + // The `+ 1` is to avoid false positives in close cases. Eg.: + // target frequency 99%, 1 presence. Without the `+ 1` this would be reported shown as + // a positive balance of +1, however that is not actually true as skipping one class + // would make it so that the balance would actually be negative (-98). The `+ 1` + // fixes this and makes sure that in situations like these, it's not reporting incorrect + // balances + return when { + presences / (total + 1f) >= targetFreq -> calcMissingAbsences( + targetFreq, absences, presences + ) + presences / (total + 0f) < targetFreq -> -calcMissingPresences( + targetFreq, absences, presences + ) + else -> 0 + } +} + +private fun calcMissingPresences(targetFreq: Double, absences: Int, presences: Int) = + calcMinRequiredPresencesFor(targetFreq, absences) - presences + +private fun calcMinRequiredPresencesFor(targetFreq: Double, absences: Int) = + ceil((targetFreq / (1 - targetFreq)) * absences).toInt() + +private fun calcMissingAbsences(targetFreq: Double, absences: Int, presences: Int) = + calcMinRequiredAbsencesFor(targetFreq, presences) - absences + +private fun calcMinRequiredAbsencesFor(targetFreq: Double, presences: Int) = + floor((presences * (1 - targetFreq)) / targetFreq).toInt() + +private fun List.sortedBy(mode: AttendanceCalculatorSortingMode) = when (mode) { + ALPHABETIC -> sortedBy(AttendanceData::subjectName) + ATTENDANCE -> sortedByDescending(AttendanceData::presencePercentage) + LESSON_BALANCE -> sortedBy(AttendanceData::lessonBalance) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt index 6e842b4d..07649e43 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt @@ -14,6 +14,7 @@ import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.databinding.DialogExcuseBinding import io.github.wulkanowy.databinding.FragmentAttendanceBinding import io.github.wulkanowy.ui.base.BaseFragment +import io.github.wulkanowy.ui.modules.attendance.calculator.AttendanceCalculatorFragment import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView @@ -134,6 +135,7 @@ class AttendanceFragment : BaseFragment(R.layout.frag override fun onOptionsItemSelected(item: MenuItem): Boolean { return if (item.itemId == R.id.attendanceMenuSummary) presenter.onSummarySwitchSelected() + else if (item.itemId == R.id.attendanceMenuCalculator) presenter.onCalculatorSwitchSelected() else false } @@ -253,6 +255,10 @@ class AttendanceFragment : BaseFragment(R.layout.frag (activity as? MainActivity)?.pushView(AttendanceSummaryFragment.newInstance()) } + override fun openCalculatorView() { + (activity as? MainActivity)?.pushView(AttendanceCalculatorFragment.newInstance()) + } + override fun startActionMode() { actionMode = (activity as MainActivity?)?.startSupportActionMode(actionModeCallback) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt index 82fe69cb..586a41ad 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt @@ -1,16 +1,36 @@ package io.github.wulkanowy.ui.modules.attendance import android.annotation.SuppressLint -import io.github.wulkanowy.data.* +import io.github.wulkanowy.data.Resource import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.flatResourceFlow +import io.github.wulkanowy.data.logResourceStatus +import io.github.wulkanowy.data.mapResourceData +import io.github.wulkanowy.data.onResourceData +import io.github.wulkanowy.data.onResourceError +import io.github.wulkanowy.data.onResourceIntermediate +import io.github.wulkanowy.data.onResourceLoading +import io.github.wulkanowy.data.onResourceNotLoading +import io.github.wulkanowy.data.onResourceSuccess import io.github.wulkanowy.data.repositories.AttendanceRepository import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler -import io.github.wulkanowy.utils.* +import io.github.wulkanowy.utils.AnalyticsHelper +import io.github.wulkanowy.utils.capitalise +import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday +import io.github.wulkanowy.utils.isExcusableOrNotExcused +import io.github.wulkanowy.utils.isHolidays +import io.github.wulkanowy.utils.monday +import io.github.wulkanowy.utils.nextSchoolDay +import io.github.wulkanowy.utils.previousOrSameSchoolDay +import io.github.wulkanowy.utils.previousSchoolDay +import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.utils.toFormattedString import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.onEach import timber.log.Timber @@ -195,6 +215,11 @@ class AttendancePresenter @Inject constructor( return true } + fun onCalculatorSwitchSelected(): Boolean { + view?.openCalculatorView() + return true + } + private fun loadData(forceRefresh: Boolean = false) { Timber.i("Loading attendance data started") diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceView.kt index 2629c217..f51ce7c7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceView.kt @@ -56,6 +56,8 @@ interface AttendanceView : BaseView { fun openSummaryView() + fun openCalculatorView() + fun startSendMessageIntent(date: LocalDate, numbers: String, reason: String) fun startActionMode() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorAdapter.kt new file mode 100644 index 00000000..73c08fd3 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorAdapter.kt @@ -0,0 +1,63 @@ +package io.github.wulkanowy.ui.modules.attendance.calculator + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import io.github.wulkanowy.R +import io.github.wulkanowy.data.pojos.AttendanceData +import io.github.wulkanowy.databinding.ItemAttendanceCalculatorHeaderBinding +import javax.inject.Inject +import kotlin.math.abs +import kotlin.math.roundToInt + +class AttendanceCalculatorAdapter @Inject constructor() : + RecyclerView.Adapter() { + + var items = emptyList() + + override fun getItemCount() = items.size + + override fun onCreateViewHolder( + parent: ViewGroup, viewType: Int + ) = ViewHolder( + ItemAttendanceCalculatorHeaderBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + ) + + @SuppressLint("SetTextI18n") + override fun onBindViewHolder(parent: ViewHolder, position: Int) { + with(parent.binding) { + val item = items[position] + attendanceCalculatorPercentage.text = "${item.presencePercentage.roundToInt()}" + + if (item.lessonBalance > 0) { + attendanceCalculatorSummaryBalance.text = root.context.getString( + R.string.attendance_calculator_summary_balance_positive, + item.lessonBalance + ) + } else if (item.lessonBalance < 0) { + attendanceCalculatorSummaryBalance.text = root.context.getString( + R.string.attendance_calculator_summary_balance_negative, + abs(item.lessonBalance) + ) + } else { + attendanceCalculatorSummaryBalance.text = root.context.getString( + R.string.attendance_calculator_summary_balance_neutral, + ) + } + attendanceCalculatorWarning.isVisible = item.lessonBalance < 0 + attendanceCalculatorTitle.text = item.subjectName + attendanceCalculatorSummaryValues.text = root.context.getString( + R.string.attendance_calculator_summary_values, + item.presences, + item.total + ) + } + } + + class ViewHolder(val binding: ItemAttendanceCalculatorHeaderBinding) : + RecyclerView.ViewHolder(binding.root) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorFragment.kt new file mode 100644 index 00000000..2d566701 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorFragment.kt @@ -0,0 +1,105 @@ +package io.github.wulkanowy.ui.modules.attendance.calculator + +import android.os.Bundle +import android.view.View +import androidx.core.view.isVisible +import androidx.recyclerview.widget.LinearLayoutManager +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R +import io.github.wulkanowy.data.pojos.AttendanceData +import io.github.wulkanowy.databinding.FragmentAttendanceCalculatorBinding +import io.github.wulkanowy.ui.base.BaseFragment +import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.ui.widgets.DividerItemDecoration +import io.github.wulkanowy.utils.getThemeAttrColor +import javax.inject.Inject + +@AndroidEntryPoint +class AttendanceCalculatorFragment : + BaseFragment(R.layout.fragment_attendance_calculator), + AttendanceCalculatorView, MainView.TitledView { + + @Inject + lateinit var presenter: AttendanceCalculatorPresenter + + @Inject + lateinit var attendanceCalculatorAdapter: AttendanceCalculatorAdapter + + override val titleStringId get() = R.string.attendance_title + + companion object { + fun newInstance() = AttendanceCalculatorFragment() + } + + override val isViewEmpty get() = attendanceCalculatorAdapter.items.isEmpty() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentAttendanceCalculatorBinding.bind(view) + messageContainer = binding.attendanceCalculatorRecycler + presenter.onAttachView(this) + } + + override fun initView() { + with(binding.attendanceCalculatorRecycler) { + layoutManager = LinearLayoutManager(context) + adapter = attendanceCalculatorAdapter + addItemDecoration(DividerItemDecoration(context)) + } + + with(binding) { + attendanceCalculatorSwipe.setOnRefreshListener(presenter::onSwipeRefresh) + attendanceCalculatorSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary)) + attendanceCalculatorSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh)) + attendanceCalculatorErrorRetry.setOnClickListener { presenter.onRetry() } + attendanceCalculatorErrorDetails.setOnClickListener { presenter.onDetailsClick() } + } + } + + override fun updateData(data: List) { + with(attendanceCalculatorAdapter) { + items = data + notifyDataSetChanged() + } + } + + override fun clearView() { + with(attendanceCalculatorAdapter) { + items = emptyList() + notifyDataSetChanged() + } + } + + override fun showEmpty(show: Boolean) { + binding.attendanceCalculatorEmpty.isVisible = show + } + + override fun showErrorView(show: Boolean) { + binding.attendanceCalculatorError.isVisible = show + } + + override fun setErrorDetails(message: String) { + binding.attendanceCalculatorErrorMessage.text = message + } + + override fun showProgress(show: Boolean) { + binding.attendanceCalculatorProgress.isVisible = show + } + + override fun enableSwipe(enable: Boolean) { + binding.attendanceCalculatorSwipe.isEnabled = enable + } + + override fun showContent(show: Boolean) { + binding.attendanceCalculatorRecycler.isVisible = show + } + + override fun showRefresh(show: Boolean) { + binding.attendanceCalculatorSwipe.isRefreshing = show + } + + override fun onDestroyView() { + presenter.onDetachView() + super.onDestroyView() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorPresenter.kt new file mode 100644 index 00000000..d292e565 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorPresenter.kt @@ -0,0 +1,84 @@ +package io.github.wulkanowy.ui.modules.attendance.calculator + +import io.github.wulkanowy.data.* +import io.github.wulkanowy.data.repositories.SemesterRepository +import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.domain.attendance.GetAttendanceCalculatorDataUseCase +import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.ui.base.ErrorHandler +import timber.log.Timber +import javax.inject.Inject + +class AttendanceCalculatorPresenter @Inject constructor( + errorHandler: ErrorHandler, + studentRepository: StudentRepository, + private val semesterRepository: SemesterRepository, + private val getAttendanceCalculatorData: GetAttendanceCalculatorDataUseCase, +) : BasePresenter(errorHandler, studentRepository) { + + private lateinit var lastError: Throwable + + override fun onAttachView(view: AttendanceCalculatorView) { + super.onAttachView(view) + view.initView() + Timber.i("Attendance calculator view was initialized") + errorHandler.showErrorMessage = ::showErrorViewOnError + loadData() + } + + fun onSwipeRefresh() { + Timber.i("Force refreshing the attendance calculator") + loadData(forceRefresh = true) + } + + fun onRetry() { + view?.run { + showErrorView(false) + showProgress(true) + } + loadData() + } + + fun onDetailsClick() { + view?.showErrorDetailsDialog(lastError) + } + + private fun loadData(forceRefresh: Boolean = false) { + flatResourceFlow { + val student = studentRepository.getCurrentStudent() + val semester = semesterRepository.getCurrentSemester(student) + getAttendanceCalculatorData(student, semester, forceRefresh) + } + .logResourceStatus("load attendance calculator") + .onResourceData { + view?.run { + showProgress(false) + showErrorView(false) + showContent(it.isNotEmpty()) + showEmpty(it.isEmpty()) + updateData(it) + } + } + .onResourceIntermediate { view?.showRefresh(true) } + .onResourceNotLoading { + view?.run { + enableSwipe(true) + showRefresh(false) + showProgress(false) + } + } + .onResourceError(errorHandler::dispatch) + .launch() + } + + private fun showErrorViewOnError(message: String, error: Throwable) { + view?.run { + if (isViewEmpty) { + lastError = error + setErrorDetails(message) + showErrorView(true) + showEmpty(false) + } else showError(message, error) + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorView.kt new file mode 100644 index 00000000..94e66121 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorView.kt @@ -0,0 +1,29 @@ +package io.github.wulkanowy.ui.modules.attendance.calculator + +import io.github.wulkanowy.data.pojos.AttendanceData +import io.github.wulkanowy.ui.base.BaseView + +interface AttendanceCalculatorView : BaseView { + + val isViewEmpty: Boolean + + fun initView() + + fun showRefresh(show: Boolean) + + fun showContent(show: Boolean) + + fun showProgress(show: Boolean) + + fun enableSwipe(enable: Boolean) + + fun showEmpty(show: Boolean) + + fun showErrorView(show: Boolean) + + fun setErrorDetails(message: String) + + fun updateData(data: List) + + fun clearView() +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt index 3d0c8052..ba234aae 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt @@ -4,6 +4,7 @@ import android.content.SharedPreferences import android.os.Bundle import android.view.View import androidx.preference.PreferenceFragmentCompat +import androidx.preference.SeekBarPreference import com.yariksoffice.lingver.Lingver import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R @@ -36,6 +37,15 @@ class AppearanceFragment : PreferenceFragmentCompat(), override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.scheme_preferences_appearance, rootKey) + val attendanceTargetPref = + findPreference(requireContext().getString(R.string.pref_key_attendance_target))!! + attendanceTargetPref.setOnPreferenceChangeListener { _, newValueObj -> + val newValue = (((newValueObj as Int).toDouble() + 2.5) / 5).toInt() * 5 + attendanceTargetPref.value = + newValue.coerceIn(attendanceTargetPref.min, attendanceTargetPref.max) + + false + } } override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { diff --git a/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt index 397c9595..3cac0b48 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt @@ -10,19 +10,19 @@ import io.github.wulkanowy.sdk.scrapper.attendance.AttendanceCategory * (https://www.vulcan.edu.pl/vulcang_files/user/AABW/AABW-PDF/uonetplus/uonetplus_Frekwencja-liczby-obecnych-nieobecnych.pdf) */ -private inline val AttendanceSummary.allPresences: Double - get() = presence.toDouble() + absenceForSchoolReasons + lateness + latenessExcused +inline val AttendanceSummary.allPresences: Int + get() = presence + absenceForSchoolReasons + lateness + latenessExcused -private inline val AttendanceSummary.allAbsences: Double - get() = absence.toDouble() + absenceExcused +inline val AttendanceSummary.allAbsences: Int + get() = absence + absenceExcused inline val Attendance.isExcusableOrNotExcused: Boolean get() = (excusable || ((absence || lateness) && !excused)) && excuseStatus == null -fun AttendanceSummary.calculatePercentage() = calculatePercentage(allPresences, allAbsences) +fun AttendanceSummary.calculatePercentage() = calculatePercentage(allPresences.toDouble(), allAbsences.toDouble()) fun List.calculatePercentage(): Double { - return calculatePercentage(sumOf { it.allPresences }, sumOf { it.allAbsences }) + return calculatePercentage(sumOf { it.allPresences.toDouble() }, sumOf { it.allAbsences.toDouble() }) } private fun calculatePercentage(presence: Double, absence: Double): Double { diff --git a/app/src/main/res/drawable/ic_menu_attendance_calculator.xml b/app/src/main/res/drawable/ic_menu_attendance_calculator.xml new file mode 100644 index 00000000..8a7d209a --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_attendance_calculator.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/fragment_attendance_calculator.xml b/app/src/main/res/layout/fragment_attendance_calculator.xml new file mode 100644 index 00000000..346c6aec --- /dev/null +++ b/app/src/main/res/layout/fragment_attendance_calculator.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_attendance_calculator_header.xml b/app/src/main/res/layout/item_attendance_calculator_header.xml new file mode 100644 index 00000000..debc7997 --- /dev/null +++ b/app/src/main/res/layout/item_attendance_calculator_header.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/pref_target_attendance.xml b/app/src/main/res/layout/pref_target_attendance.xml new file mode 100644 index 00000000..558b0d36 --- /dev/null +++ b/app/src/main/res/layout/pref_target_attendance.xml @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/action_menu_attendance.xml b/app/src/main/res/menu/action_menu_attendance.xml index bb20c8ec..5c59d239 100644 --- a/app/src/main/res/menu/action_menu_attendance.xml +++ b/app/src/main/res/menu/action_menu_attendance.xml @@ -1,6 +1,13 @@ + Wyłącz wyciszenie Wyciszyleś tego użytkownika Wyłączyłeś wyciszenie tego użytkownika + + Docelowa frekwencja (w %) + Kalkulator frekwencji diff --git a/app/src/main/res/values/preferences_defaults.xml b/app/src/main/res/values/preferences_defaults.xml index 8e6fc7d6..10941889 100644 --- a/app/src/main/res/values/preferences_defaults.xml +++ b/app/src/main/res/values/preferences_defaults.xml @@ -2,6 +2,8 @@ 0 true + 50 + alphabetic only_one_semester false one diff --git a/app/src/main/res/values/preferences_keys.xml b/app/src/main/res/values/preferences_keys.xml index 74af9262..e95c5940 100644 --- a/app/src/main/res/values/preferences_keys.xml +++ b/app/src/main/res/values/preferences_keys.xml @@ -2,6 +2,8 @@ default_menu_index attendance_present + attendance_target + attendance_calculator_sorting_mode app_theme dashboard_tiles grade_color_scheme diff --git a/app/src/main/res/values/preferences_values.xml b/app/src/main/res/values/preferences_values.xml index f56707c8..2475e491 100644 --- a/app/src/main/res/values/preferences_values.xml +++ b/app/src/main/res/values/preferences_values.xml @@ -79,6 +79,17 @@ 0.75 + + Alphabetically + By attendance percentage + By lesson balance + + + alphabetic + attendance_percentage + lesson_balance + + Alphabetically By date diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2775365d..ae6d9140 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -258,6 +258,11 @@ Attendance summary + Attendance calculator + %1$d over target + right on target + %1$d under target + %1$d/%2$d presences Absent for school reasons Excused absence Unexcused absence @@ -715,6 +720,8 @@ Calculated average options Force average calculation by app Show presence + Attendance target + Attendance calculator sorting Theme Grades expanding Show groups next to subjects diff --git a/app/src/main/res/xml/scheme_preferences_appearance.xml b/app/src/main/res/xml/scheme_preferences_appearance.xml index 9c02a491..46a0e6a9 100644 --- a/app/src/main/res/xml/scheme_preferences_appearance.xml +++ b/app/src/main/res/xml/scheme_preferences_appearance.xml @@ -85,6 +85,25 @@ app:iconSpaceReserved="false" app:key="@string/pref_key_attendance_present" app:title="@string/pref_view_present" /> + + Date: Fri, 8 Mar 2024 20:57:26 +0100 Subject: [PATCH 20/43] Update gradle wrapper to 8.6 (#2468) --- .gitignore | 1 + gradle/wrapper/gradle-wrapper.jar | Bin 63721 -> 43462 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 980085e3..c014204d 100644 --- a/.gitignore +++ b/.gitignore @@ -67,6 +67,7 @@ captures/ .idea/discord.xml .idea/migrations.xml .idea/androidTestResultsUserPreferences.xml +.idea/copilot # Keystore files *.jks diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f93135c49b765f8051ef9d0a6055ff8e46073d8..d64cd4917707c1f8861d8cb53dd15194d4248596 100644 GIT binary patch literal 43462 zcma&NWl&^owk(X(xVyW%ySuwf;qI=D6|RlDJ2cR^yEKh!@I- zp9QeisK*rlxC>+~7Dk4IxIRsKBHqdR9b3+fyL=ynHmIDe&|>O*VlvO+%z5;9Z$|DJ zb4dO}-R=MKr^6EKJiOrJdLnCJn>np?~vU-1sSFgPu;pthGwf}bG z(1db%xwr#x)r+`4AGu$j7~u2MpVs3VpLp|mx&;>`0p0vH6kF+D2CY0fVdQOZ@h;A` z{infNyvmFUiu*XG}RNMNwXrbec_*a3N=2zJ|Wh5z* z5rAX$JJR{#zP>KY**>xHTuw?|-Rg|o24V)74HcfVT;WtQHXlE+_4iPE8QE#DUm%x0 zEKr75ur~W%w#-My3Tj`hH6EuEW+8K-^5P62$7Sc5OK+22qj&Pd1;)1#4tKihi=~8C zHiQSst0cpri6%OeaR`PY>HH_;CPaRNty%WTm4{wDK8V6gCZlG@U3$~JQZ;HPvDJcT1V{ z?>H@13MJcCNe#5z+MecYNi@VT5|&UiN1D4ATT+%M+h4c$t;C#UAs3O_q=GxK0}8%8 z8J(_M9bayxN}69ex4dzM_P3oh@ZGREjVvn%%r7=xjkqxJP4kj}5tlf;QosR=%4L5y zWhgejO=vao5oX%mOHbhJ8V+SG&K5dABn6!WiKl{|oPkq(9z8l&Mm%(=qGcFzI=eLu zWc_oCLyf;hVlB@dnwY98?75B20=n$>u3b|NB28H0u-6Rpl((%KWEBOfElVWJx+5yg z#SGqwza7f}$z;n~g%4HDU{;V{gXIhft*q2=4zSezGK~nBgu9-Q*rZ#2f=Q}i2|qOp z!!y4p)4o=LVUNhlkp#JL{tfkhXNbB=Ox>M=n6soptJw-IDI|_$is2w}(XY>a=H52d z3zE$tjPUhWWS+5h=KVH&uqQS=$v3nRs&p$%11b%5qtF}S2#Pc`IiyBIF4%A!;AVoI zXU8-Rpv!DQNcF~(qQnyyMy=-AN~U>#&X1j5BLDP{?K!%h!;hfJI>$mdLSvktEr*89 zdJHvby^$xEX0^l9g$xW-d?J;L0#(`UT~zpL&*cEh$L|HPAu=P8`OQZV!-}l`noSp_ zQ-1$q$R-gDL)?6YaM!=8H=QGW$NT2SeZlb8PKJdc=F-cT@j7Xags+Pr*jPtlHFnf- zh?q<6;)27IdPc^Wdy-mX%2s84C1xZq9Xms+==F4);O`VUASmu3(RlgE#0+#giLh-& zcxm3_e}n4{%|X zJp{G_j+%`j_q5}k{eW&TlP}J2wtZ2^<^E(O)4OQX8FDp6RJq!F{(6eHWSD3=f~(h} zJXCf7=r<16X{pHkm%yzYI_=VDP&9bmI1*)YXZeB}F? z(%QsB5fo*FUZxK$oX~X^69;x~j7ms8xlzpt-T15e9}$4T-pC z6PFg@;B-j|Ywajpe4~bk#S6(fO^|mm1hKOPfA%8-_iGCfICE|=P_~e;Wz6my&)h_~ zkv&_xSAw7AZ%ThYF(4jADW4vg=oEdJGVOs>FqamoL3Np8>?!W#!R-0%2Bg4h?kz5I zKV-rKN2n(vUL%D<4oj@|`eJ>0i#TmYBtYmfla;c!ATW%;xGQ0*TW@PTlGG><@dxUI zg>+3SiGdZ%?5N=8uoLA|$4isK$aJ%i{hECP$bK{J#0W2gQ3YEa zZQ50Stn6hqdfxJ*9#NuSLwKFCUGk@c=(igyVL;;2^wi4o30YXSIb2g_ud$ zgpCr@H0qWtk2hK8Q|&wx)}4+hTYlf;$a4#oUM=V@Cw#!$(nOFFpZ;0lc!qd=c$S}Z zGGI-0jg~S~cgVT=4Vo)b)|4phjStD49*EqC)IPwyeKBLcN;Wu@Aeph;emROAwJ-0< z_#>wVm$)ygH|qyxZaet&(Vf%pVdnvKWJn9`%DAxj3ot;v>S$I}jJ$FLBF*~iZ!ZXE zkvui&p}fI0Y=IDX)mm0@tAd|fEHl~J&K}ZX(Mm3cm1UAuwJ42+AO5@HwYfDH7ipIc zmI;1J;J@+aCNG1M`Btf>YT>~c&3j~Qi@Py5JT6;zjx$cvOQW@3oQ>|}GH?TW-E z1R;q^QFjm5W~7f}c3Ww|awg1BAJ^slEV~Pk`Kd`PS$7;SqJZNj->it4DW2l15}xP6 zoCl$kyEF%yJni0(L!Z&14m!1urXh6Btj_5JYt1{#+H8w?5QI%% zo-$KYWNMJVH?Hh@1n7OSu~QhSswL8x0=$<8QG_zepi_`y_79=nK=_ZP_`Em2UI*tyQoB+r{1QYZCpb?2OrgUw#oRH$?^Tj!Req>XiE#~B|~ z+%HB;=ic+R@px4Ld8mwpY;W^A%8%l8$@B@1m5n`TlKI6bz2mp*^^^1mK$COW$HOfp zUGTz-cN9?BGEp}5A!mDFjaiWa2_J2Iq8qj0mXzk; z66JBKRP{p%wN7XobR0YjhAuW9T1Gw3FDvR5dWJ8ElNYF94eF3ebu+QwKjtvVu4L zI9ip#mQ@4uqVdkl-TUQMb^XBJVLW(-$s;Nq;@5gr4`UfLgF$adIhd?rHOa%D);whv z=;krPp~@I+-Z|r#s3yCH+c1US?dnm+C*)r{m+86sTJusLdNu^sqLrfWed^ndHXH`m zd3#cOe3>w-ga(Dus_^ppG9AC>Iq{y%%CK+Cro_sqLCs{VLuK=dev>OL1dis4(PQ5R zcz)>DjEkfV+MO;~>VUlYF00SgfUo~@(&9$Iy2|G0T9BSP?&T22>K46D zL*~j#yJ?)^*%J3!16f)@Y2Z^kS*BzwfAQ7K96rFRIh>#$*$_Io;z>ux@}G98!fWR@ zGTFxv4r~v)Gsd|pF91*-eaZ3Qw1MH$K^7JhWIdX%o$2kCbvGDXy)a?@8T&1dY4`;L z4Kn+f%SSFWE_rpEpL9bnlmYq`D!6F%di<&Hh=+!VI~j)2mfil03T#jJ_s?}VV0_hp z7T9bWxc>Jm2Z0WMU?`Z$xE74Gu~%s{mW!d4uvKCx@WD+gPUQ zV0vQS(Ig++z=EHN)BR44*EDSWIyT~R4$FcF*VEY*8@l=218Q05D2$|fXKFhRgBIEE zdDFB}1dKkoO^7}{5crKX!p?dZWNz$m>1icsXG2N+((x0OIST9Zo^DW_tytvlwXGpn zs8?pJXjEG;T@qrZi%#h93?FP$!&P4JA(&H61tqQi=opRzNpm zkrG}$^t9&XduK*Qa1?355wd8G2CI6QEh@Ua>AsD;7oRUNLPb76m4HG3K?)wF~IyS3`fXuNM>${?wmB zpVz;?6_(Fiadfd{vUCBM*_kt$+F3J+IojI;9L(gc9n3{sEZyzR9o!_mOwFC#tQ{Q~ zP3-`#uK#tP3Q7~Q;4H|wjZHO8h7e4IuBxl&vz2w~D8)w=Wtg31zpZhz%+kzSzL*dV zwp@{WU4i;hJ7c2f1O;7Mz6qRKeASoIv0_bV=i@NMG*l<#+;INk-^`5w@}Dj~;k=|}qM1vq_P z|GpBGe_IKq|LNy9SJhKOQ$c=5L{Dv|Q_lZl=-ky*BFBJLW9&y_C|!vyM~rQx=!vun z?rZJQB5t}Dctmui5i31C_;_}CEn}_W%>oSXtt>@kE1=JW*4*v4tPp;O6 zmAk{)m!)}34pTWg8{i>($%NQ(Tl;QC@J@FfBoc%Gr&m560^kgSfodAFrIjF}aIw)X zoXZ`@IsMkc8_=w%-7`D6Y4e*CG8k%Ud=GXhsTR50jUnm+R*0A(O3UKFg0`K;qp1bl z7``HN=?39ic_kR|^R^~w-*pa?Vj#7|e9F1iRx{GN2?wK!xR1GW!qa=~pjJb-#u1K8 zeR?Y2i-pt}yJq;SCiVHODIvQJX|ZJaT8nO+(?HXbLefulKKgM^B(UIO1r+S=7;kLJ zcH}1J=Px2jsh3Tec&v8Jcbng8;V-`#*UHt?hB(pmOipKwf3Lz8rG$heEB30Sg*2rx zV<|KN86$soN(I!BwO`1n^^uF2*x&vJ$2d$>+`(romzHP|)K_KkO6Hc>_dwMW-M(#S zK(~SiXT1@fvc#U+?|?PniDRm01)f^#55;nhM|wi?oG>yBsa?~?^xTU|fX-R(sTA+5 zaq}-8Tx7zrOy#3*JLIIVsBmHYLdD}!0NP!+ITW+Thn0)8SS!$@)HXwB3tY!fMxc#1 zMp3H?q3eD?u&Njx4;KQ5G>32+GRp1Ee5qMO0lZjaRRu&{W<&~DoJNGkcYF<5(Ab+J zgO>VhBl{okDPn78<%&e2mR{jwVCz5Og;*Z;;3%VvoGo_;HaGLWYF7q#jDX=Z#Ml`H z858YVV$%J|e<1n`%6Vsvq7GmnAV0wW4$5qQ3uR@1i>tW{xrl|ExywIc?fNgYlA?C5 zh$ezAFb5{rQu6i7BSS5*J-|9DQ{6^BVQ{b*lq`xS@RyrsJN?-t=MTMPY;WYeKBCNg z^2|pN!Q^WPJuuO4!|P@jzt&tY1Y8d%FNK5xK(!@`jO2aEA*4 zkO6b|UVBipci?){-Ke=+1;mGlND8)6+P;8sq}UXw2hn;fc7nM>g}GSMWu&v&fqh

iViYT=fZ(|3Ox^$aWPp4a8h24tD<|8-!aK0lHgL$N7Efw}J zVIB!7=T$U`ao1?upi5V4Et*-lTG0XvExbf!ya{cua==$WJyVG(CmA6Of*8E@DSE%L z`V^$qz&RU$7G5mg;8;=#`@rRG`-uS18$0WPN@!v2d{H2sOqP|!(cQ@ zUHo!d>>yFArLPf1q`uBvY32miqShLT1B@gDL4XoVTK&@owOoD)OIHXrYK-a1d$B{v zF^}8D3Y^g%^cnvScOSJR5QNH+BI%d|;J;wWM3~l>${fb8DNPg)wrf|GBP8p%LNGN# z3EaIiItgwtGgT&iYCFy9-LG}bMI|4LdmmJt@V@% zb6B)1kc=T)(|L@0;wr<>=?r04N;E&ef+7C^`wPWtyQe(*pD1pI_&XHy|0gIGHMekd zF_*M4yi6J&Z4LQj65)S zXwdM{SwUo%3SbPwFsHgqF@V|6afT|R6?&S;lw=8% z3}@9B=#JI3@B*#4s!O))~z zc>2_4Q_#&+5V`GFd?88^;c1i7;Vv_I*qt!_Yx*n=;rj!82rrR2rQ8u5(Ejlo{15P% zs~!{%XJ>FmJ})H^I9bn^Re&38H{xA!0l3^89k(oU;bZWXM@kn$#aoS&Y4l^-WEn-fH39Jb9lA%s*WsKJQl?n9B7_~P z-XM&WL7Z!PcoF6_D>V@$CvUIEy=+Z&0kt{szMk=f1|M+r*a43^$$B^MidrT0J;RI` z(?f!O<8UZkm$_Ny$Hth1J#^4ni+im8M9mr&k|3cIgwvjAgjH z8`N&h25xV#v*d$qBX5jkI|xOhQn!>IYZK7l5#^P4M&twe9&Ey@@GxYMxBZq2e7?`q z$~Szs0!g{2fGcp9PZEt|rdQ6bhAgpcLHPz?f-vB?$dc*!9OL?Q8mn7->bFD2Si60* z!O%y)fCdMSV|lkF9w%x~J*A&srMyYY3{=&$}H zGQ4VG_?$2X(0|vT0{=;W$~icCI{b6W{B!Q8xdGhF|D{25G_5_+%s(46lhvNLkik~R z>nr(&C#5wwOzJZQo9m|U<;&Wk!_#q|V>fsmj1g<6%hB{jGoNUPjgJslld>xmODzGjYc?7JSuA?A_QzjDw5AsRgi@Y|Z0{F{!1=!NES-#*f^s4l0Hu zz468))2IY5dmD9pa*(yT5{EyP^G>@ZWumealS-*WeRcZ}B%gxq{MiJ|RyX-^C1V=0 z@iKdrGi1jTe8Ya^x7yyH$kBNvM4R~`fbPq$BzHum-3Zo8C6=KW@||>zsA8-Y9uV5V z#oq-f5L5}V<&wF4@X@<3^C%ptp6+Ce)~hGl`kwj)bsAjmo_GU^r940Z-|`<)oGnh7 zFF0Tde3>ui?8Yj{sF-Z@)yQd~CGZ*w-6p2U<8}JO-sRsVI5dBji`01W8A&3$?}lxBaC&vn0E$c5tW* zX>5(zzZ=qn&!J~KdsPl;P@bmA-Pr8T*)eh_+Dv5=Ma|XSle6t(k8qcgNyar{*ReQ8 zTXwi=8vr>!3Ywr+BhggHDw8ke==NTQVMCK`$69fhzEFB*4+H9LIvdt-#IbhZvpS}} zO3lz;P?zr0*0$%-Rq_y^k(?I{Mk}h@w}cZpMUp|ucs55bcloL2)($u%mXQw({Wzc~ z;6nu5MkjP)0C(@%6Q_I_vsWrfhl7Zpoxw#WoE~r&GOSCz;_ro6i(^hM>I$8y>`!wW z*U^@?B!MMmb89I}2(hcE4zN2G^kwyWCZp5JG>$Ez7zP~D=J^LMjSM)27_0B_X^C(M z`fFT+%DcKlu?^)FCK>QzSnV%IsXVcUFhFdBP!6~se&xxrIxsvySAWu++IrH;FbcY$ z2DWTvSBRfLwdhr0nMx+URA$j3i7_*6BWv#DXfym?ZRDcX9C?cY9sD3q)uBDR3uWg= z(lUIzB)G$Hr!){>E{s4Dew+tb9kvToZp-1&c?y2wn@Z~(VBhqz`cB;{E4(P3N2*nJ z_>~g@;UF2iG{Kt(<1PyePTKahF8<)pozZ*xH~U-kfoAayCwJViIrnqwqO}7{0pHw$ zs2Kx?s#vQr7XZ264>5RNKSL8|Ty^=PsIx^}QqOOcfpGUU4tRkUc|kc7-!Ae6!+B{o~7nFpm3|G5^=0#Bnm6`V}oSQlrX(u%OWnC zoLPy&Q;1Jui&7ST0~#+}I^&?vcE*t47~Xq#YwvA^6^} z`WkC)$AkNub|t@S!$8CBlwbV~?yp&@9h{D|3z-vJXgzRC5^nYm+PyPcgRzAnEi6Q^gslXYRv4nycsy-SJu?lMps-? zV`U*#WnFsdPLL)Q$AmD|0`UaC4ND07+&UmOu!eHruzV|OUox<+Jl|Mr@6~C`T@P%s zW7sgXLF2SSe9Fl^O(I*{9wsFSYb2l%-;&Pi^dpv!{)C3d0AlNY6!4fgmSgj_wQ*7Am7&$z;Jg&wgR-Ih;lUvWS|KTSg!&s_E9_bXBkZvGiC6bFKDWZxsD$*NZ#_8bl zG1P-#@?OQzED7@jlMJTH@V!6k;W>auvft)}g zhoV{7$q=*;=l{O>Q4a@ ziMjf_u*o^PsO)#BjC%0^h>Xp@;5$p{JSYDt)zbb}s{Kbt!T*I@Pk@X0zds6wsefuU zW$XY%yyRGC94=6mf?x+bbA5CDQ2AgW1T-jVAJbm7K(gp+;v6E0WI#kuACgV$r}6L? zd|Tj?^%^*N&b>Dd{Wr$FS2qI#Ucs1yd4N+RBUQiSZGujH`#I)mG&VKoDh=KKFl4=G z&MagXl6*<)$6P}*Tiebpz5L=oMaPrN+caUXRJ`D?=K9!e0f{@D&cZLKN?iNP@X0aF zE(^pl+;*T5qt?1jRC=5PMgV!XNITRLS_=9{CJExaQj;lt!&pdzpK?8p>%Mb+D z?yO*uSung=-`QQ@yX@Hyd4@CI^r{2oiu`%^bNkz+Nkk!IunjwNC|WcqvX~k=><-I3 zDQdbdb|!v+Iz01$w@aMl!R)koD77Xp;eZwzSl-AT zr@Vu{=xvgfq9akRrrM)}=!=xcs+U1JO}{t(avgz`6RqiiX<|hGG1pmop8k6Q+G_mv zJv|RfDheUp2L3=^C=4aCBMBn0aRCU(DQwX-W(RkRwmLeuJYF<0urcaf(=7)JPg<3P zQs!~G)9CT18o!J4{zX{_e}4eS)U-E)0FAt}wEI(c0%HkxgggW;(1E=>J17_hsH^sP z%lT0LGgbUXHx-K*CI-MCrP66UP0PvGqM$MkeLyqHdbgP|_Cm!7te~b8p+e6sQ_3k| zVcwTh6d83ltdnR>D^)BYQpDKlLk3g0Hdcgz2}%qUs9~~Rie)A-BV1mS&naYai#xcZ z(d{8=-LVpTp}2*y)|gR~;qc7fp26}lPcLZ#=JpYcn3AT9(UIdOyg+d(P5T7D&*P}# zQCYplZO5|7+r19%9e`v^vfSS1sbX1c%=w1;oyruXB%Kl$ACgKQ6=qNWLsc=28xJjg zwvsI5-%SGU|3p>&zXVl^vVtQT3o-#$UT9LI@Npz~6=4!>mc431VRNN8od&Ul^+G_kHC`G=6WVWM z%9eWNyy(FTO|A+@x}Ou3CH)oi;t#7rAxdIXfNFwOj_@Y&TGz6P_sqiB`Q6Lxy|Q{`|fgmRG(k+!#b*M+Z9zFce)f-7;?Km5O=LHV9f9_87; zF7%R2B+$?@sH&&-$@tzaPYkw0;=i|;vWdI|Wl3q_Zu>l;XdIw2FjV=;Mq5t1Q0|f< zs08j54Bp`3RzqE=2enlkZxmX6OF+@|2<)A^RNQpBd6o@OXl+i)zO%D4iGiQNuXd+zIR{_lb96{lc~bxsBveIw6umhShTX+3@ZJ=YHh@ zWY3(d0azg;7oHn>H<>?4@*RQbi>SmM=JrHvIG(~BrvI)#W(EAeO6fS+}mxxcc+X~W6&YVl86W9WFSS}Vz-f9vS?XUDBk)3TcF z8V?$4Q)`uKFq>xT=)Y9mMFVTUk*NIA!0$?RP6Ig0TBmUFrq*Q-Agq~DzxjStQyJ({ zBeZ;o5qUUKg=4Hypm|}>>L=XKsZ!F$yNTDO)jt4H0gdQ5$f|d&bnVCMMXhNh)~mN z@_UV6D7MVlsWz+zM+inZZp&P4fj=tm6fX)SG5H>OsQf_I8c~uGCig$GzuwViK54bcgL;VN|FnyQl>Ed7(@>=8$a_UKIz|V6CeVSd2(P z0Uu>A8A+muM%HLFJQ9UZ5c)BSAv_zH#1f02x?h9C}@pN@6{>UiAp>({Fn(T9Q8B z^`zB;kJ5b`>%dLm+Ol}ty!3;8f1XDSVX0AUe5P#@I+FQ-`$(a;zNgz)4x5hz$Hfbg z!Q(z26wHLXko(1`;(BAOg_wShpX0ixfWq3ponndY+u%1gyX)_h=v1zR#V}#q{au6; z!3K=7fQwnRfg6FXtNQmP>`<;!N137paFS%y?;lb1@BEdbvQHYC{976l`cLqn;b8lp zIDY>~m{gDj(wfnK!lpW6pli)HyLEiUrNc%eXTil|F2s(AY+LW5hkKb>TQ3|Q4S9rr zpDs4uK_co6XPsn_z$LeS{K4jFF`2>U`tbgKdyDne`xmR<@6AA+_hPNKCOR-Zqv;xk zu5!HsBUb^!4uJ7v0RuH-7?l?}b=w5lzzXJ~gZcxRKOovSk@|#V+MuX%Y+=;14i*%{)_gSW9(#4%)AV#3__kac1|qUy!uyP{>?U#5wYNq}y$S9pCc zFc~4mgSC*G~j0u#qqp9 z${>3HV~@->GqEhr_Xwoxq?Hjn#=s2;i~g^&Hn|aDKpA>Oc%HlW(KA1?BXqpxB;Ydx)w;2z^MpjJ(Qi(X!$5RC z*P{~%JGDQqojV>2JbEeCE*OEu!$XJ>bWA9Oa_Hd;y)F%MhBRi*LPcdqR8X`NQ&1L# z5#9L*@qxrx8n}LfeB^J{%-?SU{FCwiWyHp682F+|pa+CQa3ZLzBqN1{)h4d6+vBbV zC#NEbQLC;}me3eeYnOG*nXOJZEU$xLZ1<1Y=7r0(-U0P6-AqwMAM`a(Ed#7vJkn6plb4eI4?2y3yOTGmmDQ!z9`wzbf z_OY#0@5=bnep;MV0X_;;SJJWEf^E6Bd^tVJ9znWx&Ks8t*B>AM@?;D4oWUGc z!H*`6d7Cxo6VuyS4Eye&L1ZRhrRmN6Lr`{NL(wDbif|y&z)JN>Fl5#Wi&mMIr5i;x zBx}3YfF>>8EC(fYnmpu~)CYHuHCyr5*`ECap%t@y=jD>!_%3iiE|LN$mK9>- zHdtpy8fGZtkZF?%TW~29JIAfi2jZT8>OA7=h;8T{{k?c2`nCEx9$r zS+*&vt~2o^^J+}RDG@+9&M^K*z4p{5#IEVbz`1%`m5c2};aGt=V?~vIM}ZdPECDI)47|CWBCfDWUbxBCnmYivQ*0Nu_xb*C>~C9(VjHM zxe<*D<#dQ8TlpMX2c@M<9$w!RP$hpG4cs%AI){jp*Sj|*`m)5(Bw*A0$*i-(CA5#%>a)$+jI2C9r6|(>J8InryENI z$NohnxDUB;wAYDwrb*!N3noBTKPpPN}~09SEL18tkG zxgz(RYU_;DPT{l?Q$+eaZaxnsWCA^ds^0PVRkIM%bOd|G2IEBBiz{&^JtNsODs;5z zICt_Zj8wo^KT$7Bg4H+y!Df#3mbl%%?|EXe!&(Vmac1DJ*y~3+kRKAD=Ovde4^^%~ zw<9av18HLyrf*_>Slp;^i`Uy~`mvBjZ|?Ad63yQa#YK`4+c6;pW4?XIY9G1(Xh9WO8{F-Aju+nS9Vmv=$Ac0ienZ+p9*O%NG zMZKy5?%Z6TAJTE?o5vEr0r>f>hb#2w2U3DL64*au_@P!J!TL`oH2r*{>ffu6|A7tv zL4juf$DZ1MW5ZPsG!5)`k8d8c$J$o;%EIL0va9&GzWvkS%ZsGb#S(?{!UFOZ9<$a| zY|a+5kmD5N&{vRqkgY>aHsBT&`rg|&kezoD)gP0fsNYHsO#TRc_$n6Lf1Z{?+DLziXlHrq4sf(!>O{?Tj;Eh@%)+nRE_2VxbN&&%%caU#JDU%vL3}Cb zsb4AazPI{>8H&d=jUaZDS$-0^AxE@utGs;-Ez_F(qC9T=UZX=>ok2k2 ziTn{K?y~a5reD2A)P${NoI^>JXn>`IeArow(41c-Wm~)wiryEP(OS{YXWi7;%dG9v zI?mwu1MxD{yp_rrk!j^cKM)dc4@p4Ezyo%lRN|XyD}}>v=Xoib0gOcdXrQ^*61HNj z=NP|pd>@yfvr-=m{8$3A8TQGMTE7g=z!%yt`8`Bk-0MMwW~h^++;qyUP!J~ykh1GO z(FZ59xuFR$(WE;F@UUyE@Sp>`aVNjyj=Ty>_Vo}xf`e7`F;j-IgL5`1~-#70$9_=uBMq!2&1l zomRgpD58@)YYfvLtPW}{C5B35R;ZVvB<<#)x%srmc_S=A7F@DW8>QOEGwD6suhwCg z>Pa+YyULhmw%BA*4yjDp|2{!T98~<6Yfd(wo1mQ!KWwq0eg+6)o1>W~f~kL<-S+P@$wx*zeI|1t7z#Sxr5 zt6w+;YblPQNplq4Z#T$GLX#j6yldXAqj>4gAnnWtBICUnA&-dtnlh=t0Ho_vEKwV` z)DlJi#!@nkYV#$!)@>udAU*hF?V`2$Hf=V&6PP_|r#Iv*J$9)pF@X3`k;5})9^o4y z&)~?EjX5yX12O(BsFy-l6}nYeuKkiq`u9145&3Ssg^y{5G3Pse z9w(YVa0)N-fLaBq1`P!_#>SS(8fh_5!f{UrgZ~uEdeMJIz7DzI5!NHHqQtm~#CPij z?=N|J>nPR6_sL7!f4hD_|KH`vf8(Wpnj-(gPWH+ZvID}%?~68SwhPTC3u1_cB`otq z)U?6qo!ZLi5b>*KnYHWW=3F!p%h1;h{L&(Q&{qY6)_qxNfbP6E3yYpW!EO+IW3?@J z);4>g4gnl^8klu7uA>eGF6rIGSynacogr)KUwE_R4E5Xzi*Qir@b-jy55-JPC8c~( zo!W8y9OGZ&`xmc8;=4-U9=h{vCqfCNzYirONmGbRQlR`WWlgnY+1wCXbMz&NT~9*| z6@FrzP!LX&{no2!Ln_3|I==_4`@}V?4a;YZKTdw;vT<+K+z=uWbW(&bXEaWJ^W8Td z-3&1bY^Z*oM<=M}LVt>_j+p=2Iu7pZmbXrhQ_k)ysE9yXKygFNw$5hwDn(M>H+e1&9BM5!|81vd%r%vEm zqxY3?F@fb6O#5UunwgAHR9jp_W2zZ}NGp2%mTW@(hz7$^+a`A?mb8|_G*GNMJ) zjqegXQio=i@AINre&%ofexAr95aop5C+0MZ0m-l=MeO8m3epm7U%vZB8+I+C*iNFM z#T3l`gknX;D$-`2XT^Cg*vrv=RH+P;_dfF++cP?B_msQI4j+lt&rX2)3GaJx%W*Nn zkML%D{z5tpHH=dksQ*gzc|}gzW;lwAbxoR07VNgS*-c3d&8J|;@3t^ zVUz*J*&r7DFRuFVDCJDK8V9NN5hvpgGjwx+5n)qa;YCKe8TKtdnh{I7NU9BCN!0dq zczrBk8pE{{@vJa9ywR@mq*J=v+PG;?fwqlJVhijG!3VmIKs>9T6r7MJpC)m!Tc#>g zMtVsU>wbwFJEfwZ{vB|ZlttNe83)$iz`~#8UJ^r)lJ@HA&G#}W&ZH*;k{=TavpjWE z7hdyLZPf*X%Gm}i`Y{OGeeu^~nB8=`{r#TUrM-`;1cBvEd#d!kPqIgYySYhN-*1;L z^byj%Yi}Gx)Wnkosi337BKs}+5H5dth1JA{Ir-JKN$7zC)*}hqeoD(WfaUDPT>0`- z(6sa0AoIqASwF`>hP}^|)a_j2s^PQn*qVC{Q}htR z5-)duBFXT_V56-+UohKXlq~^6uf!6sA#ttk1o~*QEy_Y-S$gAvq47J9Vtk$5oA$Ct zYhYJ@8{hsC^98${!#Ho?4y5MCa7iGnfz}b9jE~h%EAAv~Qxu)_rAV;^cygV~5r_~?l=B`zObj7S=H=~$W zPtI_m%g$`kL_fVUk9J@>EiBH zOO&jtn~&`hIFMS5S`g8w94R4H40mdNUH4W@@XQk1sr17b{@y|JB*G9z1|CrQjd+GX z6+KyURG3;!*BQrentw{B2R&@2&`2}n(z-2&X7#r!{yg@Soy}cRD~j zj9@UBW+N|4HW4AWapy4wfUI- zZ`gSL6DUlgj*f1hSOGXG0IVH8HxK?o2|3HZ;KW{K+yPAlxtb)NV_2AwJm|E)FRs&& z=c^e7bvUsztY|+f^k7NXs$o1EUq>cR7C0$UKi6IooHWlK_#?IWDkvywnzg&ThWo^? z2O_N{5X39#?eV9l)xI(>@!vSB{DLt*oY!K1R8}_?%+0^C{d9a%N4 zoxHVT1&Lm|uDX%$QrBun5e-F`HJ^T$ zmzv)p@4ZHd_w9!%Hf9UYNvGCw2TTTbrj9pl+T9%-_-}L(tES>Or-}Z4F*{##n3~L~TuxjirGuIY#H7{%$E${?p{Q01 zi6T`n;rbK1yIB9jmQNycD~yZq&mbIsFWHo|ZAChSFPQa<(%d8mGw*V3fh|yFoxOOiWJd(qvVb!Z$b88cg->N=qO*4k~6;R==|9ihg&riu#P~s4Oap9O7f%crSr^rljeIfXDEg>wi)&v*a%7zpz<9w z*r!3q9J|390x`Zk;g$&OeN&ctp)VKRpDSV@kU2Q>jtok($Y-*x8_$2piTxun81@vt z!Vj?COa0fg2RPXMSIo26T=~0d`{oGP*eV+$!0I<(4azk&Vj3SiG=Q!6mX0p$z7I}; z9BJUFgT-K9MQQ-0@Z=^7R<{bn2Fm48endsSs`V7_@%8?Bxkqv>BDoVcj?K#dV#uUP zL1ND~?D-|VGKe3Rw_7-Idpht>H6XRLh*U7epS6byiGvJpr%d}XwfusjH9g;Z98H`x zyde%%5mhGOiL4wljCaWCk-&uE4_OOccb9c!ZaWt4B(wYl!?vyzl%7n~QepN&eFUrw zFIOl9c({``6~QD+43*_tzP{f2x41h(?b43^y6=iwyB)2os5hBE!@YUS5?N_tXd=h( z)WE286Fbd>R4M^P{!G)f;h<3Q>Fipuy+d2q-)!RyTgt;wr$(?9ox3;q+{E*ZQHhOn;lM`cjnu9 zXa48ks-v(~b*;MAI<>YZH(^NV8vjb34beE<_cwKlJoR;k6lJNSP6v}uiyRD?|0w+X@o1ONrH8a$fCxXpf? z?$DL0)7|X}Oc%h^zrMKWc-NS9I0Utu@>*j}b@tJ=ixQSJ={4@854wzW@E>VSL+Y{i z#0b=WpbCZS>kUCO_iQz)LoE>P5LIG-hv9E+oG}DtlIDF>$tJ1aw9^LuhLEHt?BCj& z(O4I8v1s#HUi5A>nIS-JK{v!7dJx)^Yg%XjNmlkWAq2*cv#tHgz`Y(bETc6CuO1VkN^L-L3j_x<4NqYb5rzrLC-7uOv z!5e`GZt%B782C5-fGnn*GhDF$%(qP<74Z}3xx+{$4cYKy2ikxI7B2N+2r07DN;|-T->nU&!=Cm#rZt%O_5c&1Z%nlWq3TKAW0w zQqemZw_ue--2uKQsx+niCUou?HjD`xhEjjQd3%rrBi82crq*~#uA4+>vR<_S{~5ce z-2EIl?~s z1=GVL{NxP1N3%=AOaC}j_Fv=ur&THz zyO!d9kHq|c73kpq`$+t+8Bw7MgeR5~`d7ChYyGCBWSteTB>8WAU(NPYt2Dk`@#+}= zI4SvLlyk#pBgVigEe`?NG*vl7V6m+<}%FwPV=~PvvA)=#ths==DRTDEYh4V5}Cf$z@#;< zyWfLY_5sP$gc3LLl2x+Ii)#b2nhNXJ{R~vk`s5U7Nyu^3yFg&D%Txwj6QezMX`V(x z=C`{76*mNb!qHHs)#GgGZ_7|vkt9izl_&PBrsu@}L`X{95-2jf99K)0=*N)VxBX2q z((vkpP2RneSIiIUEnGb?VqbMb=Zia+rF~+iqslydE34cSLJ&BJW^3knX@M;t*b=EA zNvGzv41Ld_T+WT#XjDB840vovUU^FtN_)G}7v)1lPetgpEK9YS^OWFkPoE{ovj^=@ zO9N$S=G$1ecndT_=5ehth2Lmd1II-PuT~C9`XVePw$y8J#dpZ?Tss<6wtVglm(Ok7 z3?^oi@pPio6l&!z8JY(pJvG=*pI?GIOu}e^EB6QYk$#FJQ%^AIK$I4epJ+9t?KjqA+bkj&PQ*|vLttme+`9G=L% ziadyMw_7-M)hS(3E$QGNCu|o23|%O+VN7;Qggp?PB3K-iSeBa2b}V4_wY`G1Jsfz4 z9|SdB^;|I8E8gWqHKx!vj_@SMY^hLEIbSMCuE?WKq=c2mJK z8LoG-pnY!uhqFv&L?yEuxo{dpMTsmCn)95xanqBrNPTgXP((H$9N${Ow~Is-FBg%h z53;|Y5$MUN)9W2HBe2TD`ct^LHI<(xWrw}$qSoei?}s)&w$;&!14w6B6>Yr6Y8b)S z0r71`WmAvJJ`1h&poLftLUS6Ir zC$bG9!Im_4Zjse)#K=oJM9mHW1{%l8sz$1o?ltdKlLTxWWPB>Vk22czVt|1%^wnN@*!l)}?EgtvhC>vlHm^t+ogpgHI1_$1ox9e;>0!+b(tBrmXRB`PY1vp-R**8N7 zGP|QqI$m(Rdu#=(?!(N}G9QhQ%o!aXE=aN{&wtGP8|_qh+7a_j_sU5|J^)vxq;# zjvzLn%_QPHZZIWu1&mRAj;Sa_97p_lLq_{~j!M9N^1yp3U_SxRqK&JnR%6VI#^E12 z>CdOVI^_9aPK2eZ4h&^{pQs}xsijXgFYRIxJ~N7&BB9jUR1fm!(xl)mvy|3e6-B3j zJn#ajL;bFTYJ2+Q)tDjx=3IklO@Q+FFM}6UJr6km7hj7th9n_&JR7fnqC!hTZoM~T zBeaVFp%)0cbPhejX<8pf5HyRUj2>aXnXBqDJe73~J%P(2C?-RT{c3NjE`)om! zl$uewSgWkE66$Kb34+QZZvRn`fob~Cl9=cRk@Es}KQm=?E~CE%spXaMO6YmrMl%9Q zlA3Q$3|L1QJ4?->UjT&CBd!~ru{Ih^in&JXO=|<6J!&qp zRe*OZ*cj5bHYlz!!~iEKcuE|;U4vN1rk$xq6>bUWD*u(V@8sG^7>kVuo(QL@Ki;yL zWC!FT(q{E8#on>%1iAS0HMZDJg{Z{^!De(vSIq&;1$+b)oRMwA3nc3mdTSG#3uYO_ z>+x;7p4I;uHz?ZB>dA-BKl+t-3IB!jBRgdvAbW!aJ(Q{aT>+iz?91`C-xbe)IBoND z9_Xth{6?(y3rddwY$GD65IT#f3<(0o#`di{sh2gm{dw*#-Vnc3r=4==&PU^hCv$qd zjw;>i&?L*Wq#TxG$mFIUf>eK+170KG;~+o&1;Tom9}}mKo23KwdEM6UonXgc z!6N(@k8q@HPw{O8O!lAyi{rZv|DpgfU{py+j(X_cwpKqcalcqKIr0kM^%Br3SdeD> zHSKV94Yxw;pjzDHo!Q?8^0bb%L|wC;4U^9I#pd5O&eexX+Im{ z?jKnCcsE|H?{uGMqVie_C~w7GX)kYGWAg%-?8|N_1#W-|4F)3YTDC+QSq1s!DnOML3@d`mG%o2YbYd#jww|jD$gotpa)kntakp#K;+yo-_ZF9qrNZw<%#C zuPE@#3RocLgPyiBZ+R_-FJ_$xP!RzWm|aN)S+{$LY9vvN+IW~Kf3TsEIvP+B9Mtm! zpfNNxObWQpLoaO&cJh5>%slZnHl_Q~(-Tfh!DMz(dTWld@LG1VRF`9`DYKhyNv z2pU|UZ$#_yUx_B_|MxUq^glT}O5Xt(Vm4Mr02><%C)@v;vPb@pT$*yzJ4aPc_FZ3z z3}PLoMBIM>q_9U2rl^sGhk1VUJ89=*?7|v`{!Z{6bqFMq(mYiA?%KbsI~JwuqVA9$H5vDE+VocjX+G^%bieqx->s;XWlKcuv(s%y%D5Xbc9+ zc(_2nYS1&^yL*ey664&4`IoOeDIig}y-E~_GS?m;D!xv5-xwz+G`5l6V+}CpeJDi^ z%4ed$qowm88=iYG+(`ld5Uh&>Dgs4uPHSJ^TngXP_V6fPyl~>2bhi20QB%lSd#yYn zO05?KT1z@?^-bqO8Cg`;ft>ilejsw@2%RR7;`$Vs;FmO(Yr3Fp`pHGr@P2hC%QcA|X&N2Dn zYf`MqXdHi%cGR@%y7Rg7?d3?an){s$zA{!H;Ie5exE#c~@NhQUFG8V=SQh%UxUeiV zd7#UcYqD=lk-}sEwlpu&H^T_V0{#G?lZMxL7ih_&{(g)MWBnCZxtXg znr#}>U^6!jA%e}@Gj49LWG@*&t0V>Cxc3?oO7LSG%~)Y5}f7vqUUnQ;STjdDU}P9IF9d9<$;=QaXc zL1^X7>fa^jHBu_}9}J~#-oz3Oq^JmGR#?GO7b9a(=R@fw@}Q{{@`Wy1vIQ#Bw?>@X z-_RGG@wt|%u`XUc%W{J z>iSeiz8C3H7@St3mOr_mU+&bL#Uif;+Xw-aZdNYUpdf>Rvu0i0t6k*}vwU`XNO2he z%miH|1tQ8~ZK!zmL&wa3E;l?!!XzgV#%PMVU!0xrDsNNZUWKlbiOjzH-1Uoxm8E#r`#2Sz;-o&qcqB zC-O_R{QGuynW14@)7&@yw1U}uP(1cov)twxeLus0s|7ayrtT8c#`&2~Fiu2=R;1_4bCaD=*E@cYI>7YSnt)nQc zohw5CsK%m?8Ack)qNx`W0_v$5S}nO|(V|RZKBD+btO?JXe|~^Qqur%@eO~<8-L^9d z=GA3-V14ng9L29~XJ>a5k~xT2152zLhM*@zlp2P5Eu}bywkcqR;ISbas&#T#;HZSf z2m69qTV(V@EkY(1Dk3`}j)JMo%ZVJ*5eB zYOjIisi+igK0#yW*gBGj?@I{~mUOvRFQR^pJbEbzFxTubnrw(Muk%}jI+vXmJ;{Q6 zrSobKD>T%}jV4Ub?L1+MGOD~0Ir%-`iTnWZN^~YPrcP5y3VMAzQ+&en^VzKEb$K!Q z<7Dbg&DNXuow*eD5yMr+#08nF!;%4vGrJI++5HdCFcGLfMW!KS*Oi@=7hFwDG!h2< zPunUEAF+HncQkbfFj&pbzp|MU*~60Z(|Ik%Tn{BXMN!hZOosNIseT?R;A`W?=d?5X zK(FB=9mZusYahp|K-wyb={rOpdn=@;4YI2W0EcbMKyo~-#^?h`BA9~o285%oY zfifCh5Lk$SY@|2A@a!T2V+{^!psQkx4?x0HSV`(w9{l75QxMk!)U52Lbhn{8ol?S) zCKo*7R(z!uk<6*qO=wh!Pul{(qq6g6xW;X68GI_CXp`XwO zxuSgPRAtM8K7}5E#-GM!*ydOOG_{A{)hkCII<|2=ma*71ci_-}VPARm3crFQjLYV! z9zbz82$|l01mv`$WahE2$=fAGWkd^X2kY(J7iz}WGS z@%MyBEO=A?HB9=^?nX`@nh;7;laAjs+fbo!|K^mE!tOB>$2a_O0y-*uaIn8k^6Y zSbuv;5~##*4Y~+y7Z5O*3w4qgI5V^17u*ZeupVGH^nM&$qmAk|anf*>r zWc5CV;-JY-Z@Uq1Irpb^O`L_7AGiqd*YpGUShb==os$uN3yYvb`wm6d=?T*it&pDk zo`vhw)RZX|91^^Wa_ti2zBFyWy4cJu#g)_S6~jT}CC{DJ_kKpT`$oAL%b^!2M;JgT zM3ZNbUB?}kP(*YYvXDIH8^7LUxz5oE%kMhF!rnPqv!GiY0o}NR$OD=ITDo9r%4E>E0Y^R(rS^~XjWyVI6 zMOR5rPXhTp*G*M&X#NTL`Hu*R+u*QNoiOKg4CtNPrjgH>c?Hi4MUG#I917fx**+pJfOo!zFM&*da&G_x)L(`k&TPI*t3e^{crd zX<4I$5nBQ8Ax_lmNRa~E*zS-R0sxkz`|>7q_?*e%7bxqNm3_eRG#1ae3gtV9!fQpY z+!^a38o4ZGy9!J5sylDxZTx$JmG!wg7;>&5H1)>f4dXj;B+@6tMlL=)cLl={jLMxY zbbf1ax3S4>bwB9-$;SN2?+GULu;UA-35;VY*^9Blx)Jwyb$=U!D>HhB&=jSsd^6yw zL)?a|>GxU!W}ocTC(?-%z3!IUhw^uzc`Vz_g>-tv)(XA#JK^)ZnC|l1`@CdX1@|!| z_9gQ)7uOf?cR@KDp97*>6X|;t@Y`k_N@)aH7gY27)COv^P3ya9I{4z~vUjLR9~z1Z z5=G{mVtKH*&$*t0@}-i_v|3B$AHHYale7>E+jP`ClqG%L{u;*ff_h@)al?RuL7tOO z->;I}>%WI{;vbLP3VIQ^iA$4wl6@0sDj|~112Y4OFjMs`13!$JGkp%b&E8QzJw_L5 zOnw9joc0^;O%OpF$Qp)W1HI!$4BaXX84`%@#^dk^hFp^pQ@rx4g(8Xjy#!X%+X5Jd@fs3amGT`}mhq#L97R>OwT5-m|h#yT_-v@(k$q7P*9X~T*3)LTdzP!*B} z+SldbVWrrwQo9wX*%FyK+sRXTa@O?WM^FGWOE?S`R(0P{<6p#f?0NJvnBia?k^fX2 zNQs7K-?EijgHJY}&zsr;qJ<*PCZUd*x|dD=IQPUK_nn)@X4KWtqoJNHkT?ZWL_hF? zS8lp2(q>;RXR|F;1O}EE#}gCrY~#n^O`_I&?&z5~7N;zL0)3Tup`%)oHMK-^r$NT% zbFg|o?b9w(q@)6w5V%si<$!U<#}s#x@0aX-hP>zwS#9*75VXA4K*%gUc>+yzupTDBOKH8WR4V0pM(HrfbQ&eJ79>HdCvE=F z|J>s;;iDLB^3(9}?biKbxf1$lI!*Z%*0&8UUq}wMyPs_hclyQQi4;NUY+x2qy|0J; zhn8;5)4ED1oHwg+VZF|80<4MrL97tGGXc5Sw$wAI#|2*cvQ=jB5+{AjMiDHmhUC*a zlmiZ`LAuAn_}hftXh;`Kq0zblDk8?O-`tnilIh|;3lZp@F_osJUV9`*R29M?7H{Fy z`nfVEIDIWXmU&YW;NjU8)EJpXhxe5t+scf|VXM!^bBlwNh)~7|3?fWwo_~ZFk(22% zTMesYw+LNx3J-_|DM~`v93yXe=jPD{q;li;5PD?Dyk+b? zo21|XpT@)$BM$%F=P9J19Vi&1#{jM3!^Y&fr&_`toi`XB1!n>sbL%U9I5<7!@?t)~ z;&H%z>bAaQ4f$wIzkjH70;<8tpUoxzKrPhn#IQfS%9l5=Iu))^XC<58D!-O z{B+o5R^Z21H0T9JQ5gNJnqh#qH^na|z92=hONIM~@_iuOi|F>jBh-?aA20}Qx~EpDGElELNn~|7WRXRFnw+Wdo`|# zBpU=Cz3z%cUJ0mx_1($X<40XEIYz(`noWeO+x#yb_pwj6)R(__%@_Cf>txOQ74wSJ z0#F3(zWWaR-jMEY$7C*3HJrohc79>MCUu26mfYN)f4M~4gD`}EX4e}A!U}QV8!S47 z6y-U-%+h`1n`*pQuKE%Av0@)+wBZr9mH}@vH@i{v(m-6QK7Ncf17x_D=)32`FOjjo zg|^VPf5c6-!FxN{25dvVh#fog=NNpXz zfB$o+0jbRkHH{!TKhE709f+jI^$3#v1Nmf80w`@7-5$1Iv_`)W^px8P-({xwb;D0y z7LKDAHgX<84?l!I*Dvi2#D@oAE^J|g$3!)x1Ua;_;<@#l1fD}lqU2_tS^6Ht$1Wl} zBESo7o^)9-Tjuz$8YQSGhfs{BQV6zW7dA?0b(Dbt=UnQs&4zHfe_sj{RJ4uS-vQpC zX;Bbsuju4%!o8?&m4UZU@~ZZjeFF6ex2ss5_60_JS_|iNc+R0GIjH1@Z z=rLT9%B|WWgOrR7IiIwr2=T;Ne?30M!@{%Qf8o`!>=s<2CBpCK_TWc(DX51>e^xh8 z&@$^b6CgOd7KXQV&Y4%}_#uN*mbanXq(2=Nj`L7H7*k(6F8s6{FOw@(DzU`4-*77{ zF+dxpv}%mFpYK?>N_2*#Y?oB*qEKB}VoQ@bzm>ptmVS_EC(#}Lxxx730trt0G)#$b zE=wVvtqOct1%*9}U{q<)2?{+0TzZzP0jgf9*)arV)*e!f`|jgT{7_9iS@e)recI#z zbzolURQ+TOzE!ymqvBY7+5NnAbWxvMLsLTwEbFqW=CPyCsmJ}P1^V30|D5E|p3BC5 z)3|qgw@ra7aXb-wsa|l^in~1_fm{7bS9jhVRkYVO#U{qMp z)Wce+|DJ}4<2gp8r0_xfZpMo#{Hl2MfjLcZdRB9(B(A(f;+4s*FxV{1F|4d`*sRNd zp4#@sEY|?^FIJ;tmH{@keZ$P(sLh5IdOk@k^0uB^BWr@pk6mHy$qf&~rI>P*a;h0C{%oA*i!VjWn&D~O#MxN&f@1Po# zKN+ zrGrkSjcr?^R#nGl<#Q722^wbYcgW@{+6CBS<1@%dPA8HC!~a`jTz<`g_l5N1M@9wn9GOAZ>nqNgq!yOCbZ@1z`U_N`Z>}+1HIZxk*5RDc&rd5{3qjRh8QmT$VyS;jK z;AF+r6XnnCp=wQYoG|rT2@8&IvKq*IB_WvS%nt%e{MCFm`&W*#LXc|HrD?nVBo=(8*=Aq?u$sDA_sC_RPDUiQ+wnIJET8vx$&fxkW~kP9qXKt zozR)@xGC!P)CTkjeWvXW5&@2?)qt)jiYWWBU?AUtzAN}{JE1I)dfz~7$;}~BmQF`k zpn11qmObXwRB8&rnEG*#4Xax3XBkKlw(;tb?Np^i+H8m(Wyz9k{~ogba@laiEk;2! zV*QV^6g6(QG%vX5Um#^sT&_e`B1pBW5yVth~xUs#0}nv?~C#l?W+9Lsb_5)!71rirGvY zTIJ$OPOY516Y|_014sNv+Z8cc5t_V=i>lWV=vNu#!58y9Zl&GsMEW#pPYPYGHQ|;vFvd*9eM==$_=vc7xnyz0~ zY}r??$<`wAO?JQk@?RGvkWVJlq2dk9vB(yV^vm{=NVI8dhsX<)O(#nr9YD?I?(VmQ z^r7VfUBn<~p3()8yOBjm$#KWx!5hRW)5Jl7wY@ky9lNM^jaT##8QGVsYeaVywmpv>X|Xj7gWE1Ezai&wVLt3p)k4w~yrskT-!PR!kiyQlaxl(( zXhF%Q9x}1TMt3~u@|#wWm-Vq?ZerK={8@~&@9r5JW}r#45#rWii};t`{5#&3$W)|@ zbAf2yDNe0q}NEUvq_Quq3cTjcw z@H_;$hu&xllCI9CFDLuScEMg|x{S7GdV8<&Mq=ezDnRZAyX-8gv97YTm0bg=d)(>N z+B2FcqvI9>jGtnK%eO%y zoBPkJTk%y`8TLf4)IXPBn`U|9>O~WL2C~C$z~9|0m*YH<-vg2CD^SX#&)B4ngOSG$ zV^wmy_iQk>dfN@Pv(ckfy&#ak@MLC7&Q6Ro#!ezM*VEh`+b3Jt%m(^T&p&WJ2Oqvj zs-4nq0TW6cv~(YI$n0UkfwN}kg3_fp?(ijSV#tR9L0}l2qjc7W?i*q01=St0eZ=4h zyGQbEw`9OEH>NMuIe)hVwYHsGERWOD;JxEiO7cQv%pFCeR+IyhwQ|y@&^24k+|8fD zLiOWFNJ2&vu2&`Jv96_z-Cd5RLgmeY3*4rDOQo?Jm`;I_(+ejsPM03!ly!*Cu}Cco zrQSrEDHNyzT(D5s1rZq!8#?f6@v6dB7a-aWs(Qk>N?UGAo{gytlh$%_IhyL7h?DLXDGx zgxGEBQoCAWo-$LRvM=F5MTle`M})t3vVv;2j0HZY&G z22^iGhV@uaJh(XyyY%} zd4iH_UfdV#T=3n}(Lj^|n;O4|$;xhu*8T3hR1mc_A}fK}jfZ7LX~*n5+`8N2q#rI$ z@<_2VANlYF$vIH$ zl<)+*tIWW78IIINA7Rr7i{<;#^yzxoLNkXL)eSs=%|P>$YQIh+ea_3k z_s7r4%j7%&*NHSl?R4k%1>Z=M9o#zxY!n8sL5>BO-ZP;T3Gut>iLS@U%IBrX6BA3k z)&@q}V8a{X<5B}K5s(c(LQ=%v1ocr`t$EqqY0EqVjr65usa=0bkf|O#ky{j3)WBR(((L^wmyHRzoWuL2~WTC=`yZ zn%VX`L=|Ok0v7?s>IHg?yArBcync5rG#^+u)>a%qjES%dRZoIyA8gQ;StH z1Ao7{<&}6U=5}4v<)1T7t!J_CL%U}CKNs-0xWoTTeqj{5{?Be$L0_tk>M9o8 zo371}S#30rKZFM{`H_(L`EM9DGp+Mifk&IP|C2Zu_)Ghr4Qtpmkm1osCf@%Z$%t+7 zYH$Cr)Ro@3-QDeQJ8m+x6%;?YYT;k6Z0E-?kr>x33`H%*ueBD7Zx~3&HtWn0?2Wt} zTG}*|v?{$ajzt}xPzV%lL1t-URi8*Zn)YljXNGDb>;!905Td|mpa@mHjIH%VIiGx- zd@MqhpYFu4_?y5N4xiHn3vX&|e6r~Xt> zZG`aGq|yTNjv;9E+Txuoa@A(9V7g?1_T5FzRI;!=NP1Kqou1z5?%X~Wwb{trRfd>i z8&y^H)8YnKyA_Fyx>}RNmQIczT?w2J4SNvI{5J&}Wto|8FR(W;Qw#b1G<1%#tmYzQ zQ2mZA-PAdi%RQOhkHy9Ea#TPSw?WxwL@H@cbkZwIq0B!@ns}niALidmn&W?!Vd4Gj zO7FiuV4*6Mr^2xlFSvM;Cp_#r8UaqIzHJQg_z^rEJw&OMm_8NGAY2)rKvki|o1bH~ z$2IbfVeY2L(^*rMRU1lM5Y_sgrDS`Z??nR2lX;zyR=c%UyGb*%TC-Dil?SihkjrQy~TMv6;BMs7P8il`H7DmpVm@rJ;b)hW)BL)GjS154b*xq-NXq2cwE z^;VP7ua2pxvCmxrnqUYQMH%a%nHmwmI33nJM(>4LznvY*k&C0{8f*%?zggpDgkuz&JBx{9mfb@wegEl2v!=}Sq2Gaty0<)UrOT0{MZtZ~j5y&w zXlYa_jY)I_+VA-^#mEox#+G>UgvM!Ac8zI<%JRXM_73Q!#i3O|)lOP*qBeJG#BST0 zqohi)O!|$|2SeJQo(w6w7%*92S})XfnhrH_Z8qe!G5>CglP=nI7JAOW?(Z29;pXJ9 zR9`KzQ=WEhy*)WH>$;7Cdz|>*i>=##0bB)oU0OR>>N<21e4rMCHDemNi2LD>Nc$;& zQRFthpWniC1J6@Zh~iJCoLOxN`oCKD5Q4r%ynwgUKPlIEd#?QViIqovY|czyK8>6B zSP%{2-<;%;1`#0mG^B(8KbtXF;Nf>K#Di72UWE4gQ%(_26Koiad)q$xRL~?pN71ZZ zujaaCx~jXjygw;rI!WB=xrOJO6HJ!!w}7eiivtCg5K|F6$EXa)=xUC za^JXSX98W`7g-tm@uo|BKj39Dl;sg5ta;4qjo^pCh~{-HdLl6qI9Ix6f$+qiZ$}s= zNguKrU;u+T@ko(Vr1>)Q%h$?UKXCY>3se%&;h2osl2D zE4A9bd7_|^njDd)6cI*FupHpE3){4NQ*$k*cOWZ_?CZ>Z4_fl@n(mMnYK62Q1d@+I zr&O))G4hMihgBqRIAJkLdk(p(D~X{-oBUA+If@B}j& zsHbeJ3RzTq96lB7d($h$xTeZ^gP0c{t!Y0c)aQE;$FY2!mACg!GDEMKXFOPI^)nHZ z`aSPJpvV0|bbrzhWWkuPURlDeN%VT8tndV8?d)eN*i4I@u zVKl^6{?}A?P)Fsy?3oi#clf}L18t;TjNI2>eI&(ezDK7RyqFxcv%>?oxUlonv(px) z$vnPzRH`y5A(x!yOIfL0bmgeMQB$H5wenx~!ujQK*nUBW;@Em&6Xv2%s(~H5WcU2R z;%Nw<$tI)a`Ve!>x+qegJnQsN2N7HaKzrFqM>`6R*gvh%O*-%THt zrB$Nk;lE;z{s{r^PPm5qz(&lM{sO*g+W{sK+m3M_z=4=&CC>T`{X}1Vg2PEfSj2x_ zmT*(x;ov%3F?qoEeeM>dUn$a*?SIGyO8m806J1W1o+4HRhc2`9$s6hM#qAm zChQ87b~GEw{ADfs+5}FJ8+|bIlIv(jT$Ap#hSHoXdd9#w<#cA<1Rkq^*EEkknUd4& zoIWIY)sAswy6fSERVm&!SO~#iN$OgOX*{9@_BWFyJTvC%S++ilSfCrO(?u=Dc?CXZ zzCG&0yVR{Z`|ZF0eEApWEo#s9osV>F{uK{QA@BES#&;#KsScf>y zvs?vIbI>VrT<*!;XmQS=bhq%46-aambZ(8KU-wOO2=en~D}MCToB_u;Yz{)1ySrPZ z@=$}EvjTdzTWU7c0ZI6L8=yP+YRD_eMMos}b5vY^S*~VZysrkq<`cK3>>v%uy7jgq z0ilW9KjVDHLv0b<1K_`1IkbTOINs0=m-22c%M~l=^S}%hbli-3?BnNq?b`hx^HX2J zIe6ECljRL0uBWb`%{EA=%!i^4sMcj+U_TaTZRb+~GOk z^ZW!nky0n*Wb*r+Q|9H@ml@Z5gU&W`(z4-j!OzC1wOke`TRAYGZVl$PmQ16{3196( zO*?`--I}Qf(2HIwb2&1FB^!faPA2=sLg(@6P4mN)>Dc3i(B0;@O-y2;lM4akD>@^v z=u>*|!s&9zem70g7zfw9FXl1bpJW(C#5w#uy5!V?Q(U35A~$dR%LDVnq@}kQm13{} zd53q3N(s$Eu{R}k2esbftfjfOITCL;jWa$}(mmm}d(&7JZ6d3%IABCapFFYjdEjdK z&4Edqf$G^MNAtL=uCDRs&Fu@FXRgX{*0<(@c3|PNHa>L%zvxWS={L8%qw`STm+=Rd zA}FLspESSIpE_^41~#5yI2bJ=9`oc;GIL!JuW&7YetZ?0H}$$%8rW@*J37L-~Rsx!)8($nI4 zZhcZ2^=Y+p4YPl%j!nFJA|*M^gc(0o$i3nlphe+~-_m}jVkRN{spFs(o0ajW@f3K{ zDV!#BwL322CET$}Y}^0ixYj2w>&Xh12|R8&yEw|wLDvF!lZ#dOTHM9pK6@Nm-@9Lnng4ZHBgBSrr7KI8YCC9DX5Kg|`HsiwJHg2(7#nS;A{b3tVO?Z% za{m5b3rFV6EpX;=;n#wltDv1LE*|g5pQ+OY&*6qCJZc5oDS6Z6JD#6F)bWxZSF@q% z+1WV;m!lRB!n^PC>RgQCI#D1br_o^#iPk>;K2hB~0^<~)?p}LG%kigm@moD#q3PE+ zA^Qca)(xnqw6x>XFhV6ku9r$E>bWNrVH9fum0?4s?Rn2LG{Vm_+QJHse6xa%nzQ?k zKug4PW~#Gtb;#5+9!QBgyB@q=sk9=$S{4T>wjFICStOM?__fr+Kei1 z3j~xPqW;W@YkiUM;HngG!;>@AITg}vAE`M2Pj9Irl4w1fo4w<|Bu!%rh%a(Ai^Zhi zs92>v5;@Y(Zi#RI*ua*h`d_7;byQSa*v9E{2x$<-_=5Z<7{%)}4XExANcz@rK69T0x3%H<@frW>RA8^swA+^a(FxK| zFl3LD*ImHN=XDUkrRhp6RY5$rQ{bRgSO*(vEHYV)3Mo6Jy3puiLmU&g82p{qr0F?ohmbz)f2r{X2|T2 z$4fdQ=>0BeKbiVM!e-lIIs8wVTuC_m7}y4A_%ikI;Wm5$9j(^Y z(cD%U%k)X>_>9~t8;pGzL6L-fmQO@K; zo&vQzMlgY95;1BSkngY)e{`n0!NfVgf}2mB3t}D9@*N;FQ{HZ3Pb%BK6;5#-O|WI( zb6h@qTLU~AbVW#_6?c!?Dj65Now7*pU{h!1+eCV^KCuPAGs28~3k@ueL5+u|Z-7}t z9|lskE`4B7W8wMs@xJa{#bsCGDFoRSNSnmNYB&U7 zVGKWe%+kFB6kb)e;TyHfqtU6~fRg)f|>=5(N36)0+C z`hv65J<$B}WUc!wFAb^QtY31yNleq4dzmG`1wHTj=c*=hay9iD071Hc?oYoUk|M*_ zU1GihAMBsM@5rUJ(qS?9ZYJ6@{bNqJ`2Mr+5#hKf?doa?F|+^IR!8lq9)wS3tF_9n zW_?hm)G(M+MYb?V9YoX^_mu5h-LP^TL^!Q9Z7|@sO(rg_4+@=PdI)WL(B7`!K^ND- z-uIuVDCVEdH_C@c71YGYT^_Scf_dhB8Z2Xy6vGtBSlYud9vggOqv^L~F{BraSE_t} zIkP+Hp2&nH^-MNEs}^`oMLy11`PQW$T|K(`Bu*(f@)mv1-qY(_YG&J2M2<7k;;RK~ zL{Fqj9yCz8(S{}@c)S!65aF<=&eLI{hAMErCx&>i7OeDN>okvegO87OaG{Jmi<|}D zaT@b|0X{d@OIJ7zvT>r+eTzgLq~|Dpu)Z&db-P4z*`M$UL51lf>FLlq6rfG)%doyp z)3kk_YIM!03eQ8Vu_2fg{+osaEJPtJ-s36R+5_AEG12`NG)IQ#TF9c@$99%0iye+ zUzZ57=m2)$D(5Nx!n)=5Au&O0BBgwxIBaeI(mro$#&UGCr<;C{UjJVAbVi%|+WP(a zL$U@TYCxJ=1{Z~}rnW;7UVb7+ZnzgmrogDxhjLGo>c~MiJAWs&&;AGg@%U?Y^0JhL ze(x6Z74JG6FlOFK(T}SXQfhr}RIFl@QXKnIcXYF)5|V~e-}suHILKT-k|<*~Ij|VF zC;t@=uj=hot~*!C68G8hTA%8SzOfETOXQ|3FSaIEjvBJp(A)7SWUi5!Eu#yWgY+;n zlm<$+UDou*V+246_o#V4kMdto8hF%%Lki#zPh}KYXmMf?hrN0;>Mv%`@{0Qn`Ujp) z=lZe+13>^Q!9zT);H<(#bIeRWz%#*}sgUX9P|9($kexOyKIOc`dLux}c$7It4u|Rl z6SSkY*V~g_B-hMPo_ak>>z@AVQ(_N)VY2kB3IZ0G(iDUYw+2d7W^~(Jq}KY=JnWS( z#rzEa&0uNhJ>QE8iiyz;n2H|SV#Og+wEZv=f2%1ELX!SX-(d3tEj$5$1}70Mp<&eI zCkfbByL7af=qQE@5vDVxx1}FSGt_a1DoE3SDI+G)mBAna)KBG4p8Epxl9QZ4BfdAN zFnF|Y(umr;gRgG6NLQ$?ZWgllEeeq~z^ZS7L?<(~O&$5|y)Al^iMKy}&W+eMm1W z7EMU)u^ke(A1#XCV>CZ71}P}0x)4wtHO8#JRG3MA-6g=`ZM!FcICCZ{IEw8Dm2&LQ z1|r)BUG^0GzI6f946RrBlfB1Vs)~8toZf~7)+G;pv&XiUO(%5bm)pl=p>nV^o*;&T z;}@oZSibzto$arQgfkp|z4Z($P>dTXE{4O=vY0!)kDO* zGF8a4wq#VaFpLfK!iELy@?-SeRrdz%F*}hjKcA*y@mj~VD3!it9lhRhX}5YOaR9$} z3mS%$2Be7{l(+MVx3 z(4?h;P!jnRmX9J9sYN#7i=iyj_5q7n#X(!cdqI2lnr8T$IfOW<_v`eB!d9xY1P=2q&WtOXY=D9QYteP)De?S4}FK6#6Ma z=E*V+#s8>L;8aVroK^6iKo=MH{4yEZ_>N-N z`(|;aOATba1^asjxlILk<4}f~`39dBFlxj>Dw(hMYKPO3EEt1@S`1lxFNM+J@uB7T zZ8WKjz7HF1-5&2=l=fqF-*@>n5J}jIxdDwpT?oKM3s8Nr`x8JnN-kCE?~aM1H!hAE z%%w(3kHfGwMnMmNj(SU(w42OrC-euI>Dsjk&jz3ts}WHqmMpzQ3vZrsXrZ|}+MHA7 z068obeXZTsO*6RS@o3x80E4ok``rV^Y3hr&C1;|ZZ0|*EKO`$lECUYG2gVFtUTw)R z4Um<0ZzlON`zTdvVdL#KFoMFQX*a5wM0Czp%wTtfK4Sjs)P**RW&?lP$(<}q%r68Z zS53Y!d@&~ne9O)A^tNrXHhXBkj~$8j%pT1%%mypa9AW5E&s9)rjF4@O3ytH{0z6riz|@< zB~UPh*wRFg2^7EbQrHf0y?E~dHlkOxof_a?M{LqQ^C!i2dawHTPYUE=X@2(3<=OOxs8qn_(y>pU>u^}3y&df{JarR0@VJn0f+U%UiF=$Wyq zQvnVHESil@d|8&R<%}uidGh7@u^(%?$#|&J$pvFC-n8&A>utA=n3#)yMkz+qnG3wd zP7xCnF|$9Dif@N~L)Vde3hW8W!UY0BgT2v(wzp;tlLmyk2%N|0jfG$%<;A&IVrOI< z!L)o>j>;dFaqA3pL}b-Je(bB@VJ4%!JeX@3x!i{yIeIso^=n?fDX`3bU=eG7sTc%g%ye8$v8P@yKE^XD=NYxTb zbf!Mk=h|otpqjFaA-vs5YOF-*GwWPc7VbaOW&stlANnCN8iftFMMrUdYNJ_Bnn5Vt zxfz@Ah|+4&P;reZxp;MmEI7C|FOv8NKUm8njF7Wb6Gi7DeODLl&G~}G4be&*Hi0Qw z5}77vL0P+7-B%UL@3n1&JPxW^d@vVwp?u#gVcJqY9#@-3X{ok#UfW3<1fb%FT`|)V~ggq z(3AUoUS-;7)^hCjdT0Kf{i}h)mBg4qhtHHBti=~h^n^OTH5U*XMgDLIR@sre`AaB$ zg)IGBET_4??m@cx&c~bA80O7B8CHR7(LX7%HThkeC*@vi{-pL%e)yXp!B2InafbDF zjPXf1mko3h59{lT6EEbxKO1Z5GF71)WwowO6kY|6tjSVSWdQ}NsK2x{>i|MKZK8%Q zfu&_0D;CO-Jg0#YmyfctyJ!mRJp)e#@O0mYdp|8x;G1%OZQ3Q847YWTyy|%^cpA;m zze0(5p{tMu^lDkpe?HynyO?a1$_LJl2L&mpeKu%8YvgRNr=%2z${%WThHG=vrWY@4 zsA`OP#O&)TetZ>s%h!=+CE15lOOls&nvC~$Qz0Ph7tHiP;O$i|eDwpT{cp>+)0-|; zY$|bB+Gbel>5aRN3>c0x)4U=|X+z+{ zn*_p*EQoquRL+=+p;=lm`d71&1NqBz&_ph)MXu(Nv6&XE7(RsS)^MGj5Q?Fwude-(sq zjJ>aOq!7!EN>@(fK7EE#;i_BGvli`5U;r!YA{JRodLBc6-`n8K+Fjgwb%sX;j=qHQ z7&Tr!)!{HXoO<2BQrV9Sw?JRaLXV8HrsNevvnf>Y-6|{T!pYLl7jp$-nEE z#X!4G4L#K0qG_4Z;Cj6=;b|Be$hi4JvMH!-voxqx^@8cXp`B??eFBz2lLD8RRaRGh zn7kUfy!YV~p(R|p7iC1Rdgt$_24i0cd-S8HpG|`@my70g^y`gu%#Tf_L21-k?sRRZHK&at(*ED0P8iw{7?R$9~OF$Ko;Iu5)ur5<->x!m93Eb zFYpIx60s=Wxxw=`$aS-O&dCO_9?b1yKiPCQmSQb>T)963`*U+Ydj5kI(B(B?HNP8r z*bfSBpSu)w(Z3j7HQoRjUG(+d=IaE~tv}y14zHHs|0UcN52fT8V_<@2ep_ee{QgZG zmgp8iv4V{k;~8@I%M3<#B;2R>Ef(Gg_cQM7%}0s*^)SK6!Ym+~P^58*wnwV1BW@eG z4sZLqsUvBbFsr#8u7S1r4teQ;t)Y@jnn_m5jS$CsW1um!p&PqAcc8!zyiXHVta9QC zY~wCwCF0U%xiQPD_INKtTb;A|Zf29(mu9NI;E zc-e>*1%(LSXB`g}kd`#}O;veb<(sk~RWL|f3ljxCnEZDdNSTDV6#Td({6l&y4IjKF z^}lIUq*ZUqgTPumD)RrCN{M^jhY>E~1pn|KOZ5((%F)G|*ZQ|r4zIbrEiV%42hJV8 z3xS)=!X1+=olbdGJ=yZil?oXLct8FM{(6ikLL3E%=q#O6(H$p~gQu6T8N!plf!96| z&Q3=`L~>U0zZh;z(pGR2^S^{#PrPxTRHD1RQOON&f)Siaf`GLj#UOk&(|@0?zm;Sx ztsGt8=29-MZs5CSf1l1jNFtNt5rFNZxJPvkNu~2}7*9468TWm>nN9TP&^!;J{-h)_ z7WsHH9|F%I`Pb!>KAS3jQWKfGivTVkMJLO-HUGM_a4UQ_%RgL6WZvrW+Z4ujZn;y@ zz9$=oO!7qVTaQAA^BhX&ZxS*|5dj803M=k&2%QrXda`-Q#IoZL6E(g+tN!6CA!CP* zCpWtCujIea)ENl0liwVfj)Nc<9mV%+e@=d`haoZ*`B7+PNjEbXBkv=B+Pi^~L#EO$D$ZqTiD8f<5$eyb54-(=3 zh)6i8i|jp(@OnRrY5B8t|LFXFQVQ895n*P16cEKTrT*~yLH6Z4e*bZ5otpRDri&+A zfNbK1D5@O=sm`fN=WzWyse!za5n%^+6dHPGX#8DyIK>?9qyX}2XvBWVqbP%%D)7$= z=#$WulZlZR<{m#gU7lwqK4WS1Ne$#_P{b17qe$~UOXCl>5b|6WVh;5vVnR<%d+Lnp z$uEmML38}U4vaW8>shm6CzB(Wei3s#NAWE3)a2)z@i{4jTn;;aQS)O@l{rUM`J@K& l00vQ5JBs~;vo!vr%%-k{2_Fq1Mn4QF81S)AQ99zk{{c4yR+0b! literal 63721 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鯊%1Et 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⁢ zv5o9#E7!G0XmdzfsS{efPNi`1b44~SZ4Z8fuX!I}#8g+(wxzQwUT#Xb2(tbY1+EUhGKoT@KEU9Ktl>_0 z%bjDJg;#*gtJZv!-Zs`?^}v5eKmnbjqlvnSzE@_SP|LG_PJ6CYU+6zY6>92%E+ z=j@TZf-iW4(%U{lnYxQA;7Q!b;^brF8n0D>)`q5>|WDDXLrqYU_tKN2>=#@~OE7grMnNh?UOz-O~6 z6%rHy{#h9K0AT+lDC7q4{hw^|q6*Ry;;L%Q@)Ga}$60_q%D)rv(CtS$CQbpq9|y1e zRSrN4;$Jyl{m5bZw`$8TGvb}(LpY{-cQ)fcyJv7l3S52TLXVDsphtv&aPuDk1OzCA z4A^QtC(!11`IsNx_HnSy?>EKpHJWT^wmS~hc^p^zIIh@9f6U@I2 zC=Mve{j2^)mS#U$e{@Q?SO6%LDsXz@SY+=cK_QMmXBIU)j!$ajc-zLx3V60EXJ!qC zi<%2x8Q24YN+&8U@CIlN zrZkcT9yh%LrlGS9`G)KdP(@9Eo-AQz@8GEFWcb7U=a0H^ZVbLmz{+&M7W(nXJ4sN8 zJLR7eeK(K8`2-}j(T7JsO`L!+CvbueT%izanm-^A1Dn{`1Nw`9P?cq;7no+XfC`K(GO9?O^5zNIt4M+M8LM0=7Gz8UA@Z0N+lg+cX)NfazRu z5D)~HA^(u%w^cz+@2@_#S|u>GpB+j4KzQ^&Wcl9f z&hG#bCA(Yk0D&t&aJE^xME^&E-&xGHhXn%}psEIj641H+Nl-}boj;)Zt*t(4wZ5DN z@GXF$bL=&pBq-#vkTkh>7hl%K5|3 z{`Vn9b$iR-SoGENp}bn4;fR3>9sA%X2@1L3aE9yTra;Wb#_`xWwLSLdfu+PAu+o3| zGVnpzPr=ch{uuoHjtw7+_!L_2;knQ!DuDl0R`|%jr+}jFzXtrHIKc323?JO{l&;VF z*L1+}JU7%QJOg|5|Tc|D8fN zJORAg=_vsy{ak|o);@)Yh8Lkcg@$FG3k@ep36BRa^>~UmnRPziS>Z=`Jb2x*Q#`%A zU*i3&Vg?TluO@X0O;r2Jl6LKLUOVhSqg1*qOt^|8*c7 zo(298@+r$k_wQNGHv{|$tW(T8L+4_`FQ{kEW5Jgg{yf7ey4ss_(SNKfz(N9lx&a;< je(UuV8hP?p&}TPdm1I$XmG#(RzlD&B2izSj9sl%y5~4qc diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e6aba251..2ea3535d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 30413086fc4331958a81f92fa380d29386fbce11 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Mar 2024 20:13:58 +0000 Subject: [PATCH 21/43] Bump kotlin_version from 1.9.22 to 1.9.23 (#2466) --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index ec19ee49..f245e71b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - kotlin_version = '1.9.22' + kotlin_version = '1.9.23' about_libraries = '11.1.0' hilt_version = '2.51' } @@ -13,7 +13,7 @@ buildscript { dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" - classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$kotlin_version-1.0.16" + classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$kotlin_version-1.0.19" classpath 'com.android.tools.build:gradle:8.2.2' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.4.1' From a71ef4a4b24a7c6dbcfca28415debd727757b7bc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Mar 2024 20:14:33 +0000 Subject: [PATCH 22/43] Bump com.google.firebase:firebase-bom from 32.7.3 to 32.7.4 (#2467) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 31de5104..ef347661 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -252,7 +252,7 @@ dependencies { implementation 'com.fredporciuncula:flow-preferences:1.9.1' implementation 'org.apache.commons:commons-text:1.11.0' - playImplementation platform('com.google.firebase:firebase-bom:32.7.3') + playImplementation platform('com.google.firebase:firebase-bom:32.7.4') playImplementation 'com.google.firebase:firebase-analytics' playImplementation 'com.google.firebase:firebase-messaging' playImplementation 'com.google.firebase:firebase-crashlytics:' From a7c2009e4971891f8e756ccca504cddee727c689 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Mar 2024 20:16:03 +0000 Subject: [PATCH 23/43] Bump com.google.android.gms:play-services-ads from 22.6.0 to 23.0.0 (#2469) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index ef347661..3afd068b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -258,7 +258,7 @@ dependencies { playImplementation 'com.google.firebase:firebase-crashlytics:' playImplementation 'com.google.firebase:firebase-config' - playImplementation 'com.google.android.gms:play-services-ads:22.6.0' + playImplementation 'com.google.android.gms:play-services-ads:23.0.0' playImplementation "com.google.android.play:integrity:1.3.0" playImplementation 'com.google.android.play:app-update-ktx:2.1.0' playImplementation 'com.google.android.play:review-ktx:2.0.1' From 5dd5697f650ac6e2fd44103692bfe499e5c64437 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sat, 9 Mar 2024 10:03:36 +0100 Subject: [PATCH 24/43] Remove firebase disable flag (#2471) --- .circleci/config.yml | 2 +- .github/workflows/deploy-store.yml | 2 +- .github/workflows/deploy-test.yml | 5 +- .gitignore | 13 ++-- .travis.yml | 2 +- app/build.gradle | 6 +- app/{src/debug => }/google-services.json | 0 app/src/debug/agconnect-services.json | 92 ------------------------ app/src/main/AndroidManifest.xml | 24 ------- app/src/release/google-services.json | 42 ----------- 10 files changed, 14 insertions(+), 174 deletions(-) rename app/{src/debug => }/google-services.json (100%) delete mode 100644 app/src/debug/agconnect-services.json delete mode 100644 app/src/release/google-services.json diff --git a/.circleci/config.yml b/.circleci/config.yml index ce2922ba..2cb2e147 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -162,7 +162,7 @@ jobs: openssl aes-256-cbc -d -in ./app/upload-key-encrypted.jks -k $ENCRYPT_KEY >> ./app/upload-key.jks - run: name: Publish release - command: ./gradlew publishPlayRelease --no-daemon --stacktrace --console=plain -PenableCrashlytics -PdisablePreDex + command: ./gradlew publishPlayRelease --no-daemon --stacktrace --console=plain -PdisablePreDex workflows: version: 2 diff --git a/.github/workflows/deploy-store.yml b/.github/workflows/deploy-store.yml index e8237a38..0195f3e5 100644 --- a/.github/workflows/deploy-store.yml +++ b/.github/workflows/deploy-store.yml @@ -40,7 +40,7 @@ jobs: SINGLE_SUPPORT_AD_ID: ${{ secrets.SINGLE_SUPPORT_AD_ID }} DASHBOARD_TILE_AD_ID: ${{ secrets.DASHBOARD_TILE_AD_ID }} SET_BUILD_TIMESTAMP: ${{ secrets.SET_BUILD_TIMESTAMP }} - run: ./gradlew publishPlayReleaseApps -PenableFirebase --stacktrace; + run: ./gradlew publishPlayReleaseApps --stacktrace; deploy-app-gallery: name: AppGallery diff --git a/.github/workflows/deploy-test.yml b/.github/workflows/deploy-test.yml index c4f55e6a..42c1f8e7 100644 --- a/.github/workflows/deploy-test.yml +++ b/.github/workflows/deploy-test.yml @@ -36,8 +36,7 @@ jobs: - name: Prepare build configuration run: | sed -i -e "s#applicationIdSuffix \".dev\"#applicationIdSuffix \".${GITHUB_HEAD_REF//[-.\/]/_}\"#" app/build.gradle - sed -i -e "s#.dev\"#.${GITHUB_HEAD_REF//[-.\/]/_}\"#" app/src/debug/google-services.json - sed -i -e "s#.dev\"#.${GITHUB_HEAD_REF//[-.\/]/_}\"#" app/src/debug/agconnect-services.json + sed -i -e "s#.dev\"#.${GITHUB_HEAD_REF//[-.\/]/_}\"#" app/google-services.json sed -i -e '/versionNameSuffix/d' app/build.gradle - name: Add signing config run: | @@ -131,7 +130,7 @@ jobs: BITRISE_KEYSTORE_PASSWORD: ${{ secrets.BITRISE_KEYSTORE_PASSWORD }} BITRISE_KEY_ALIAS: ${{ secrets.BITRISE_KEY_ALIAS }} BITRISE_KEY_PASSWORD: ${{ secrets.BITRISE_KEY_PASSWORD }} - run: ./gradlew assemblePlayDebug -PenableFirebase --stacktrace + run: ./gradlew assemblePlayDebug --stacktrace - name: Upload apk to github artifacts uses: actions/upload-artifact@v3 with: diff --git a/.gitignore b/.gitignore index c014204d..ee435baa 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,8 @@ captures/ .idea/migrations.xml .idea/androidTestResultsUserPreferences.xml .idea/copilot +.idea/deploymentTargetDropDown.xml +.idea/kotlinc.xml # Keystore files *.jks @@ -114,12 +116,13 @@ Thumbs.db *.ear ### AndroidStudio Patch ### - !/gradle/wrapper/gradle-wrapper.jar .idea/jarRepositories.xml +### Services config files +agconnect-services.json +agconnect-credentials.json +google-services.json +!app/google-services.json + -app/src/release/agconnect-services.json -app/src/release/agconnect-credentials.json -.idea/deploymentTargetDropDown.xml -.idea/kotlinc.xml diff --git a/.travis.yml b/.travis.yml index 04db3a61..e0b0be97 100644 --- a/.travis.yml +++ b/.travis.yml @@ -61,7 +61,7 @@ script: gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/agconnect-services.json.gpg; gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/key.p12.gpg; gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg; - ./gradlew publishPlayRelease -PenableFirebase --stacktrace; + ./gradlew publishPlayRelease --stacktrace; fi after_success: diff --git a/app/build.gradle b/app/build.gradle index 3afd068b..d1c67ab0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -32,10 +32,7 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" - manifestPlaceholders = [ - firebase_enabled: project.hasProperty("enableFirebase"), - admob_project_id: "" - ] + manifestPlaceholders = [admob_project_id: ""] buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "null" buildConfigField "String", "DASHBOARD_TILE_AD_ID", "null" @@ -76,7 +73,6 @@ android { resValue "string", "app_name", "Wulkanowy DEV" applicationIdSuffix ".dev" versionNameSuffix "-dev" - ext.enableCrashlytics = project.hasProperty("enableFirebase") buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\"" buildConfigField "String", "SCHOOLS_BASE_URL", '"https://schools.wulkanowy.net.pl"' } diff --git a/app/src/debug/google-services.json b/app/google-services.json similarity index 100% rename from app/src/debug/google-services.json rename to app/google-services.json diff --git a/app/src/debug/agconnect-services.json b/app/src/debug/agconnect-services.json deleted file mode 100644 index 52426f54..00000000 --- a/app/src/debug/agconnect-services.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "agcgw": { - "backurl": "connect-dre.hispace.hicloud.com", - "url": "connect-dre.dbankcloud.cn", - "websocketbackurl": "connect-ws-dre.hispace.dbankcloud.com", - "websocketurl": "connect-ws-dre.hispace.dbankcloud.cn" - }, - "agcgw_all": { - "CN": "connect-drcn.dbankcloud.cn", - "CN_back": "connect-drcn.hispace.hicloud.com", - "DE": "connect-dre.dbankcloud.cn", - "DE_back": "connect-dre.hispace.hicloud.com", - "RU": "connect-drru.hispace.dbankcloud.ru", - "RU_back": "connect-drru.hispace.dbankcloud.cn", - "SG": "connect-dra.dbankcloud.cn", - "SG_back": "connect-dra.hispace.hicloud.com" - }, - "websocketgw_all": { - "CN": "connect-ws-drcn.hispace.dbankcloud.cn", - "CN_back": "connect-ws-drcn.hispace.dbankcloud.com", - "DE": "connect-ws-dre.hispace.dbankcloud.cn", - "DE_back": "connect-ws-dre.hispace.dbankcloud.com", - "RU": "connect-ws-drru.hispace.dbankcloud.ru", - "RU_back": "connect-ws-drru.hispace.dbankcloud.cn", - "SG": "connect-ws-dra.hispace.dbankcloud.cn", - "SG_back": "connect-ws-dra.hispace.dbankcloud.com" - }, - "client": { - "cp_id": "890048000024105546", - "product_id": "736430079244736562", - "client_id": "514530959291319360", - "client_secret": "C42522DBF17D3D4BBE9D9C1783A54484B7E6844B388B7A67502D36A633A4186B", - "project_id": "736430079244736562", - "app_id": "106552551", - "api_key": "CgB6e3x9BUNiq+r8ebCHNojjjYsMT4pJSjjNDOkm9owtBb6rVI6LjnASoZBRxbjjhObcrV5gANo99fI/eKZDTbWS", - "package_name": "io.github.wulkanowy.dev" - }, - "oauth_client": { - "client_id": "106552551", - "client_type": 1 - }, - "app_info": { - "app_id": "106552551", - "package_name": "io.github.wulkanowy.dev" - }, - "service": { - "analytics": { - "collector_url": "datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn", - "collector_url_ru": "datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com", - "collector_url_sg": "datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn", - "collector_url_de": "datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn", - "collector_url_cn": "datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn", - "resource_id": "p1", - "channel_id": "" - }, - "search":{ - "url":"https://search-dre.cloud.huawei.com" - }, - "cloudstorage": { - "storage_url_sg_back": "https://agc-storage-dra.cloud.huawei.asia", - "storage_url_ru_back": "https://agc-storage-drru.cloud.huawei.ru", - "storage_url_ru": "https://agc-storage-drru.cloud.huawei.ru", - "storage_url_de_back": "https://agc-storage-dre.cloud.huawei.eu", - "storage_url_de": "https://ops-dre.agcstorage.link", - "storage_url": "https://agc-storage-drcn.platform.dbankcloud.cn", - "storage_url_sg": "https://ops-dra.agcstorage.link", - "storage_url_cn_back": "https://agc-storage-drcn.cloud.huawei.com.cn", - "storage_url_cn": "https://agc-storage-drcn.platform.dbankcloud.cn" - }, - "ml": { - "mlservice_url": "ml-api-dre.ai.dbankcloud.com,ml-api-dre.ai.dbankcloud.cn" - } - }, - "region": "DE", - "configuration_version": "3.0", - "appInfos": [ - { - "package_name": "io.github.wulkanowy.dev", - "client": { - "app_id": "106552551" - }, - "app_info": { - "package_name": "io.github.wulkanowy.dev", - "app_id": "106552551" - }, - "oauth_client": { - "client_type": 1, - "client_id": "106552551" - } - } - ] -} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f43dfdd2..4e617c93 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -155,33 +155,9 @@ android:resource="@xml/provider_paths" /> - - - - - - - - - diff --git a/app/src/release/google-services.json b/app/src/release/google-services.json deleted file mode 100644 index ebd157e1..00000000 --- a/app/src/release/google-services.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "project_info": { - "project_number": "", - "firebase_url": "", - "project_id": "", - "storage_bucket": "" - }, - "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:1091101852179:android:b558a25f65d088b1", - "android_client_info": { - "package_name": "io.github.wulkanowy" - } - }, - "oauth_client": [ - { - "client_id": "", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "" - } - ], - "services": { - "analytics_service": { - "status": 1 - }, - "appinvite_service": { - "status": 1, - "other_platform_oauth_client": [] - }, - "ads_service": { - "status": 2 - } - } - } - ], - "configuration_version": "1" -} From 4ef9fb1f2866247007f0181726814be71ac820cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sat, 9 Mar 2024 10:05:12 +0100 Subject: [PATCH 25/43] Update preferences strings (#2472) --- app/src/main/res/values/preferences_values.xml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values/preferences_values.xml b/app/src/main/res/values/preferences_values.xml index 2475e491..92048202 100644 --- a/app/src/main/res/values/preferences_values.xml +++ b/app/src/main/res/values/preferences_values.xml @@ -1,5 +1,8 @@ + + Alphabetically + @string/dashboard_title @string/grade_title @@ -80,9 +83,9 @@ - Alphabetically + @string/general_alphabetically By attendance percentage - By lesson balance + By subject attendance balance alphabetic @@ -91,7 +94,7 @@ - Alphabetically + @string/general_alphabetically By date By average From c72cc39920226bca53c0114fdd9e69673bde90a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sat, 9 Mar 2024 21:01:58 +0100 Subject: [PATCH 26/43] Separate strings from array to avoid duplications (#2473) --- .../main/res/values-cs/preferences_values.xml | 5 -- .../res/values-da-rDK/preferences_values.xml | 70 ------------------- .../main/res/values-de/preferences_values.xml | 5 -- .../res/values-es-rES/preferences_values.xml | 70 ------------------- .../res/values-it-rIT/preferences_values.xml | 70 ------------------- .../main/res/values-pl/preferences_values.xml | 5 -- .../main/res/values-ru/preferences_values.xml | 5 -- .../main/res/values-sk/preferences_values.xml | 5 -- .../main/res/values-uk/preferences_values.xml | 5 -- .../main/res/values/preferences_values.xml | 22 +++--- 10 files changed, 13 insertions(+), 249 deletions(-) delete mode 100644 app/src/main/res/values-da-rDK/preferences_values.xml delete mode 100644 app/src/main/res/values-es-rES/preferences_values.xml delete mode 100644 app/src/main/res/values-it-rIT/preferences_values.xml diff --git a/app/src/main/res/values-cs/preferences_values.xml b/app/src/main/res/values-cs/preferences_values.xml index 1590c47a..5d6c5e8c 100644 --- a/app/src/main/res/values-cs/preferences_values.xml +++ b/app/src/main/res/values-cs/preferences_values.xml @@ -31,11 +31,6 @@ 0,5 0,75 - - Abecedně - Podle data - Podle průměru - Dzienniczek+ Wulkanowy diff --git a/app/src/main/res/values-da-rDK/preferences_values.xml b/app/src/main/res/values-da-rDK/preferences_values.xml deleted file mode 100644 index 5aff12de..00000000 --- a/app/src/main/res/values-da-rDK/preferences_values.xml +++ /dev/null @@ -1,70 +0,0 @@ - - - - Light - Dark - Black (AMOLED) - - - System language - Polski - English - Pусский - Українська - Deutsch - Čeština - Slovenčina - - - 15 minutes - 30 minutes - 1 hour - 2 hours - 6 hours - 12 hours - 24 hours - - - 0,00 - 0,25 - 0,33 - 0,5 - 0,75 - - - Alphabetically - By date - By average - - - Dzienniczek+ - Wulkanowy - Grade colors in register - - - Up to 1 at once - Always expanded - Unlimited expansions - - - Average of grades only from selected semester - Average of averages from both semesters - Average of grades from the whole year - - - Don\'t show - Only between lessons - Before and between lessons - - - Lucky number - Unread messages - Attendance - Lessons - Grades - Homework - School announcements - Exams - Conferences - - diff --git a/app/src/main/res/values-de/preferences_values.xml b/app/src/main/res/values-de/preferences_values.xml index d1001c74..17c19e7d 100644 --- a/app/src/main/res/values-de/preferences_values.xml +++ b/app/src/main/res/values-de/preferences_values.xml @@ -31,11 +31,6 @@ 0,5 0,75 - - Alphabetisch - Nach Datum - Nach Durchschnitt - Dzienniczek+ Wulkanowy diff --git a/app/src/main/res/values-es-rES/preferences_values.xml b/app/src/main/res/values-es-rES/preferences_values.xml deleted file mode 100644 index 5aff12de..00000000 --- a/app/src/main/res/values-es-rES/preferences_values.xml +++ /dev/null @@ -1,70 +0,0 @@ - - - - Light - Dark - Black (AMOLED) - - - System language - Polski - English - Pусский - Українська - Deutsch - Čeština - Slovenčina - - - 15 minutes - 30 minutes - 1 hour - 2 hours - 6 hours - 12 hours - 24 hours - - - 0,00 - 0,25 - 0,33 - 0,5 - 0,75 - - - Alphabetically - By date - By average - - - Dzienniczek+ - Wulkanowy - Grade colors in register - - - Up to 1 at once - Always expanded - Unlimited expansions - - - Average of grades only from selected semester - Average of averages from both semesters - Average of grades from the whole year - - - Don\'t show - Only between lessons - Before and between lessons - - - Lucky number - Unread messages - Attendance - Lessons - Grades - Homework - School announcements - Exams - Conferences - - diff --git a/app/src/main/res/values-it-rIT/preferences_values.xml b/app/src/main/res/values-it-rIT/preferences_values.xml deleted file mode 100644 index 5aff12de..00000000 --- a/app/src/main/res/values-it-rIT/preferences_values.xml +++ /dev/null @@ -1,70 +0,0 @@ - - - - Light - Dark - Black (AMOLED) - - - System language - Polski - English - Pусский - Українська - Deutsch - Čeština - Slovenčina - - - 15 minutes - 30 minutes - 1 hour - 2 hours - 6 hours - 12 hours - 24 hours - - - 0,00 - 0,25 - 0,33 - 0,5 - 0,75 - - - Alphabetically - By date - By average - - - Dzienniczek+ - Wulkanowy - Grade colors in register - - - Up to 1 at once - Always expanded - Unlimited expansions - - - Average of grades only from selected semester - Average of averages from both semesters - Average of grades from the whole year - - - Don\'t show - Only between lessons - Before and between lessons - - - Lucky number - Unread messages - Attendance - Lessons - Grades - Homework - School announcements - Exams - Conferences - - diff --git a/app/src/main/res/values-pl/preferences_values.xml b/app/src/main/res/values-pl/preferences_values.xml index 2f2432e9..70b81294 100644 --- a/app/src/main/res/values-pl/preferences_values.xml +++ b/app/src/main/res/values-pl/preferences_values.xml @@ -31,11 +31,6 @@ 0,5 0,75 - - Alfabetycznie - Według daty - Według średniej - Dzienniczek+ Wulkanowy diff --git a/app/src/main/res/values-ru/preferences_values.xml b/app/src/main/res/values-ru/preferences_values.xml index df3629c0..16b337a7 100644 --- a/app/src/main/res/values-ru/preferences_values.xml +++ b/app/src/main/res/values-ru/preferences_values.xml @@ -31,11 +31,6 @@ 0,5 0,75 - - В алфавитном порядке - По дате - По средней - Dzienniczek+ Wulkanowy diff --git a/app/src/main/res/values-sk/preferences_values.xml b/app/src/main/res/values-sk/preferences_values.xml index 6cd22154..c9862751 100644 --- a/app/src/main/res/values-sk/preferences_values.xml +++ b/app/src/main/res/values-sk/preferences_values.xml @@ -31,11 +31,6 @@ 0,5 0,75 - - Abecedne - Podľa dátumu - Podľa priemeru - Dzienniczek+ Wulkanowy diff --git a/app/src/main/res/values-uk/preferences_values.xml b/app/src/main/res/values-uk/preferences_values.xml index c02efb54..f0cfdd12 100644 --- a/app/src/main/res/values-uk/preferences_values.xml +++ b/app/src/main/res/values-uk/preferences_values.xml @@ -31,11 +31,6 @@ 0,5 0,75 - - За алфавітом - За датою - За середньою - Dzienniczek+ Wulkanowy diff --git a/app/src/main/res/values/preferences_values.xml b/app/src/main/res/values/preferences_values.xml index 92048202..b588ea5e 100644 --- a/app/src/main/res/values/preferences_values.xml +++ b/app/src/main/res/values/preferences_values.xml @@ -1,7 +1,11 @@ - Alphabetically + Alphabetically + By date + By average + By attendance percentage + By subject attendance balance @string/dashboard_title @@ -82,10 +86,10 @@ 0.75 - - @string/general_alphabetically - By attendance percentage - By subject attendance balance + + @string/sort_alphabetically + @string/sort_by_attendance_percentage + @string/sort_by_subject_attendance_balance alphabetic @@ -93,10 +97,10 @@ lesson_balance - - @string/general_alphabetically - By date - By average + + @string/sort_alphabetically + @string/sort_by_date + @string/sort_by_average alphabetic From 38c00ddab5815c3e430260c8c2bc6124cbd4c6e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Mon, 11 Mar 2024 11:44:59 +0100 Subject: [PATCH 27/43] Fix task description color crash (#2475) --- .gitignore | 1 + .../io/github/wulkanowy/ui/base/BaseActivity.kt | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index ee435baa..ad83ced8 100644 --- a/.gitignore +++ b/.gitignore @@ -69,6 +69,7 @@ captures/ .idea/androidTestResultsUserPreferences.xml .idea/copilot .idea/deploymentTargetDropDown.xml +.idea/deploymentTargetSelector.xml .idea/kotlinc.xml # Keystore files diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt index 10735dab..922c3536 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.ui.base import android.app.ActivityManager +import android.os.Build import android.os.Bundle import android.view.View import android.widget.Toast @@ -45,11 +46,19 @@ abstract class BaseActivity, VB : ViewBinding> : themeManager.applyActivityTheme(this) super.onCreate(savedInstanceState) supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleLogger, true) + applyCustomTaskDescription() + } - @Suppress("DEPRECATION") - setTaskDescription( - ActivityManager.TaskDescription(null, null, getThemeAttrColor(R.attr.colorSurface)) - ) + @Suppress("DEPRECATION") + private fun applyCustomTaskDescription() { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) return + try { + val newColor = getThemeAttrColor(R.attr.colorSurface) + val taskDescription = ActivityManager.TaskDescription(null, null, newColor) + setTaskDescription(taskDescription) + } catch (e: Exception) { + Timber.e(e) + } } override fun showError(text: String, error: Throwable) { From 0e99c81eb8c58ffe62ff0460103737c615a04059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Mon, 11 Mar 2024 11:45:15 +0100 Subject: [PATCH 28/43] Add missing onDetachView in AutDialog (#2476) --- .../java/io/github/wulkanowy/ui/modules/auth/AuthDialog.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthDialog.kt index fa29df47..0f7c4234 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthDialog.kt @@ -78,4 +78,9 @@ class AuthDialog : BaseDialogFragment(), AuthView { override fun showDescriptionWithName(name: String) { binding.authDescription.text = getString(R.string.auth_description, name).parseAsHtml() } + + override fun onDestroyView() { + presenter.onDetachView() + super.onDestroyView() + } } From 88def5eff8ccc2a5575e070e755c448bfb4b3277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Mon, 11 Mar 2024 11:45:28 +0100 Subject: [PATCH 29/43] Remove savedInstance in MessagePreviewFragment (#2477) --- .../mailboxchooser/MailboxChooserDialog.kt | 1 - .../message/preview/MessagePreviewFragment.kt | 11 +++-------- .../message/preview/MessagePreviewPresenter.kt | 13 +++++++++---- .../github/wulkanowy/utils/BundleExtension.kt | 17 +++++++++-------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserDialog.kt index 8bd84f2b..11d3c6c1 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserDialog.kt @@ -47,7 +47,6 @@ class MailboxChooserDialog : BaseDialogFragment(), } - @Suppress("UNCHECKED_CAST") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) presenter.onAttachView( diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt index ebdb96a4..8e7c7276 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt @@ -82,10 +82,10 @@ class MessagePreviewFragment : get() = getString(R.string.message_not_exists) companion object { - const val MESSAGE_ID_KEY = "message_id" + private const val MESSAGE_ARG_KEY = "message" fun newInstance(message: Message) = MessagePreviewFragment().apply { - arguments = bundleOf(MESSAGE_ID_KEY to message) + arguments = bundleOf(MESSAGE_ARG_KEY to message) } } @@ -101,7 +101,7 @@ class MessagePreviewFragment : messageContainer = binding.messagePreviewContainer presenter.onAttachView( view = this, - message = (savedInstanceState ?: arguments)?.serializable(MESSAGE_ID_KEY), + message = requireArguments().serializable(MESSAGE_ARG_KEY), ) } @@ -233,11 +233,6 @@ class MessagePreviewFragment : (activity as MainActivity).popView() } - override fun onSaveInstanceState(outState: Bundle) { - outState.putSerializable(MESSAGE_ID_KEY, presenter.messageWithAttachments?.message) - super.onSaveInstanceState(outState) - } - override fun onDestroyView() { presenter.onDetachView() super.onDestroyView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt index 9bb0d32a..3b3b2b42 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt @@ -3,10 +3,15 @@ package io.github.wulkanowy.ui.modules.message.preview import android.annotation.SuppressLint import androidx.core.text.parseAsHtml import io.github.wulkanowy.R -import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.MessageWithAttachment import io.github.wulkanowy.data.enums.MessageFolder +import io.github.wulkanowy.data.flatResourceFlow +import io.github.wulkanowy.data.logResourceStatus +import io.github.wulkanowy.data.onResourceData +import io.github.wulkanowy.data.onResourceError +import io.github.wulkanowy.data.onResourceNotLoading +import io.github.wulkanowy.data.onResourceSuccess import io.github.wulkanowy.data.repositories.MessageRepository import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository @@ -28,17 +33,17 @@ class MessagePreviewPresenter @Inject constructor( private val analytics: AnalyticsHelper ) : BasePresenter(errorHandler, studentRepository) { - var messageWithAttachments: MessageWithAttachment? = null + private var messageWithAttachments: MessageWithAttachment? = null private lateinit var lastError: Throwable private var retryCallback: () -> Unit = {} - fun onAttachView(view: MessagePreviewView, message: Message?) { + fun onAttachView(view: MessagePreviewView, message: Message) { super.onAttachView(view) view.initView() errorHandler.showErrorMessage = ::showErrorViewOnError - loadData(requireNotNull(message)) + loadData(message) } private fun onMessageLoadRetry(message: Message) { diff --git a/app/src/main/java/io/github/wulkanowy/utils/BundleExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/BundleExtension.kt index d3c9f800..b1742b4f 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/BundleExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/BundleExtension.kt @@ -4,30 +4,31 @@ import android.content.Intent import android.os.Build import android.os.Bundle import android.os.Parcelable +import androidx.core.os.BundleCompat import java.io.Serializable +// Even though API was introduced in 33, we use 34 as 33 is bugged in some scenarios. + inline fun Bundle.serializable(key: String): T = when { - Build.VERSION.SDK_INT >= 33 -> getSerializable(key, T::class.java)!! + Build.VERSION.SDK_INT >= 34 -> getSerializable(key, T::class.java)!! else -> @Suppress("DEPRECATION") getSerializable(key) as T } inline fun Bundle.nullableSerializable(key: String): T? = when { - Build.VERSION.SDK_INT >= 33 -> getSerializable(key, T::class.java) + Build.VERSION.SDK_INT >= 34 -> getSerializable(key, T::class.java) else -> @Suppress("DEPRECATION") getSerializable(key) as T? } @Suppress("UNCHECKED_CAST") -inline fun Bundle.parcelableArray(key: String): Array? = when { - Build.VERSION.SDK_INT >= 33 -> getParcelableArray(key, T::class.java) - else -> @Suppress("DEPRECATION") getParcelableArray(key) as Array? -} +inline fun Bundle.parcelableArray(key: String): Array? = + BundleCompat.getParcelableArray(this, key, T::class.java) as Array? inline fun Intent.serializable(key: String): T = when { - Build.VERSION.SDK_INT >= 33 -> getSerializableExtra(key, T::class.java)!! + Build.VERSION.SDK_INT >= 34 -> getSerializableExtra(key, T::class.java)!! else -> @Suppress("DEPRECATION") getSerializableExtra(key) as T } inline fun Intent.nullableSerializable(key: String): T? = when { - Build.VERSION.SDK_INT >= 33 -> getSerializableExtra(key, T::class.java) + Build.VERSION.SDK_INT >= 34 -> getSerializableExtra(key, T::class.java) else -> @Suppress("DEPRECATION") getSerializableExtra(key) as T? } From eb6fdd900e9ba393b1fe3fd00439a0b1f2f4468a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Mon, 11 Mar 2024 11:47:13 +0100 Subject: [PATCH 30/43] New Crowdin updates (#2470) --- .../main/res/values-cs/preferences_values.xml | 7 +++- app/src/main/res/values-cs/strings.xml | 41 +++++++++++-------- .../main/res/values-de/preferences_values.xml | 5 +++ app/src/main/res/values-de/strings.xml | 7 ++++ .../main/res/values-pl/preferences_values.xml | 5 +++ app/src/main/res/values-pl/strings.xml | 10 +++-- .../main/res/values-ru/preferences_values.xml | 5 +++ app/src/main/res/values-ru/strings.xml | 7 ++++ .../main/res/values-sk/preferences_values.xml | 7 +++- app/src/main/res/values-sk/strings.xml | 41 +++++++++++-------- .../main/res/values-uk/preferences_values.xml | 5 +++ app/src/main/res/values-uk/strings.xml | 7 ++++ 12 files changed, 108 insertions(+), 39 deletions(-) diff --git a/app/src/main/res/values-cs/preferences_values.xml b/app/src/main/res/values-cs/preferences_values.xml index 5d6c5e8c..5e488e40 100644 --- a/app/src/main/res/values-cs/preferences_values.xml +++ b/app/src/main/res/values-cs/preferences_values.xml @@ -1,5 +1,10 @@ + Abecedně + Podle data + Podle průměru + Podle procenta docházky + Podle rovnováhy docházky předmětu Světlý Tmavý @@ -54,7 +59,7 @@ Šťastné číslo Nepřečtené zprávy - Frekvence + Docházka Lekce Známky Domácí úkoly diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 58c1d7d3..780d9351 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -4,7 +4,7 @@ Přihlášení Wulkanowy Známky - Frekvence + Docházka Zkoušky Plán lekce Nastavení @@ -64,7 +64,7 @@ Symbol najdete na stránce deníku v  Uczeń→ Dostęp Mobilny → Wygeneruj kod dostępu.\n\nUjistěte se, že jste nastavili správnou variantu deníku v poli Variace deníku UONET+ na první přihlašovací obrazovce Vyberte žáky, kteří se mají do aplikace přihlásit Jiné možnosti - V tomto režimu nefungují následující: šťastné číslo, statistiky třídy, shrnutí frekvencí, ospravedlnění nepřítomnosti, dokončené lekce, informace o škole a prohlížení seznamu registrovaných zařízení + V tomto režimu nefungují následující: šťastné číslo, statistiky třídy, shrnutí docházky, ospravedlnění nepřítomnosti, dokončené lekce, informace o škole a prohlížení seznamu registrovaných zařízení Tento režim zobrazuje stejná data, která se zobrazují na webových stránkách deníka Kombinace nejlepších vlastností ostatních dvou režimů. Funguje rychleji než scraper a poskytuje funkce, které nejsou k dispozici v režimu Mobile API. Je to v experimentální fázi Ochrana osobních údajů @@ -264,7 +264,12 @@ Čas ukončení Čas ukončení musí být pozdější než čas zahájení - Shrnutí frekvencí + Shrnutí docházky + Kalkulačka docházky + %1$d nad cílem + přesně v cíli + %1$d pod cílem + %1$d/%2$d přítomnosti Nepřítomnost ze školních důvodů Omluvená nepřítomnost Neomluvená nepřítomnost @@ -282,22 +287,22 @@ Musíte vybrat alespoň jednu nepřítomnost! Ospravedlnit - Nové frekvence - Nové frekvence - Nové frekvence - Nové frekvence + Nová docházka + Nové docházky + Nové docházky + Nové docházky - %1$d nové frekvence - %1$d nové frekvence - %1$d nových frekvencí - %1$d nových frekvencí + %1$d nová docházka + %1$d nové docházky + %1$d nových docházek + %1$d nových docházek - %d frekvence - %d frekvence - %d frekvencí - %d frekvencí + %d docházka + %d docházky + %d docházek + %d docházek Společně @@ -731,6 +736,8 @@ Možnosti vypočítaného průměru Vynutit průměrný výpočet podle aplikace Zobrazit přítomnost + Cílová docházka + Třídění kalkulačky docházky Motiv Rozvíjení známek Zobrazit skupiny vedle předmětů @@ -797,7 +804,7 @@ Známky Domů Viditelnost dlaždic - Frekvence + Docházka Plán lekce Známky Vypočítaný průměr @@ -825,7 +832,7 @@ Nadcházející lekce Ladění Změny plánu lekcí - Nové frekvence + Nové docházky Černá Červená diff --git a/app/src/main/res/values-de/preferences_values.xml b/app/src/main/res/values-de/preferences_values.xml index 17c19e7d..0170acfa 100644 --- a/app/src/main/res/values-de/preferences_values.xml +++ b/app/src/main/res/values-de/preferences_values.xml @@ -1,5 +1,10 @@ + Alphabetically + By date + By average + By attendance percentage + By subject attendance balance Licht Dunkel diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index daabc7d8..7bc5aa99 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -237,6 +237,11 @@ Endzeit muss grösser sein als Startzeit Übersicht über die Schulbesuch + Attendance calculator + %1$d over target + right on target + %1$d under target + %1$d/%2$d presences Aus schulischen Gründen abwesend Entschuldigte Abwesenheit Unentschuldigtes Abwesenheit @@ -637,6 +642,8 @@ Berechnete Durchschnittsoptionen Mittelwertberechnung durch App erzwingen Anwesendheit zeigen + Attendance target + Attendance calculator sorting Thema Steigende Sorten Gruppen neben Schulfächen anzeigen diff --git a/app/src/main/res/values-pl/preferences_values.xml b/app/src/main/res/values-pl/preferences_values.xml index 70b81294..4df60b51 100644 --- a/app/src/main/res/values-pl/preferences_values.xml +++ b/app/src/main/res/values-pl/preferences_values.xml @@ -1,5 +1,10 @@ + Alfabetycznie + Według daty + Według średniej + Według procentu obecności + Według balansu frekwencji przedmiotu Jasny Ciemny diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 91a00e51..d1d603b6 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -265,6 +265,11 @@ Godzina zakończenia musi być późniejsza niż godzina rozpoczęcia Podsumowanie frekwencji + Kalkulator obecności + %1$d powyżej celu + dokładnie u celu + %1$d poniżej celu + %1$d/%2$d obecności Nieobecność z przyczyn szkolnych Nieobecność usprawiedliwiona Nieobecność nieusprawiedliwiona @@ -731,6 +736,8 @@ Opcje obliczonej średniej Wymuś obliczanie średniej przez aplikację Pokazuj obecność + Docelowa obecność + Sortowanie kalkulatora obecności Motyw Rozwijanie ocen Pokazuj grupę obok przedmiotu @@ -876,7 +883,4 @@ Wyłącz wyciszenie Wyciszyleś tego użytkownika Wyłączyłeś wyciszenie tego użytkownika - - Docelowa frekwencja (w %) - Kalkulator frekwencji diff --git a/app/src/main/res/values-ru/preferences_values.xml b/app/src/main/res/values-ru/preferences_values.xml index 16b337a7..8d4bd8d7 100644 --- a/app/src/main/res/values-ru/preferences_values.xml +++ b/app/src/main/res/values-ru/preferences_values.xml @@ -1,5 +1,10 @@ + Alphabetically + By date + By average + By attendance percentage + By subject attendance balance Светлая Тёмная diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 8a5fcc40..0e7e0e1d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -265,6 +265,11 @@ Время окончания должно быть больше, чем время начала Итоговая посещаемость + Attendance calculator + %1$d over target + right on target + %1$d under target + %1$d/%2$d presences Отсутствие по школьным причинам Отсутствие по уважительной причине Отсутствие по неуважительной причине @@ -731,6 +736,8 @@ Параметры расчёта средних оценок Принудительно высчитать среднюю оценку через приложение Показывать присутствия + Attendance target + Attendance calculator sorting Тема Разворачивание оценок Показать группы рядом с темами diff --git a/app/src/main/res/values-sk/preferences_values.xml b/app/src/main/res/values-sk/preferences_values.xml index c9862751..d78dd92d 100644 --- a/app/src/main/res/values-sk/preferences_values.xml +++ b/app/src/main/res/values-sk/preferences_values.xml @@ -1,5 +1,10 @@ + Abecedne + Podľa dátumu + Podľa priemeru + Podľa percenta dochádzky + Podľa rovnováhy dochádzky predmetu Svetlý Tmavý @@ -54,7 +59,7 @@ Šťastné číslo Neprečítané správy - Frekvencia + Dochádzka Lekcie Známky Domáce úlohy diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 0f521566..9dbf7282 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -4,7 +4,7 @@ Prihlásenie Wulkanowy Známky - Frekvencia + Dochádzka Skúšky Plán lekcie Nastavenia @@ -64,7 +64,7 @@ Symbol nájdete na stránke denníka v  Uczeń→ Dostęp Mobilny → Wygeneruj kod dostępu.\n\nUistite sa, že ste nastavili správny variant denníka v poli Variácia denníka UONET+ na prvej prihlasovacej obrazovke Vyberte žiakov, ktorí sa majú do aplikácie prihlásiť Iné možnosti - V tomto režime nefungujú nasledovné: šťastné číslo, štatistiky triedy, zhrnutie frekvencií, ospravedlnenie neprítomnosti, dokončené lekcie, informácie o škole a prezeranie zoznamu registrovaných zariadení + V tomto režime nefungujú nasledovné: šťastné číslo, štatistiky triedy, zhrnutie dochádzky, ospravedlnenie neprítomnosti, dokončené lekcie, informácie o škole a prezeranie zoznamu registrovaných zariadení Tento režim zobrazuje rovnaké dáta, ktoré sa zobrazujú na webových stránkach denníka Kombinácia najlepších vlastností ostatných dvoch režimov. Funguje rýchlejšie ako scraper a poskytuje funkcie, ktoré nie sú k dispozícii v režime Mobilne API. Je to v experimentálnej fáze Ochrana osobných údajov @@ -264,7 +264,12 @@ Čas ukončenia Čas ukončenia musí byť neskorší ako čas začatia - Zhrnutie frekvencií + Zhrnutie dochádzky + Kalkulačka dochádzky + %1$d nad cieľom + presne v cieli + %1$d pod cieľom + %1$d/%2$d prítomnosti Neprítomnosť zo školských dôvodov Ospravedlnená neprítomnosť Neospravedlnená neprítomnosť @@ -282,22 +287,22 @@ Musíte vybrať aspoň jednu neprítomnosť! Ospravedlniť - Nová frekvencia - Nové frekvencie - Nové frekvencie - Nové frekvencie + Nová dochádzka + Nové dochádzky + Nové dochádzky + Nové dochádzky - %1$d nová frekvencia - %1$d nové frekvencie - %1$d nových frekvencií - %1$d nových frekvencií + %1$d nová dochádzka + %1$d nové dochádzky + %1$d nových dochádzok + %1$d nových dochádzok - %d frekvencia - %d frekvencie - %d frekvencií - %d frekvencií + %d dochádzka + %d dochádzky + %d dochádzok + %d dochádzok Spoločne @@ -731,6 +736,8 @@ Možnosti vypočítaného priemeru Vynútiť priemerný výpočet podľa aplikácie Zobraziť prítomnosť + Cieľová dochádzka + Triedenie kalkulačky dochádzky Motív Rozvijanie známok Zobraziť skupiny vedľa predmetov @@ -797,7 +804,7 @@ Známky Domov Viditeľnosť dlaždíc - Frekvencia + Dochádzka Plán lekcie Známky Vypočítaný priemer @@ -825,7 +832,7 @@ Nadchádzajúce lekcie Ladenie Zmeny plánu lekcií - Nové frekvencie + Nové dochádzky Čierna Červená diff --git a/app/src/main/res/values-uk/preferences_values.xml b/app/src/main/res/values-uk/preferences_values.xml index f0cfdd12..c32eedb9 100644 --- a/app/src/main/res/values-uk/preferences_values.xml +++ b/app/src/main/res/values-uk/preferences_values.xml @@ -1,5 +1,10 @@ + За алфавітом + За датою + За середньою + За відсотком відвідуваності + За балансом відвідування теми Світла Темна diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index a0d4b6c0..2d8ac1f4 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -265,6 +265,11 @@ Час завершення має бути пізніше часу початку Підсумок відвідуваності + Калькулятор відвідуваності + %1$d понад ціль + точно у цілі + %1$d під ціллю + %1$d/%2$d відвідуваності Відсутність зі шкільних причин Відсутність з поважних причин Відсутність без поважних причин @@ -731,6 +736,8 @@ Параметри розраховування середніх оцінок Примусово розраховувати середню оцінку через додаток Показувати присутність + Цільова відвідуваність + Сортування калькулятора відвідування Тема Розгортання оцінок Показувати групи поруч з темами From 95e41b5570a0c143ab81e576222a625b77eac90c Mon Sep 17 00:00:00 2001 From: Michael <5672750+mibac138@users.noreply.github.com> Date: Mon, 11 Mar 2024 20:19:24 +0100 Subject: [PATCH 31/43] Handle subjects with no attendances in attendance calculator better (#2478) --------- Co-authored-by: Faierbel --- .../java/io/github/wulkanowy/data/Resource.kt | 3 ++ .../repositories/PreferencesRepository.kt | 9 ++++ .../GetAttendanceCalculatorDataUseCase.kt | 3 ++ .../calculator/AttendanceCalculatorAdapter.kt | 54 ++++++++++--------- .../main/res/values/preferences_defaults.xml | 1 + app/src/main/res/values/preferences_keys.xml | 1 + app/src/main/res/values/strings.xml | 2 + .../res/xml/scheme_preferences_appearance.xml | 5 ++ 8 files changed, 53 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/data/Resource.kt b/app/src/main/java/io/github/wulkanowy/data/Resource.kt index c698c42d..b4982b9a 100644 --- a/app/src/main/java/io/github/wulkanowy/data/Resource.kt +++ b/app/src/main/java/io/github/wulkanowy/data/Resource.kt @@ -71,6 +71,9 @@ fun Resource.mapData(block: (T) -> U) = when (this) { is Resource.Error -> Resource.Error(this.error) } +/** + * Injects another flow into this flow's resource data. + */ inline fun Flow>.combineWithResourceData( flow: Flow, crossinline block: suspend (T1, T2) -> R diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index 4735293c..2bb1538c 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt @@ -54,6 +54,15 @@ class PreferencesRepository @Inject constructor( context.resources.getString(R.string.pref_default_attendance_calculator_sorting_mode) ).asFlow().map(AttendanceCalculatorSortingMode::getByValue) + /** + * Subjects are empty when they don't have any attendances (total = 0, attendances = 0, absences = 0). + */ + val attendanceCalculatorShowEmptySubjects: Flow + get() = flowSharedPref.getBoolean( + context.getString(R.string.pref_key_attendance_calculator_show_empty_subjects), + context.resources.getBoolean(R.bool.pref_default_attendance_calculator_show_empty_subjects) + ).asFlow() + private val gradeAverageModePref: Preference get() = getObjectFlow( R.string.pref_key_grade_average_mode, diff --git a/app/src/main/java/io/github/wulkanowy/domain/attendance/GetAttendanceCalculatorDataUseCase.kt b/app/src/main/java/io/github/wulkanowy/domain/attendance/GetAttendanceCalculatorDataUseCase.kt index ea68050d..294abd1b 100644 --- a/app/src/main/java/io/github/wulkanowy/domain/attendance/GetAttendanceCalculatorDataUseCase.kt +++ b/app/src/main/java/io/github/wulkanowy/domain/attendance/GetAttendanceCalculatorDataUseCase.kt @@ -49,6 +49,9 @@ class GetAttendanceCalculatorDataUseCase @Inject constructor( // intermediates that will be visible for barely any time. .debounceIntermediates() } + .combineWithResourceData(preferencesRepository.attendanceCalculatorShowEmptySubjects) { attendanceDataList, showEmptySubjects -> + attendanceDataList.filter { it.total != 0 || showEmptySubjects } + } .combineWithResourceData(preferencesRepository.attendanceCalculatorSortingModeFlow, List::sortedBy) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorAdapter.kt index 73c08fd3..4b908bba 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorAdapter.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.attendance.calculator -import android.annotation.SuppressLint import android.view.LayoutInflater import android.view.ViewGroup import androidx.core.view.isVisible @@ -19,42 +18,47 @@ class AttendanceCalculatorAdapter @Inject constructor() : override fun getItemCount() = items.size - override fun onCreateViewHolder( - parent: ViewGroup, viewType: Int - ) = ViewHolder( + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( ItemAttendanceCalculatorHeaderBinding.inflate( LayoutInflater.from(parent.context), parent, false ) ) - @SuppressLint("SetTextI18n") override fun onBindViewHolder(parent: ViewHolder, position: Int) { + val context = parent.binding.root.context + val item = items[position] + with(parent.binding) { - val item = items[position] attendanceCalculatorPercentage.text = "${item.presencePercentage.roundToInt()}" - if (item.lessonBalance > 0) { - attendanceCalculatorSummaryBalance.text = root.context.getString( - R.string.attendance_calculator_summary_balance_positive, - item.lessonBalance - ) - } else if (item.lessonBalance < 0) { - attendanceCalculatorSummaryBalance.text = root.context.getString( - R.string.attendance_calculator_summary_balance_negative, - abs(item.lessonBalance) - ) - } else { - attendanceCalculatorSummaryBalance.text = root.context.getString( - R.string.attendance_calculator_summary_balance_neutral, - ) + attendanceCalculatorSummaryBalance.text = when { + item.lessonBalance > 0 -> { + context.getString( + R.string.attendance_calculator_summary_balance_positive, + item.lessonBalance + ) + } + + item.lessonBalance < 0 -> { + context.getString( + R.string.attendance_calculator_summary_balance_negative, + abs(item.lessonBalance) + ) + } + + else -> context.getString(R.string.attendance_calculator_summary_balance_neutral) } attendanceCalculatorWarning.isVisible = item.lessonBalance < 0 attendanceCalculatorTitle.text = item.subjectName - attendanceCalculatorSummaryValues.text = root.context.getString( - R.string.attendance_calculator_summary_values, - item.presences, - item.total - ) + attendanceCalculatorSummaryValues.text = if (item.total == 0) { + context.getString(R.string.attendance_calculator_summary_values_empty) + } else { + context.getString( + R.string.attendance_calculator_summary_values, + item.presences, + item.total + ) + } } } diff --git a/app/src/main/res/values/preferences_defaults.xml b/app/src/main/res/values/preferences_defaults.xml index 10941889..2981e184 100644 --- a/app/src/main/res/values/preferences_defaults.xml +++ b/app/src/main/res/values/preferences_defaults.xml @@ -4,6 +4,7 @@ true 50 alphabetic + false only_one_semester false one diff --git a/app/src/main/res/values/preferences_keys.xml b/app/src/main/res/values/preferences_keys.xml index e95c5940..080456ef 100644 --- a/app/src/main/res/values/preferences_keys.xml +++ b/app/src/main/res/values/preferences_keys.xml @@ -4,6 +4,7 @@ attendance_present attendance_target attendance_calculator_sorting_mode + attendance_calculator_show_empty_subjects app_theme dashboard_tiles grade_color_scheme diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ae6d9140..56cf94f0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -263,6 +263,7 @@ right on target %1$d under target %1$d/%2$d presences + No attendances recorded Absent for school reasons Excused absence Unexcused absence @@ -721,6 +722,7 @@ Force average calculation by app Show presence Attendance target + Show subjects without any attendances Attendance calculator sorting Theme Grades expanding diff --git a/app/src/main/res/xml/scheme_preferences_appearance.xml b/app/src/main/res/xml/scheme_preferences_appearance.xml index 46a0e6a9..a05d95c0 100644 --- a/app/src/main/res/xml/scheme_preferences_appearance.xml +++ b/app/src/main/res/xml/scheme_preferences_appearance.xml @@ -104,6 +104,11 @@ app:key="@string/pref_key_attendance_calculator_sorting_mode" app:title="@string/pref_view_attendance_calculator_sorting_mode" app:useSimpleSummaryProvider="true" /> + Date: Mon, 11 Mar 2024 23:38:17 +0100 Subject: [PATCH 32/43] Fix lateness color in attendance (#2481) --- .../modules/attendance/AttendanceAdapter.kt | 25 +++++++++++-------- app/src/main/res/values/colors.xml | 5 ++-- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt index 4e9baac3..f5689ec8 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.ui.modules.attendance +import android.content.res.ColorStateList import android.graphics.Typeface import android.view.LayoutInflater import android.view.View @@ -33,17 +34,17 @@ class AttendanceAdapter @Inject constructor() : ) override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + val context = holder.binding.root.context val item = items[position] with(holder.binding) { attendanceItemNumber.text = item.number.toString() - attendanceItemSubject.text = item.subject.ifBlank { - root.context.getString(R.string.all_no_data) - } + attendanceItemSubject.text = item.subject + .ifBlank { context.getString(R.string.all_no_data) } attendanceItemDescription.setText(item.descriptionRes) attendanceItemDescription.setTextColor( - root.context.getThemeAttrColor( + context.getThemeAttrColor( when { item.absence && !item.excused -> R.attr.colorAttendanceAbsence item.lateness && !item.excused -> R.attr.colorAttendanceLateness @@ -61,13 +62,15 @@ class AttendanceAdapter @Inject constructor() : attendanceItemAlert.isVisible = item.let { (it.absence && !it.excused) || (it.lateness && !it.excused) } - attendanceItemAlert.setColorFilter(root.context.getThemeAttrColor( - when{ - item.absence && !item.excused -> R.attr.colorAttendanceAbsence - item.lateness && !item.excused -> R.attr.colorAttendanceLateness - else -> android.R.attr.colorPrimary - } - )) + attendanceItemAlert.imageTintList = ColorStateList.valueOf( + context.getThemeAttrColor( + when { + item.absence && !item.excused -> R.attr.colorAttendanceAbsence + item.lateness && !item.excused -> R.attr.colorAttendanceLateness + else -> android.R.attr.colorPrimary + } + ) + ) attendanceItemNumber.visibility = View.GONE attendanceItemExcuseInfo.visibility = View.GONE attendanceItemExcuseCheckbox.visibility = View.GONE diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 8ad27ad8..f31a5f94 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -46,14 +46,15 @@ #d32f2f #e57373 + #ff8f00 #ffd54f #d32f2f #e57373 - #cd2a01 - #f05d0e + #ff8f00 + #ffd54f #1f000000 #1fffffff From 6a8f6f9496fdd29337b8f1352390beffb3600ef7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Mon, 11 Mar 2024 23:38:39 +0100 Subject: [PATCH 33/43] Add WulkanowySdkFactory (#2479) --- .../io/github/wulkanowy/data/DataModule.kt | 21 ------ .../wulkanowy/data/WulkanowySdkFactory.kt | 64 +++++++++++++++++++ .../data/repositories/AttendanceRepository.kt | 18 +++--- .../AttendanceSummaryRepository.kt | 11 ++-- .../CompletedLessonsRepository.kt | 9 +-- .../data/repositories/ConferenceRepository.kt | 9 +-- .../data/repositories/ExamRepository.kt | 9 +-- .../data/repositories/GradeRepository.kt | 9 +-- .../repositories/GradeStatisticsRepository.kt | 15 ++--- .../data/repositories/HomeworkRepository.kt | 9 +-- .../repositories/LuckyNumberRepository.kt | 9 +-- .../data/repositories/MessageRepository.kt | 59 +++++++++-------- .../repositories/MobileDeviceRepository.kt | 15 ++--- .../data/repositories/NoteRepository.kt | 9 +-- .../data/repositories/RecipientRepository.kt | 10 +-- .../data/repositories/RecoverRepository.kt | 20 ++++-- .../SchoolAnnouncementRepository.kt | 7 +- .../data/repositories/SchoolRepository.kt | 9 +-- .../data/repositories/SchoolsRepository.kt | 11 ++-- .../data/repositories/SemesterRepository.kt | 9 ++- .../repositories/StudentInfoRepository.kt | 12 ++-- .../data/repositories/StudentRepository.kt | 17 ++--- .../data/repositories/SubjectRepository.kt | 9 +-- .../data/repositories/TeacherRepository.kt | 9 +-- .../data/repositories/TimetableRepository.kt | 9 +-- .../ui/modules/captcha/CaptchaDialog.kt | 6 +- .../io/github/wulkanowy/utils/SdkExtension.kt | 42 ------------ .../wulkanowy/WulkanowySdkFactoryCreator.kt | 12 ++++ .../repositories/AttendanceRepositoryTest.kt | 10 +-- .../CompletedLessonsRepositoryTest.kt | 9 +-- .../data/repositories/ExamRemoteTest.kt | 9 +-- .../data/repositories/GradeRepositoryTest.kt | 9 +-- .../GradeStatisticsRepositoryTest.kt | 16 +++-- .../repositories/LuckyNumberRemoteTest.kt | 9 +-- .../repositories/MessageRepositoryTest.kt | 11 ++-- .../MobileDeviceRepositoryTest.kt | 10 +-- .../data/repositories/RecipientLocalTest.kt | 16 +++-- .../repositories/SemesterRepositoryTest.kt | 10 +-- .../repositories/TimetableRepositoryTest.kt | 9 +-- 39 files changed, 283 insertions(+), 283 deletions(-) create mode 100644 app/src/main/java/io/github/wulkanowy/data/WulkanowySdkFactory.kt delete mode 100644 app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt create mode 100644 app/src/test/java/io/github/wulkanowy/WulkanowySdkFactoryCreator.kt diff --git a/app/src/main/java/io/github/wulkanowy/data/DataModule.kt b/app/src/main/java/io/github/wulkanowy/data/DataModule.kt index 50d6c8f9..a492c08d 100644 --- a/app/src/main/java/io/github/wulkanowy/data/DataModule.kt +++ b/app/src/main/java/io/github/wulkanowy/data/DataModule.kt @@ -18,17 +18,13 @@ import io.github.wulkanowy.data.api.SchoolsService import io.github.wulkanowy.data.db.AppDatabase import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.repositories.PreferencesRepository -import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AppInfo -import io.github.wulkanowy.utils.RemoteConfigHelper -import io.github.wulkanowy.utils.WebkitCookieManagerProxy import kotlinx.serialization.json.Json import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.create -import timber.log.Timber import java.util.concurrent.TimeUnit import javax.inject.Singleton @@ -36,23 +32,6 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) internal class DataModule { - @Singleton - @Provides - fun provideSdk( - chuckerInterceptor: ChuckerInterceptor, - remoteConfig: RemoteConfigHelper, - webkitCookieManagerProxy: WebkitCookieManagerProxy, - ) = Sdk().apply { - androidVersion = android.os.Build.VERSION.RELEASE - buildTag = android.os.Build.MODEL - userAgentTemplate = remoteConfig.userAgentTemplate - setSimpleHttpLogger { Timber.d(it) } - setAdditionalCookieManager(webkitCookieManagerProxy) - - // for debug only - addInterceptor(chuckerInterceptor, network = true) - } - @Singleton @Provides fun provideChuckerCollector( diff --git a/app/src/main/java/io/github/wulkanowy/data/WulkanowySdkFactory.kt b/app/src/main/java/io/github/wulkanowy/data/WulkanowySdkFactory.kt new file mode 100644 index 00000000..6d4f9eda --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/WulkanowySdkFactory.kt @@ -0,0 +1,64 @@ +package io.github.wulkanowy.data + +import com.chuckerteam.chucker.api.ChuckerInterceptor +import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.utils.RemoteConfigHelper +import io.github.wulkanowy.utils.WebkitCookieManagerProxy +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class WulkanowySdkFactory @Inject constructor( + private val chuckerInterceptor: ChuckerInterceptor, + private val remoteConfig: RemoteConfigHelper, + private val webkitCookieManagerProxy: WebkitCookieManagerProxy +) { + + private val sdk = Sdk().apply { + androidVersion = android.os.Build.VERSION.RELEASE + buildTag = android.os.Build.MODEL + userAgentTemplate = remoteConfig.userAgentTemplate + setSimpleHttpLogger { Timber.d(it) } + setAdditionalCookieManager(webkitCookieManagerProxy) + + // for debug only + addInterceptor(chuckerInterceptor, network = true) + } + + fun create() = sdk + + fun create(student: Student, semester: Semester? = null): Sdk { + return create().apply { + email = student.email + password = student.password + symbol = student.symbol + schoolSymbol = student.schoolSymbol + studentId = student.studentId + classId = student.classId + emptyCookieJarInterceptor = true + + if (Sdk.Mode.valueOf(student.loginMode) == Sdk.Mode.HEBE) { + mobileBaseUrl = student.mobileBaseUrl + } else { + scrapperBaseUrl = student.scrapperBaseUrl + domainSuffix = student.scrapperDomainSuffix + loginType = Sdk.ScrapperLoginType.valueOf(student.loginType) + } + + mode = Sdk.Mode.valueOf(student.loginMode) + mobileBaseUrl = student.mobileBaseUrl + keyId = student.certificateKey + privatePem = student.privateKey + + if (semester != null) { + diaryId = semester.diaryId + kindergartenDiaryId = semester.kindergartenDiaryId + schoolYear = semester.schoolYear + unitId = semester.unitId + } + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt index 46ea29f8..9b94cc10 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.data.WulkanowySdkFactory import io.github.wulkanowy.data.db.dao.AttendanceDao import io.github.wulkanowy.data.db.dao.TimetableDao import io.github.wulkanowy.data.db.entities.Attendance @@ -7,14 +8,11 @@ import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.networkBoundResource -import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.pojo.Absent 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.sync.Mutex @@ -28,7 +26,7 @@ import javax.inject.Singleton class AttendanceRepository @Inject constructor( private val attendanceDb: AttendanceDao, private val timetableDb: TimetableDao, - private val sdk: Sdk, + private val wulkanowySdkFactory: WulkanowySdkFactory, private val refreshHelper: AutoRefreshHelper, ) { @@ -59,8 +57,7 @@ class AttendanceRepository @Inject constructor( val lessons = timetableDb.load( semester.diaryId, semester.studentId, start.monday, end.sunday ) - sdk.init(student) - .switchSemester(semester) + wulkanowySdkFactory.create(student, semester) .getAttendance(start.monday, end.sunday) .mapToEntities(semester, lessons) }, @@ -90,8 +87,10 @@ class AttendanceRepository @Inject constructor( } suspend fun excuseForAbsence( - student: Student, semester: Semester, - absenceList: List, reason: String? = null + student: Student, + semester: Semester, + absenceList: List, + reason: String? = null ) { val items = absenceList.map { attendance -> Absent( @@ -99,8 +98,7 @@ class AttendanceRepository @Inject constructor( timeId = attendance.timeId ) } - sdk.init(student) - .switchSemester(semester) + wulkanowySdkFactory.create(student, semester) .excuseForAbsence(items, reason) } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt index 1129598a..78c98169 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt @@ -1,17 +1,15 @@ package io.github.wulkanowy.data.repositories import androidx.room.withTransaction -import io.github.wulkanowy.data.* +import io.github.wulkanowy.data.WulkanowySdkFactory import io.github.wulkanowy.data.db.AppDatabase import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities -import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.data.networkBoundResource 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.uniqueSubtract import kotlinx.coroutines.sync.Mutex import javax.inject.Inject @@ -20,9 +18,9 @@ import javax.inject.Singleton @Singleton class AttendanceSummaryRepository @Inject constructor( private val attendanceDb: AttendanceSummaryDao, - private val sdk: Sdk, private val refreshHelper: AutoRefreshHelper, private val appDatabase: AppDatabase, + private val wulkanowySdkFactory: WulkanowySdkFactory, ) { private val saveFetchResultMutex = Mutex() @@ -43,8 +41,7 @@ class AttendanceSummaryRepository @Inject constructor( }, query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, subjectId) }, fetch = { - sdk.init(student) - .switchSemester(semester) + wulkanowySdkFactory.create(student, semester) .getAttendanceSummary(subjectId) .mapToEntities(semester, subjectId) }, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt index f7f86b23..45a36f55 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt @@ -1,17 +1,15 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.data.WulkanowySdkFactory import io.github.wulkanowy.data.db.dao.CompletedLessonsDao import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.networkBoundResource -import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey -import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.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 java.time.LocalDate @@ -21,7 +19,7 @@ import javax.inject.Singleton @Singleton class CompletedLessonsRepository @Inject constructor( private val completedLessonsDb: CompletedLessonsDao, - private val sdk: Sdk, + private val wulkanowySdkFactory: WulkanowySdkFactory, private val refreshHelper: AutoRefreshHelper, ) { @@ -53,8 +51,7 @@ class CompletedLessonsRepository @Inject constructor( ) }, fetch = { - sdk.init(student) - .switchSemester(semester) + wulkanowySdkFactory.create(student, semester) .getCompletedLessons(start.monday, end.sunday) .mapToEntities(semester) }, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt index fbe57860..58ce0091 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt @@ -1,16 +1,14 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.data.WulkanowySdkFactory import io.github.wulkanowy.data.db.dao.ConferenceDao import io.github.wulkanowy.data.db.entities.Conference import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.networkBoundResource -import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey -import io.github.wulkanowy.utils.init -import io.github.wulkanowy.utils.switchSemester import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex @@ -21,7 +19,7 @@ import javax.inject.Singleton @Singleton class ConferenceRepository @Inject constructor( private val conferenceDb: ConferenceDao, - private val sdk: Sdk, + private val wulkanowySdkFactory: WulkanowySdkFactory, private val refreshHelper: AutoRefreshHelper, ) { @@ -46,8 +44,7 @@ class ConferenceRepository @Inject constructor( conferenceDb.loadAll(semester.diaryId, student.studentId, startDate) }, fetch = { - sdk.init(student) - .switchSemester(semester) + wulkanowySdkFactory.create(student, semester) .getConferences() .mapToEntities(semester) .filter { it.date >= startDate } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt index 9b8dd02e..89dbcd5c 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt @@ -1,18 +1,16 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.data.WulkanowySdkFactory import io.github.wulkanowy.data.db.dao.ExamDao import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.networkBoundResource -import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.endExamsDay import io.github.wulkanowy.utils.getRefreshKey -import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.startExamsDay -import io.github.wulkanowy.utils.switchSemester import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex @@ -23,7 +21,7 @@ import javax.inject.Singleton @Singleton class ExamRepository @Inject constructor( private val examDb: ExamDao, - private val sdk: Sdk, + private val wulkanowySdkFactory: WulkanowySdkFactory, private val refreshHelper: AutoRefreshHelper, ) { @@ -56,8 +54,7 @@ class ExamRepository @Inject constructor( ) }, fetch = { - sdk.init(student) - .switchSemester(semester) + wulkanowySdkFactory.create(student, semester) .getExams(start.startExamsDay, start.endExamsDay) .mapToEntities(semester) }, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt index ac1ef541..e899f900 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.data.WulkanowySdkFactory import io.github.wulkanowy.data.db.dao.GradeDao import io.github.wulkanowy.data.db.dao.GradeDescriptiveDao import io.github.wulkanowy.data.db.dao.GradeSummaryDao @@ -10,11 +11,8 @@ import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.networkBoundResource -import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey -import io.github.wulkanowy.utils.init -import io.github.wulkanowy.utils.switchSemester import io.github.wulkanowy.utils.toLocalDate import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow @@ -30,7 +28,7 @@ class GradeRepository @Inject constructor( private val gradeDb: GradeDao, private val gradeSummaryDb: GradeSummaryDao, private val gradeDescriptiveDb: GradeDescriptiveDao, - private val sdk: Sdk, + private val wulkanowySdkFactory: WulkanowySdkFactory, private val refreshHelper: AutoRefreshHelper, ) { @@ -63,8 +61,7 @@ class GradeRepository @Inject constructor( } }, fetch = { - val (details, summary, descriptive) = sdk.init(student) - .switchSemester(semester) + val (details, summary, descriptive) = wulkanowySdkFactory.create(student, semester) .getGrades(semester.semesterId) Triple( diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt index 809f92d3..f120d34f 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.data.WulkanowySdkFactory import io.github.wulkanowy.data.db.dao.GradePartialStatisticsDao import io.github.wulkanowy.data.db.dao.GradePointsStatisticsDao import io.github.wulkanowy.data.db.dao.GradeSemesterStatisticsDao @@ -12,11 +13,8 @@ import io.github.wulkanowy.data.mappers.mapPointsToStatisticsItems import io.github.wulkanowy.data.mappers.mapSemesterToStatisticItems import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.networkBoundResource -import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey -import io.github.wulkanowy.utils.init -import io.github.wulkanowy.utils.switchSemester import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.sync.Mutex import java.util.Locale @@ -28,7 +26,7 @@ class GradeStatisticsRepository @Inject constructor( private val gradePartialStatisticsDb: GradePartialStatisticsDao, private val gradePointsStatisticsDb: GradePointsStatisticsDao, private val gradeSemesterStatisticsDb: GradeSemesterStatisticsDao, - private val sdk: Sdk, + private val wulkanowySdkFactory: WulkanowySdkFactory, private val refreshHelper: AutoRefreshHelper, ) { @@ -56,8 +54,7 @@ class GradeStatisticsRepository @Inject constructor( }, query = { gradePartialStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, fetch = { - sdk.init(student) - .switchSemester(semester) + wulkanowySdkFactory.create(student, semester) .getGradesPartialStatistics(semester.semesterId) .mapToEntities(semester) }, @@ -104,8 +101,7 @@ class GradeStatisticsRepository @Inject constructor( }, query = { gradeSemesterStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, fetch = { - sdk.init(student) - .switchSemester(semester) + wulkanowySdkFactory.create(student, semester) .getGradesSemesterStatistics(semester.semesterId) .mapToEntities(semester) }, @@ -163,8 +159,7 @@ class GradeStatisticsRepository @Inject constructor( }, query = { gradePointsStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, fetch = { - sdk.init(student) - .switchSemester(semester) + wulkanowySdkFactory.create(student, semester) .getGradesPointsStatistics(semester.semesterId) .mapToEntities(semester) }, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt index 1a9c7ffa..7893ef63 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt @@ -1,18 +1,16 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.data.WulkanowySdkFactory import io.github.wulkanowy.data.db.dao.HomeworkDao import io.github.wulkanowy.data.db.entities.Homework import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.networkBoundResource -import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey -import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.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 java.time.LocalDate @@ -22,7 +20,7 @@ import javax.inject.Singleton @Singleton class HomeworkRepository @Inject constructor( private val homeworkDb: HomeworkDao, - private val sdk: Sdk, + private val wulkanowySdkFactory: WulkanowySdkFactory, private val refreshHelper: AutoRefreshHelper, ) { @@ -55,8 +53,7 @@ class HomeworkRepository @Inject constructor( ) }, fetch = { - sdk.init(student) - .switchSemester(semester) + wulkanowySdkFactory.create(student, semester) .getHomework(start.monday, end.sunday) .mapToEntities(semester) }, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt index 45b7f6e2..3636cb51 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt @@ -1,12 +1,11 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.data.WulkanowySdkFactory import io.github.wulkanowy.data.db.dao.LuckyNumberDao import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntity import io.github.wulkanowy.data.networkBoundResource -import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.utils.init import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.sync.Mutex @@ -18,7 +17,7 @@ import javax.inject.Singleton @Singleton class LuckyNumberRepository @Inject constructor( private val luckyNumberDb: LuckyNumberDao, - private val sdk: Sdk, + private val wulkanowySdkFactory: WulkanowySdkFactory, ) { private val saveFetchResultMutex = Mutex() @@ -33,7 +32,9 @@ class LuckyNumberRepository @Inject constructor( shouldFetch = { it == null || forceRefresh }, query = { luckyNumberDb.load(student.studentId, now()) }, fetch = { - sdk.init(student).getLuckyNumber(student.schoolShortName)?.mapToEntity(student) + wulkanowySdkFactory.create(student) + .getLuckyNumber(student.schoolShortName) + ?.mapToEntity(student) }, saveFetchResult = { oldLuckyNumber, newLuckyNumber -> newLuckyNumber ?: return@networkBoundResource diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt index a4517760..ede2a0fd 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt @@ -4,6 +4,7 @@ import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.Resource +import io.github.wulkanowy.data.WulkanowySdkFactory import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.dao.MailboxDao import io.github.wulkanowy.data.db.dao.MessageAttachmentDao @@ -29,11 +30,9 @@ import io.github.wulkanowy.data.pojos.MessageDraft import io.github.wulkanowy.data.toFirstResult import io.github.wulkanowy.data.waitForResult import io.github.wulkanowy.domain.messages.GetMailboxByStudentUseCase -import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.pojo.Folder import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey -import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex @@ -48,7 +47,7 @@ class MessageRepository @Inject constructor( private val messagesDb: MessagesDao, private val mutedMessageSendersDao: MutedMessageSendersDao, private val messageAttachmentDao: MessageAttachmentDao, - private val sdk: Sdk, + private val wulkanowySdkFactory: WulkanowySdkFactory, @ApplicationContext private val context: Context, private val refreshHelper: AutoRefreshHelper, private val sharedPrefProvider: SharedPrefProvider, @@ -82,10 +81,16 @@ class MessageRepository @Inject constructor( } else messagesDb.loadMessagesWithMutedAuthor(mailbox.globalKey, folder.id) }, fetch = { - sdk.init(student).getMessages( - folder = Folder.valueOf(folder.name), - mailboxKey = mailbox?.globalKey, - ).mapToEntities(student, mailbox, mailboxDao.loadAll(student.email)) + wulkanowySdkFactory.create(student) + .getMessages( + folder = Folder.valueOf(folder.name), + mailboxKey = mailbox?.globalKey, + ) + .mapToEntities( + student = student, + mailbox = mailbox, + allMailboxes = mailboxDao.loadAll(student.email) + ) }, saveFetchResult = { oldWithAuthors, new -> val old = oldWithAuthors.map { it.message } @@ -115,10 +120,11 @@ class MessageRepository @Inject constructor( }, query = { messagesDb.loadMessageWithAttachment(message.messageGlobalKey) }, fetch = { - sdk.init(student).getMessageDetails( - messageKey = it!!.message.messageGlobalKey, - markAsRead = message.unread && markAsRead, - ) + wulkanowySdkFactory.create(student) + .getMessageDetails( + messageKey = it!!.message.messageGlobalKey, + markAsRead = message.unread && markAsRead, + ) }, saveFetchResult = { old, new -> checkNotNull(old) { "Fetched message no longer exist!" } @@ -159,19 +165,19 @@ class MessageRepository @Inject constructor( recipients: List, mailbox: Mailbox, ) { - sdk.init(student).sendMessage( - subject = subject, - content = content, - recipients = recipients.mapFromEntities(), - mailboxId = mailbox.globalKey, - ) + wulkanowySdkFactory.create(student) + .sendMessage( + subject = subject, + content = content, + recipients = recipients.mapFromEntities(), + mailboxId = mailbox.globalKey, + ) refreshFolders(student, mailbox, listOf(SENT)) } suspend fun restoreMessages(student: Student, mailbox: Mailbox?, messages: List) { - sdk.init(student).restoreMessages( - messages = messages.map { it.messageGlobalKey }, - ) + wulkanowySdkFactory.create(student) + .restoreMessages(messages = messages.map { it.messageGlobalKey }) refreshFolders(student, mailbox) } @@ -182,10 +188,11 @@ class MessageRepository @Inject constructor( suspend fun deleteMessages(student: Student, messages: List) { val firstMessage = messages.first() - sdk.init(student).deleteMessages( - messages = messages.map { it.messageGlobalKey }, - removeForever = firstMessage.folderId == TRASHED.id, - ) + wulkanowySdkFactory.create(student) + .deleteMessages( + messages = messages.map { it.messageGlobalKey }, + removeForever = firstMessage.folderId == TRASHED.id, + ) if (firstMessage.folderId != TRASHED.id) { val deletedMessages = messages.map { @@ -230,7 +237,9 @@ class MessageRepository @Inject constructor( }, query = { mailboxDao.loadAll(student.email, student.symbol, student.schoolSymbol) }, fetch = { - sdk.init(student).getMailboxes().mapToEntities(student) + wulkanowySdkFactory.create(student) + .getMailboxes() + .mapToEntities(student) }, saveFetchResult = { old, new -> mailboxDao.deleteAll(old uniqueSubtract new) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt index 48b4fc28..19466554 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.data.WulkanowySdkFactory import io.github.wulkanowy.data.db.dao.MobileDeviceDao import io.github.wulkanowy.data.db.entities.MobileDevice import io.github.wulkanowy.data.db.entities.Semester @@ -8,11 +9,8 @@ import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.mappers.mapToMobileDeviceToken import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.data.pojos.MobileDeviceToken -import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey -import io.github.wulkanowy.utils.init -import io.github.wulkanowy.utils.switchSemester import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.sync.Mutex import javax.inject.Inject @@ -21,7 +19,7 @@ import javax.inject.Singleton @Singleton class MobileDeviceRepository @Inject constructor( private val mobileDb: MobileDeviceDao, - private val sdk: Sdk, + private val wulkanowySdkFactory: WulkanowySdkFactory, private val refreshHelper: AutoRefreshHelper, ) { @@ -42,8 +40,7 @@ class MobileDeviceRepository @Inject constructor( }, query = { mobileDb.loadAll(student.userLoginId) }, fetch = { - sdk.init(student) - .switchSemester(semester) + wulkanowySdkFactory.create(student, semester) .getRegisteredDevices() .mapToEntities(student) }, @@ -57,16 +54,14 @@ class MobileDeviceRepository @Inject constructor( ) suspend fun unregisterDevice(student: Student, semester: Semester, device: MobileDevice) { - sdk.init(student) - .switchSemester(semester) + wulkanowySdkFactory.create(student, semester) .unregisterDevice(device.deviceId) mobileDb.deleteAll(listOf(device)) } suspend fun getToken(student: Student, semester: Semester): MobileDeviceToken { - return sdk.init(student) - .switchSemester(semester) + return wulkanowySdkFactory.create(student, semester) .getToken() .mapToMobileDeviceToken() } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt index feb92c15..9551e01e 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt @@ -1,16 +1,14 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.data.WulkanowySdkFactory import io.github.wulkanowy.data.db.dao.NoteDao import io.github.wulkanowy.data.db.entities.Note import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.networkBoundResource -import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey -import io.github.wulkanowy.utils.init -import io.github.wulkanowy.utils.switchSemester import io.github.wulkanowy.utils.toLocalDate import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow @@ -21,7 +19,7 @@ import javax.inject.Singleton @Singleton class NoteRepository @Inject constructor( private val noteDb: NoteDao, - private val sdk: Sdk, + private val wulkanowySdkFactory: WulkanowySdkFactory, private val refreshHelper: AutoRefreshHelper, ) { @@ -45,8 +43,7 @@ class NoteRepository @Inject constructor( }, query = { noteDb.loadAll(student.studentId) }, fetch = { - sdk.init(student) - .switchSemester(semester) + wulkanowySdkFactory.create(student, semester) .getNotes() .mapToEntities(semester) }, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/RecipientRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/RecipientRepository.kt index 4a1474ce..8233d932 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/RecipientRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/RecipientRepository.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.data.WulkanowySdkFactory import io.github.wulkanowy.data.db.dao.RecipientDao import io.github.wulkanowy.data.db.entities.Mailbox import io.github.wulkanowy.data.db.entities.MailboxType @@ -7,10 +8,8 @@ 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.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey -import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.uniqueSubtract import javax.inject.Inject import javax.inject.Singleton @@ -18,14 +17,15 @@ import javax.inject.Singleton @Singleton class RecipientRepository @Inject constructor( private val recipientDb: RecipientDao, - private val sdk: Sdk, + private val wulkanowySdkFactory: WulkanowySdkFactory, private val refreshHelper: AutoRefreshHelper, ) { private val cacheKey = "recipient" suspend fun refreshRecipients(student: Student, mailbox: Mailbox, type: MailboxType) { - val new = sdk.init(student).getRecipients(mailbox.globalKey) + val new = wulkanowySdkFactory.create(student) + .getRecipients(mailbox.globalKey) .mapToEntities(mailbox.globalKey) val old = recipientDb.loadAll(type, mailbox.globalKey) @@ -60,7 +60,7 @@ class RecipientRepository @Inject constructor( ): List { mailbox ?: return emptyList() - return sdk.init(student) + return wulkanowySdkFactory.create(student) .getMessageReplayDetails(message.messageGlobalKey) .sender .let(::listOf) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/RecoverRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/RecoverRepository.kt index 5940f477..b554bda0 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/RecoverRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/RecoverRepository.kt @@ -1,17 +1,23 @@ package io.github.wulkanowy.data.repositories -import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.data.WulkanowySdkFactory import javax.inject.Inject import javax.inject.Singleton @Singleton -class RecoverRepository @Inject constructor(private val sdk: Sdk) { +class RecoverRepository @Inject constructor( + private val wulkanowySdkFactory: WulkanowySdkFactory +) { - suspend fun getReCaptchaSiteKey(host: String, symbol: String): Pair { - return sdk.getPasswordResetCaptchaCode(host, symbol) - } + suspend fun getReCaptchaSiteKey(host: String, symbol: String): Pair = + wulkanowySdkFactory.create() + .getPasswordResetCaptchaCode(host, symbol) suspend fun sendRecoverRequest( - url: String, symbol: String, email: String, reCaptchaResponse: String - ): String = sdk.sendPasswordResetRequest(url, symbol, email, reCaptchaResponse) + url: String, + symbol: String, + email: String, + reCaptchaResponse: String + ): String = wulkanowySdkFactory.create() + .sendPasswordResetRequest(url, symbol, email, reCaptchaResponse) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolAnnouncementRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolAnnouncementRepository.kt index f09a46aa..6a04ce75 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolAnnouncementRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolAnnouncementRepository.kt @@ -1,14 +1,13 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.data.WulkanowySdkFactory import io.github.wulkanowy.data.db.dao.SchoolAnnouncementDao import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.networkBoundResource -import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey -import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex @@ -18,7 +17,7 @@ import javax.inject.Singleton @Singleton class SchoolAnnouncementRepository @Inject constructor( private val schoolAnnouncementDb: SchoolAnnouncementDao, - private val sdk: Sdk, + private val wulkanowySdkFactory: WulkanowySdkFactory, private val refreshHelper: AutoRefreshHelper, ) { @@ -41,7 +40,7 @@ class SchoolAnnouncementRepository @Inject constructor( schoolAnnouncementDb.loadAll(student.userLoginId) }, fetch = { - val sdk = sdk.init(student) + val sdk = wulkanowySdkFactory.create(student) val lastAnnouncements = sdk.getLastAnnouncements().mapToEntities(student) val directorInformation = sdk.getDirectorInformation().mapToEntities(student) lastAnnouncements + directorInformation diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt index b42b4d57..c48abb6f 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt @@ -1,15 +1,13 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.data.WulkanowySdkFactory import io.github.wulkanowy.data.db.dao.SchoolDao import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntity import io.github.wulkanowy.data.networkBoundResource -import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey -import io.github.wulkanowy.utils.init -import io.github.wulkanowy.utils.switchSemester import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -17,7 +15,7 @@ import javax.inject.Singleton @Singleton class SchoolRepository @Inject constructor( private val schoolDb: SchoolDao, - private val sdk: Sdk, + private val wulkanowySdkFactory: WulkanowySdkFactory, private val refreshHelper: AutoRefreshHelper, ) { @@ -40,8 +38,7 @@ class SchoolRepository @Inject constructor( }, query = { schoolDb.load(semester.studentId, semester.classId) }, fetch = { - sdk.init(student) - .switchSemester(semester) + wulkanowySdkFactory.create(student, semester) .getSchool() .mapToEntity(semester) }, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolsRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolsRepository.kt index 216a8c11..4a16d6f1 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolsRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolsRepository.kt @@ -1,17 +1,15 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.data.WulkanowySdkFactory import io.github.wulkanowy.data.api.SchoolsService import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.pojos.IntegrityRequest import io.github.wulkanowy.data.pojos.LoginEvent -import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.utils.IntegrityHelper import io.github.wulkanowy.utils.getCurrentOrLast -import io.github.wulkanowy.utils.init -import io.github.wulkanowy.utils.switchSemester import kotlinx.coroutines.withTimeout import timber.log.Timber import java.util.UUID @@ -23,7 +21,7 @@ import kotlin.time.Duration.Companion.seconds class SchoolsRepository @Inject constructor( private val integrityHelper: IntegrityHelper, private val schoolsService: SchoolsService, - private val sdk: Sdk, + private val wulkanowySdkFactory: WulkanowySdkFactory, ) { suspend fun logSchoolLogin(loginData: LoginData, students: List) { @@ -40,10 +38,9 @@ class SchoolsRepository @Inject constructor( private suspend fun logLogin(loginData: LoginData, student: Student, semester: Semester) { val requestId = UUID.randomUUID().toString() val token = integrityHelper.getIntegrityToken(requestId) ?: return + val updatedStudent = student.copy(password = loginData.password) - val schoolInfo = sdk - .init(student.copy(password = loginData.password)) - .switchSemester(semester) + val schoolInfo = wulkanowySdkFactory.create(updatedStudent, semester) .getSchool() schoolsService.logLoginEvent( diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt index 9ae22bab..da21f59a 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.data.WulkanowySdkFactory import io.github.wulkanowy.data.db.dao.SemesterDao import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student @@ -7,7 +8,6 @@ import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.sdk.Sdk 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 @@ -18,7 +18,7 @@ import javax.inject.Singleton @Singleton class SemesterRepository @Inject constructor( private val semesterDb: SemesterDao, - private val sdk: Sdk, + private val wulkanowySdkFactory: WulkanowySdkFactory, private val dispatchers: DispatchersProvider, ) { @@ -60,7 +60,10 @@ class SemesterRepository @Inject constructor( } private suspend fun refreshSemesters(student: Student) { - val new = sdk.init(student).getSemesters().mapToEntities(student.studentId) + val new = wulkanowySdkFactory.create(student) + .getSemesters() + .mapToEntities(student.studentId) + if (new.isEmpty()) return Timber.i("Empty semester list!") val old = semesterDb.loadAll(student.studentId, student.classId) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt index d42be180..db4c0aeb 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt @@ -1,13 +1,11 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.data.WulkanowySdkFactory import io.github.wulkanowy.data.db.dao.StudentInfoDao import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntity import io.github.wulkanowy.data.networkBoundResource -import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.utils.init -import io.github.wulkanowy.utils.switchSemester import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -15,7 +13,7 @@ import javax.inject.Singleton @Singleton class StudentInfoRepository @Inject constructor( private val studentInfoDao: StudentInfoDao, - private val sdk: Sdk, + private val wulkanowySdkFactory: WulkanowySdkFactory, ) { private val saveFetchResultMutex = Mutex() @@ -30,9 +28,9 @@ class StudentInfoRepository @Inject constructor( shouldFetch = { it == null || forceRefresh }, query = { studentInfoDao.loadStudentInfo(student.studentId) }, fetch = { - sdk.init(student) - .switchSemester(semester) - .getStudentInfo().mapToEntity(semester) + wulkanowySdkFactory.create(student, semester) + .getStudentInfo() + .mapToEntity(semester) }, saveFetchResult = { old, new -> if (old != null && new != old) { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt index e063840c..9a5ecd53 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.data.repositories import androidx.room.withTransaction +import io.github.wulkanowy.data.WulkanowySdkFactory import io.github.wulkanowy.data.db.AppDatabase import io.github.wulkanowy.data.db.dao.SemesterDao import io.github.wulkanowy.data.db.dao.StudentDao @@ -14,9 +15,7 @@ import io.github.wulkanowy.data.mappers.mapToPojo import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.DispatchersProvider -import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.security.Scrambler -import io.github.wulkanowy.utils.switchSemester import kotlinx.coroutines.withContext import javax.inject.Inject import javax.inject.Singleton @@ -26,7 +25,7 @@ class StudentRepository @Inject constructor( private val dispatchers: DispatchersProvider, private val studentDb: StudentDao, private val semesterDb: SemesterDao, - private val sdk: Sdk, + private val wulkanowySdkFactory: WulkanowySdkFactory, private val appDatabase: AppDatabase, private val scrambler: Scrambler, ) { @@ -37,7 +36,7 @@ class StudentRepository @Inject constructor( pin: String, symbol: String, token: String - ): RegisterUser = sdk + ): RegisterUser = wulkanowySdkFactory.create() .getStudentsFromHebe(token, pin, symbol, "") .mapToPojo(null) @@ -47,7 +46,7 @@ class StudentRepository @Inject constructor( scrapperBaseUrl: String, domainSuffix: String, symbol: String - ): RegisterUser = sdk + ): RegisterUser = wulkanowySdkFactory.create() .getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, domainSuffix, symbol) .mapToPojo(password) @@ -56,7 +55,7 @@ class StudentRepository @Inject constructor( password: String, scrapperBaseUrl: String, symbol: String - ): RegisterUser = sdk + ): RegisterUser = wulkanowySdkFactory.create() .getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol) .mapToPojo(password) @@ -149,13 +148,11 @@ class StudentRepository @Inject constructor( .distinctBy { it.student.studentName }.size == 1 suspend fun authorizePermission(student: Student, semester: Semester, pesel: String) = - sdk.init(student) - .switchSemester(semester) + wulkanowySdkFactory.create(student, semester) .authorizePermission(pesel) suspend fun refreshStudentName(student: Student, semester: Semester) { - val newCurrentApiStudent = sdk.init(student) - .switchSemester(semester) + val newCurrentApiStudent = wulkanowySdkFactory.create(student, semester) .getCurrentStudent() ?: return val studentName = StudentName( diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt index cf7f86c2..573c7c14 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt @@ -1,15 +1,13 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.data.WulkanowySdkFactory import io.github.wulkanowy.data.db.dao.SubjectDao import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.networkBoundResource -import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey -import io.github.wulkanowy.utils.init -import io.github.wulkanowy.utils.switchSemester import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.sync.Mutex import javax.inject.Inject @@ -18,7 +16,7 @@ import javax.inject.Singleton @Singleton class SubjectRepository @Inject constructor( private val subjectDao: SubjectDao, - private val sdk: Sdk, + private val wulkanowySdkFactory: WulkanowySdkFactory, private val refreshHelper: AutoRefreshHelper, ) { @@ -39,8 +37,7 @@ class SubjectRepository @Inject constructor( }, query = { subjectDao.loadAll(semester.diaryId, semester.studentId) }, fetch = { - sdk.init(student) - .switchSemester(semester) + wulkanowySdkFactory.create(student, semester) .getSubjects() .mapToEntities(semester) }, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt index 5a488b27..a5a6e3f9 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt @@ -1,15 +1,13 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.data.WulkanowySdkFactory import io.github.wulkanowy.data.db.dao.TeacherDao import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.networkBoundResource -import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey -import io.github.wulkanowy.utils.init -import io.github.wulkanowy.utils.switchSemester import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.sync.Mutex import javax.inject.Inject @@ -18,7 +16,7 @@ import javax.inject.Singleton @Singleton class TeacherRepository @Inject constructor( private val teacherDb: TeacherDao, - private val sdk: Sdk, + private val wulkanowySdkFactory: WulkanowySdkFactory, private val refreshHelper: AutoRefreshHelper, ) { @@ -39,8 +37,7 @@ class TeacherRepository @Inject constructor( }, query = { teacherDb.loadAll(semester.studentId, semester.classId) }, fetch = { - sdk.init(student) - .switchSemester(semester) + wulkanowySdkFactory.create(student, semester) .getTeachers() .mapToEntities(semester) }, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt index 0d208c1f..33578999 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.data.WulkanowySdkFactory import io.github.wulkanowy.data.db.dao.TimetableAdditionalDao import io.github.wulkanowy.data.db.dao.TimetableDao import io.github.wulkanowy.data.db.dao.TimetableHeaderDao @@ -11,14 +12,11 @@ import io.github.wulkanowy.data.db.entities.TimetableHeader import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.data.pojos.TimetableFull -import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper 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.combine @@ -33,7 +31,7 @@ class TimetableRepository @Inject constructor( private val timetableDb: TimetableDao, private val timetableAdditionalDb: TimetableAdditionalDao, private val timetableHeaderDb: TimetableHeaderDao, - private val sdk: Sdk, + private val wulkanowySdkFactory: WulkanowySdkFactory, private val schedulerHelper: TimetableNotificationSchedulerHelper, private val refreshHelper: AutoRefreshHelper, ) { @@ -74,8 +72,7 @@ class TimetableRepository @Inject constructor( }, query = { getFullTimetableFromDatabase(student, semester, start, end) }, fetch = { - val timetableFull = sdk.init(student) - .switchSemester(semester) + val timetableFull = wulkanowySdkFactory.create(student, semester) .getTimetable(start.monday, end.sunday) timetableFull.mapToEntities(semester) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/captcha/CaptchaDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/captcha/CaptchaDialog.kt index 98b4fda7..ce2173d2 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/captcha/CaptchaDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/captcha/CaptchaDialog.kt @@ -10,8 +10,8 @@ import android.webkit.WebViewClient import androidx.core.os.bundleOf import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R +import io.github.wulkanowy.data.WulkanowySdkFactory import io.github.wulkanowy.databinding.DialogCaptchaBinding -import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.ui.base.BaseDialogFragment import io.github.wulkanowy.utils.WebkitCookieManagerProxy import timber.log.Timber @@ -21,7 +21,7 @@ import javax.inject.Inject class CaptchaDialog : BaseDialogFragment() { @Inject - lateinit var sdk: Sdk + lateinit var wulkanowySdkFactory: WulkanowySdkFactory @Inject lateinit var webkitCookieManagerProxy: WebkitCookieManagerProxy @@ -59,7 +59,7 @@ class CaptchaDialog : BaseDialogFragment() { webView = this with(settings) { javaScriptEnabled = true - userAgentString = sdk.userAgent + userAgentString = wulkanowySdkFactory.create().userAgent } webViewClient = object : WebViewClient() { diff --git a/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt deleted file mode 100644 index 9b6ca706..00000000 --- a/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt +++ /dev/null @@ -1,42 +0,0 @@ -package io.github.wulkanowy.utils - -import io.github.wulkanowy.data.db.entities.Semester -import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.sdk.Sdk -import timber.log.Timber - -fun Sdk.init(student: Student): Sdk { - email = student.email - password = student.password - symbol = student.symbol - schoolSymbol = student.schoolSymbol - studentId = student.studentId - classId = student.classId - emptyCookieJarInterceptor = true - - if (Sdk.Mode.valueOf(student.loginMode) == Sdk.Mode.HEBE) { - mobileBaseUrl = student.mobileBaseUrl - } else { - scrapperBaseUrl = student.scrapperBaseUrl - domainSuffix = student.scrapperDomainSuffix - loginType = Sdk.ScrapperLoginType.valueOf(student.loginType) - } - - mode = Sdk.Mode.valueOf(student.loginMode) - mobileBaseUrl = student.mobileBaseUrl - keyId = student.certificateKey - privatePem = student.privateKey - - Timber.d("Sdk in ${student.loginMode} mode reinitialized") - - return this -} - -fun Sdk.switchSemester(semester: Semester): Sdk { - return switchDiary( - diaryId = semester.diaryId, - kindergartenDiaryId = semester.kindergartenDiaryId, - schoolYear = semester.schoolYear, - unitId = semester.unitId, - ) -} diff --git a/app/src/test/java/io/github/wulkanowy/WulkanowySdkFactoryCreator.kt b/app/src/test/java/io/github/wulkanowy/WulkanowySdkFactoryCreator.kt new file mode 100644 index 00000000..dd1ce056 --- /dev/null +++ b/app/src/test/java/io/github/wulkanowy/WulkanowySdkFactoryCreator.kt @@ -0,0 +1,12 @@ +package io.github.wulkanowy + +import io.github.wulkanowy.data.WulkanowySdkFactory +import io.github.wulkanowy.sdk.Sdk +import io.mockk.every +import io.mockk.mockk + +fun createWulkanowySdkFactoryMock(sdk: Sdk) = mockk() + .apply { + every { create() } returns sdk + every { create(any(), any()) } answers { callOriginal() } + } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt index e64144c2..b3490236 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.createWulkanowySdkFactoryMock import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.dao.AttendanceDao import io.github.wulkanowy.data.db.dao.TimetableDao @@ -16,8 +17,8 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK -import io.mockk.impl.annotations.SpyK import io.mockk.just +import io.mockk.spyk import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest @@ -30,8 +31,8 @@ import io.github.wulkanowy.sdk.pojo.Attendance as SdkAttendance class AttendanceRepositoryTest { - @SpyK - private var sdk = Sdk() + private var sdk = spyk() + private val wulkanowySdkFactory = createWulkanowySdkFactoryMock(sdk) @MockK private lateinit var attendanceDb: AttendanceDao @@ -63,7 +64,8 @@ class AttendanceRepositoryTest { every { refreshHelper.shouldBeRefreshed(any()) } returns false coEvery { timetableDb.load(any(), any(), any(), any()) } returns emptyList() - attendanceRepository = AttendanceRepository(attendanceDb, timetableDb, sdk, refreshHelper) + attendanceRepository = + AttendanceRepository(attendanceDb, timetableDb, wulkanowySdkFactory, refreshHelper) } @Test diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepositoryTest.kt index f8f68850..e20603d2 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepositoryTest.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.createWulkanowySdkFactoryMock import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.dao.CompletedLessonsDao import io.github.wulkanowy.data.errorOrNull @@ -15,8 +16,8 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK -import io.mockk.impl.annotations.SpyK import io.mockk.just +import io.mockk.spyk import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals @@ -28,8 +29,8 @@ import io.github.wulkanowy.sdk.pojo.CompletedLesson as SdkCompletedLesson class CompletedLessonsRepositoryTest { - @SpyK - private var sdk = Sdk() + private var sdk = spyk() + private val wulkanowySdkFactory = createWulkanowySdkFactoryMock(sdk) @MockK private lateinit var completedLessonDb: CompletedLessonsDao @@ -58,7 +59,7 @@ class CompletedLessonsRepositoryTest { every { refreshHelper.shouldBeRefreshed(any()) } returns false completedLessonRepository = - CompletedLessonsRepository(completedLessonDb, sdk, refreshHelper) + CompletedLessonsRepository(completedLessonDb, wulkanowySdkFactory, refreshHelper) } @Test diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt index d1ed9ca3..671c66f9 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.createWulkanowySdkFactoryMock import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.dao.ExamDao import io.github.wulkanowy.data.errorOrNull @@ -15,8 +16,8 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK -import io.mockk.impl.annotations.SpyK import io.mockk.just +import io.mockk.spyk import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest @@ -28,8 +29,8 @@ import io.github.wulkanowy.sdk.pojo.Exam as SdkExam class ExamRemoteTest { - @SpyK - private var sdk = Sdk() + private var sdk = spyk() + private val wulkanowySdkFactory = createWulkanowySdkFactoryMock(sdk) @MockK private lateinit var examDb: ExamDao @@ -59,7 +60,7 @@ class ExamRemoteTest { MockKAnnotations.init(this) every { refreshHelper.shouldBeRefreshed(any()) } returns false - examRepository = ExamRepository(examDb, sdk, refreshHelper) + examRepository = ExamRepository(examDb, wulkanowySdkFactory, refreshHelper) } @Test diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt index 0ea5d3fa..0045badf 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.createWulkanowySdkFactoryMock import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.dao.GradeDao import io.github.wulkanowy.data.db.dao.GradeDescriptiveDao @@ -18,8 +19,8 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK -import io.mockk.impl.annotations.SpyK import io.mockk.just +import io.mockk.spyk import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest @@ -35,8 +36,8 @@ import io.github.wulkanowy.sdk.pojo.Grade as SdkGrade class GradeRepositoryTest { - @SpyK - private var sdk = Sdk() + private var sdk = spyk() + private val wulkanowySdkFactory = createWulkanowySdkFactoryMock(sdk) @MockK private lateinit var gradeDb: GradeDao @@ -65,7 +66,7 @@ class GradeRepositoryTest { gradeDb = gradeDb, gradeSummaryDb = gradeSummaryDb, gradeDescriptiveDb = gradeDescriptiveDb, - sdk = sdk, + wulkanowySdkFactory = wulkanowySdkFactory, refreshHelper = refreshHelper, ) diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepositoryTest.kt index dfd36ee1..6733190b 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepositoryTest.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.createWulkanowySdkFactoryMock import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.dao.GradePartialStatisticsDao import io.github.wulkanowy.data.db.dao.GradePointsStatisticsDao @@ -13,9 +14,14 @@ import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.pojo.GradeStatisticsItem import io.github.wulkanowy.sdk.pojo.GradeStatisticsSubject 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.SpyK +import io.mockk.just +import io.mockk.spyk import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals @@ -24,8 +30,8 @@ import org.junit.Test class GradeStatisticsRepositoryTest { - @SpyK - private var sdk = Sdk() + private var sdk = spyk() + private val wulkanowySdkFactory = createWulkanowySdkFactoryMock(sdk) @MockK private lateinit var gradePartialStatisticsDb: GradePartialStatisticsDao @@ -54,7 +60,7 @@ class GradeStatisticsRepositoryTest { gradePartialStatisticsDb = gradePartialStatisticsDb, gradePointsStatisticsDb = gradePointsStatisticsDb, gradeSemesterStatisticsDb = gradeSemesterStatisticsDb, - sdk = sdk, + wulkanowySdkFactory = wulkanowySdkFactory, refreshHelper = refreshHelper, ) } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/LuckyNumberRemoteTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/LuckyNumberRemoteTest.kt index fa78b1bd..854d5d54 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/LuckyNumberRemoteTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/LuckyNumberRemoteTest.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.createWulkanowySdkFactoryMock import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.dao.LuckyNumberDao import io.github.wulkanowy.data.errorOrNull @@ -12,8 +13,8 @@ import io.mockk.Runs import io.mockk.coEvery import io.mockk.coVerify import io.mockk.impl.annotations.MockK -import io.mockk.impl.annotations.SpyK import io.mockk.just +import io.mockk.spyk import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest @@ -25,8 +26,8 @@ import io.github.wulkanowy.sdk.pojo.LuckyNumber as SdkLuckyNumber class LuckyNumberRemoteTest { - @SpyK - private var sdk = Sdk() + private var sdk = spyk() + private val wulkanowySdkFactory = createWulkanowySdkFactoryMock(sdk) @MockK private lateinit var luckyNumberDb: LuckyNumberDao @@ -43,7 +44,7 @@ class LuckyNumberRemoteTest { fun setUp() { MockKAnnotations.init(this) - luckyNumberRepository = LuckyNumberRepository(luckyNumberDb, sdk) + luckyNumberRepository = LuckyNumberRepository(luckyNumberDb, wulkanowySdkFactory) } @Test diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt index fbbe4934..9819fb1f 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.data.repositories import android.content.Context +import io.github.wulkanowy.createWulkanowySdkFactoryMock import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.dao.MailboxDao @@ -28,10 +29,9 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK -import io.mockk.impl.annotations.SpyK import io.mockk.just import io.mockk.mockk -import kotlinx.coroutines.ExperimentalCoroutinesApi +import io.mockk.spyk import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking @@ -45,11 +45,10 @@ import java.time.Instant import java.time.ZoneOffset import kotlin.test.assertTrue -@OptIn(ExperimentalCoroutinesApi::class) class MessageRepositoryTest { - @SpyK - private var sdk = Sdk() + private var sdk = spyk() + private val wulkanowySdkFactory = createWulkanowySdkFactoryMock(sdk) @MockK private lateinit var messageDb: MessagesDao @@ -102,7 +101,7 @@ class MessageRepositoryTest { messagesDb = messageDb, mutedMessageSendersDao = mutesDb, messageAttachmentDao = messageAttachmentDao, - sdk = sdk, + wulkanowySdkFactory = wulkanowySdkFactory, context = context, refreshHelper = refreshHelper, sharedPrefProvider = sharedPrefProvider, diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt index aa93a5e6..5513a95f 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.createWulkanowySdkFactoryMock import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.dao.MobileDeviceDao import io.github.wulkanowy.data.errorOrNull @@ -16,8 +17,8 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK -import io.mockk.impl.annotations.SpyK import io.mockk.just +import io.mockk.spyk import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Assert @@ -28,8 +29,8 @@ import java.time.ZonedDateTime.of class MobileDeviceRepositoryTest { - @SpyK - private var sdk = Sdk() + private var sdk = spyk() + private val wulkanowySdkFactory = createWulkanowySdkFactoryMock(sdk) @MockK private lateinit var mobileDeviceDb: MobileDeviceDao @@ -53,7 +54,8 @@ class MobileDeviceRepositoryTest { MockKAnnotations.init(this) every { refreshHelper.shouldBeRefreshed(any()) } returns false - mobileDeviceRepository = MobileDeviceRepository(mobileDeviceDb, sdk, refreshHelper) + mobileDeviceRepository = + MobileDeviceRepository(mobileDeviceDb, wulkanowySdkFactory, refreshHelper) } @Test diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/RecipientLocalTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/RecipientLocalTest.kt index e608cafb..0ecaad9e 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/RecipientLocalTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/RecipientLocalTest.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.createWulkanowySdkFactoryMock import io.github.wulkanowy.data.db.dao.RecipientDao import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.getMailboxEntity @@ -7,9 +8,14 @@ import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.pojo.MailboxType 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.SpyK +import io.mockk.just +import io.mockk.spyk import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Before @@ -18,8 +24,8 @@ import io.github.wulkanowy.sdk.pojo.Recipient as SdkRecipient class RecipientLocalTest { - @SpyK - private var sdk = Sdk() + private var sdk = spyk() + private val wulkanowySdkFactory = createWulkanowySdkFactoryMock(sdk) @MockK private lateinit var recipientDb: RecipientDao @@ -63,7 +69,7 @@ class RecipientLocalTest { MockKAnnotations.init(this) every { refreshHelper.shouldBeRefreshed(any()) } returns false - recipientRepository = RecipientRepository(recipientDb, sdk, refreshHelper) + recipientRepository = RecipientRepository(recipientDb, wulkanowySdkFactory, refreshHelper) } @Test diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/SemesterRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/SemesterRepositoryTest.kt index 96db8a79..3a18ac48 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/SemesterRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/SemesterRepositoryTest.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.data.repositories import io.github.wulkanowy.TestDispatchersProvider +import io.github.wulkanowy.createWulkanowySdkFactoryMock import io.github.wulkanowy.data.db.dao.SemesterDao import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.getSemesterEntity @@ -12,8 +13,8 @@ import io.mockk.Runs import io.mockk.coEvery import io.mockk.coVerify import io.mockk.impl.annotations.MockK -import io.mockk.impl.annotations.SpyK import io.mockk.just +import io.mockk.spyk import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals @@ -24,8 +25,8 @@ import java.time.LocalDate.now class SemesterRepositoryTest { - @SpyK - private var sdk = Sdk() + private var sdk = spyk() + private val wulkanowySdkFactory = createWulkanowySdkFactoryMock(sdk) @MockK private lateinit var semesterDb: SemesterDao @@ -38,7 +39,8 @@ class SemesterRepositoryTest { fun initTest() { MockKAnnotations.init(this) - semesterRepository = SemesterRepository(semesterDb, sdk, TestDispatchersProvider()) + semesterRepository = + SemesterRepository(semesterDb, wulkanowySdkFactory, TestDispatchersProvider()) } @Test diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt index 2a61f99c..a1460543 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.createWulkanowySdkFactoryMock import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.dao.TimetableAdditionalDao import io.github.wulkanowy.data.db.dao.TimetableDao @@ -18,9 +19,9 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK -import io.mockk.impl.annotations.SpyK import io.mockk.just import io.mockk.mockk +import io.mockk.spyk import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals @@ -37,8 +38,8 @@ class TimetableRepositoryTest { @MockK(relaxed = true) private lateinit var timetableNotificationSchedulerHelper: TimetableNotificationSchedulerHelper - @SpyK - private var sdk = Sdk() + private var sdk = spyk() + private val wulkanowySdkFactory = createWulkanowySdkFactoryMock(sdk) @MockK private lateinit var timetableDb: TimetableDao @@ -71,7 +72,7 @@ class TimetableRepositoryTest { timetableDb, timetableAdditionalDao, timetableHeaderDao, - sdk, + wulkanowySdkFactory, timetableNotificationSchedulerHelper, refreshHelper ) From 8a90b61b97bd4ed663d1d3b64543d26b0da8446a Mon Sep 17 00:00:00 2001 From: Michael <5672750+mibac138@users.noreply.github.com> Date: Wed, 13 Mar 2024 13:01:00 +0100 Subject: [PATCH 34/43] Refactor networkBoundResource (#2482) --------- Co-authored-by: Faierbel --- .../java/io/github/wulkanowy/data/Resource.kt | 127 ++++++++---------- .../repositories/AdminMessageRepository.kt | 3 +- .../data/repositories/MessageRepository.kt | 2 +- .../io/github/wulkanowy/data/ResourceTest.kt | 10 +- 4 files changed, 62 insertions(+), 80 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/data/Resource.kt b/app/src/main/java/io/github/wulkanowy/data/Resource.kt index b4982b9a..7c6c2a9f 100644 --- a/app/src/main/java/io/github/wulkanowy/data/Resource.kt +++ b/app/src/main/java/io/github/wulkanowy/data/Resource.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow @@ -22,15 +23,15 @@ import timber.log.Timber import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds -sealed class Resource { +sealed interface Resource { - open class Loading : Resource() + open class Loading : Resource data class Intermediate(val data: T) : Loading() - data class Success(val data: T) : Resource() + data class Success(val data: T) : Resource - data class Error(val error: Throwable) : Resource() + data class Error(val error: Throwable) : Resource } val Resource.dataOrNull: T? @@ -97,7 +98,7 @@ fun Flow>.logResourceStatus(name: String, showData: Boolean = fa Timber.i("$name: $description") } -fun Flow>.mapResourceData(block: suspend (T) -> U) = map { +inline fun Flow>.mapResourceData(crossinline block: suspend (T) -> U) = map { when (it) { is Resource.Success -> Resource.Success(block(it.data)) is Resource.Intermediate -> Resource.Intermediate(block(it.data)) @@ -167,33 +168,32 @@ suspend fun Flow>.waitForResult() = takeWhile { it is Resource.L // Can cause excessive amounts of `Resource.Intermediate` to be emitted. Unless that is desired, // use `debounceIntermediates` to alleviate this behavior. -inline fun combineResourceFlows( - flows: Iterable>>, -): Flow>> = combine(flows) { items -> - var isIntermediate = false - val data = mutableListOf() - for (item in items) { - when (item) { - is Resource.Success -> data.add(item.data) - is Resource.Intermediate -> { - isIntermediate = true - data.add(item.data) - } +inline fun combineResourceFlows(flows: Iterable>>): Flow>> = + combine(flows) { items -> + var isIntermediate = false + val data = mutableListOf() + for (item in items) { + when (item) { + is Resource.Success -> data.add(item.data) + is Resource.Intermediate -> { + isIntermediate = true + data.add(item.data) + } - is Resource.Loading -> return@combine Resource.Loading() - is Resource.Error -> continue + is Resource.Loading -> return@combine Resource.Loading() + is Resource.Error -> continue + } + } + if (data.isEmpty()) { + // All items have to be errors for this to happen, so just return the first one. + // mapData is functionally useless and exists only to satisfy the type checker + items.first().mapData { listOf(it) } + } else if (isIntermediate) { + Resource.Intermediate(data) + } else { + Resource.Success(data) } } - if (data.isEmpty()) { - // All items have to be errors for this to happen, so just return the first one. - // mapData is functionally useless and exists only to satisfy the type checker - items.first().mapData { listOf(it) } - } else if (isIntermediate) { - Resource.Intermediate(data) - } else { - Resource.Success(data) - } -} @OptIn(FlowPreview::class) fun Flow>.debounceIntermediates(timeout: Duration = 5.seconds) = flow { @@ -214,70 +214,51 @@ fun Flow>.debounceIntermediates(timeout: Duration = 5.seconds) = }) } + inline fun networkBoundResource( mutex: Mutex = Mutex(), - showSavedOnLoading: Boolean = true, crossinline isResultEmpty: (ResultType) -> Boolean, crossinline query: () -> Flow, - crossinline fetch: suspend (ResultType) -> RequestType, + crossinline fetch: suspend () -> RequestType, crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit, - crossinline onFetchFailed: (Throwable) -> Unit = { }, crossinline shouldFetch: (ResultType) -> Boolean = { true }, crossinline filterResult: (ResultType) -> ResultType = { it } -) = flow { - emit(Resource.Loading()) - - val data = query().first() - emitAll(if (shouldFetch(data)) { - val filteredResult = filterResult(data) - - if (showSavedOnLoading && !isResultEmpty(filteredResult)) { - emit(Resource.Intermediate(filteredResult)) - } - - try { - val newData = fetch(data) - mutex.withLock { saveFetchResult(query().first(), newData) } - query().map { Resource.Success(filterResult(it)) } - } catch (throwable: Throwable) { - onFetchFailed(throwable) - flowOf(Resource.Error(throwable)) - } - } else { - query().map { Resource.Success(filterResult(it)) } - }) -} +) = networkBoundResource( + mutex = mutex, + isResultEmpty = isResultEmpty, + query = query, + fetch = fetch, + saveFetchResult = saveFetchResult, + shouldFetch = shouldFetch, + mapResult = filterResult +) @JvmName("networkBoundResourceWithMap") -inline fun networkBoundResource( +inline fun networkBoundResource( mutex: Mutex = Mutex(), - showSavedOnLoading: Boolean = true, - crossinline isResultEmpty: (T) -> Boolean, + crossinline isResultEmpty: (MappedResultType) -> Boolean, crossinline query: () -> Flow, - crossinline fetch: suspend (ResultType) -> RequestType, + crossinline fetch: suspend () -> RequestType, crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit, - crossinline onFetchFailed: (Throwable) -> Unit = { }, crossinline shouldFetch: (ResultType) -> Boolean = { true }, - crossinline mapResult: (ResultType) -> T, + crossinline mapResult: (ResultType) -> MappedResultType, ) = flow { emit(Resource.Loading()) val data = query().first() - emitAll(if (shouldFetch(data)) { - val mappedResult = mapResult(data) + if (shouldFetch(data)) { + emit(Resource.Intermediate(data)) - if (showSavedOnLoading && !isResultEmpty(mappedResult)) { - emit(Resource.Intermediate(mappedResult)) - } try { - val newData = fetch(data) + val newData = fetch() mutex.withLock { saveFetchResult(query().first(), newData) } - query().map { Resource.Success(mapResult(it)) } } catch (throwable: Throwable) { - onFetchFailed(throwable) - flowOf(Resource.Error(throwable)) + emit(Resource.Error(throwable)) + return@flow } - } else { - query().map { Resource.Success(mapResult(it)) } - }) + } + + emitAll(query().map { Resource.Success(it) }) } + .mapResourceData { mapResult(it) } + .filterNot { it is Resource.Intermediate && isResultEmpty(it.data) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt index b831ee75..aa0022b0 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt @@ -6,6 +6,7 @@ import io.github.wulkanowy.data.db.dao.AdminMessageDao import io.github.wulkanowy.data.db.entities.AdminMessage import io.github.wulkanowy.data.networkBoundResource import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -28,6 +29,6 @@ class AdminMessageRepository @Inject constructor( saveFetchResult = { oldItems, newItems -> adminMessageDao.removeOldAndSaveNew(oldItems, newItems) }, - showSavedOnLoading = false, ) + .filterNot { it is Resource.Intermediate } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt index ede2a0fd..f91dc63e 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt @@ -122,7 +122,7 @@ class MessageRepository @Inject constructor( fetch = { wulkanowySdkFactory.create(student) .getMessageDetails( - messageKey = it!!.message.messageGlobalKey, + messageKey = message.messageGlobalKey, markAsRead = message.unread && markAsRead, ) }, diff --git a/app/src/test/java/io/github/wulkanowy/data/ResourceTest.kt b/app/src/test/java/io/github/wulkanowy/data/ResourceTest.kt index ea846a57..aa79a637 100644 --- a/app/src/test/java/io/github/wulkanowy/data/ResourceTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/ResourceTest.kt @@ -1,6 +1,10 @@ package io.github.wulkanowy.data -import io.mockk.* +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerifyOrder +import io.mockk.just +import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.flowOf @@ -42,7 +46,6 @@ class ResourceTest { // first networkBoundResource( isResultEmpty = { false }, - showSavedOnLoading = false, query = { repo.query() }, fetch = { val data = repo.fetch() @@ -57,7 +60,6 @@ class ResourceTest { // second networkBoundResource( isResultEmpty = { false }, - showSavedOnLoading = false, query = { repo.query() }, fetch = { val data = repo.fetch() @@ -124,7 +126,6 @@ class ResourceTest { networkBoundResource( isResultEmpty = { false }, mutex = saveResultMutex, - showSavedOnLoading = false, query = { repo.query() }, fetch = { val data = repo.fetch() @@ -143,7 +144,6 @@ class ResourceTest { networkBoundResource( isResultEmpty = { false }, mutex = saveResultMutex, - showSavedOnLoading = false, query = { repo.query() }, fetch = { val data = repo.fetch() From 961bc24f2799355638ab839cbd72b31b3db2bdb4 Mon Sep 17 00:00:00 2001 From: Michael <5672750+mibac138@users.noreply.github.com> Date: Wed, 13 Mar 2024 19:13:56 +0100 Subject: [PATCH 35/43] Add docs to Resource, changing networkBoundResource generics naming (#2483) --- app/play-publish-lint.sh | 3 +- .../java/io/github/wulkanowy/data/Resource.kt | 51 +++++++++++++------ 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/app/play-publish-lint.sh b/app/play-publish-lint.sh index d3354b1a..5f0391de 100755 --- a/app/play-publish-lint.sh +++ b/app/play-publish-lint.sh @@ -1,7 +1,8 @@ #!/bin/bash - content=$(cat < "app/src/main/play/release-notes/pl-PL/default.txt") || exit -if [[ "${#content}" -gt 500 ]]; then +content2=echo "$content" | dos2unix +if [[ "${#content2}" -gt 500 ]]; then echo >&2 "Release notes content has reached the limit of 500 characters" exit 1 fi diff --git a/app/src/main/java/io/github/wulkanowy/data/Resource.kt b/app/src/main/java/io/github/wulkanowy/data/Resource.kt index 7c6c2a9f..712a946f 100644 --- a/app/src/main/java/io/github/wulkanowy/data/Resource.kt +++ b/app/src/main/java/io/github/wulkanowy/data/Resource.kt @@ -24,13 +24,34 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds sealed interface Resource { - + /** + * The initial value of a resource flow. Indicates no data that is currently available to be shown, + * however with the expectation that the state will transition to another one soon. + */ open class Loading : Resource + /** + * A semi-loading state with some data available to be displayed (usually cached data loaded from + * the database). Still not the target state and it's expected to transition into another one soon. + */ data class Intermediate(val data: T) : Loading() + /** + * The happy-path target state. Data can either be: + * - loaded from the database - while it may seem like this case is already handled by the + * Intermediate state, the difference here is semantic. Cached data is returned as Intermediate + * when there's a API request in progress (or soon expected to be), however when there is no + * intention of immediately querying the API, the cached data is returned as a Success. + * - fetched from the API. + */ data class Success(val data: T) : Resource + /** + * Something bad happened and we were unable to get the requested data. This can be caused by + * a database error, a network error, or really just any other error. Upon receiving this state + * the UI can either: display a full screen error, or, when it has received any data previously, + * display a snack bar informing of the problem. + */ data class Error(val error: Throwable) : Resource } @@ -215,14 +236,14 @@ fun Flow>.debounceIntermediates(timeout: Duration = 5.seconds) = } -inline fun networkBoundResource( +inline fun networkBoundResource( mutex: Mutex = Mutex(), - crossinline isResultEmpty: (ResultType) -> Boolean, - crossinline query: () -> Flow, - crossinline fetch: suspend () -> RequestType, - crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit, - crossinline shouldFetch: (ResultType) -> Boolean = { true }, - crossinline filterResult: (ResultType) -> ResultType = { it } + crossinline isResultEmpty: (OutputType) -> Boolean, + crossinline query: () -> Flow, + crossinline fetch: suspend () -> ApiType, + crossinline saveFetchResult: suspend (old: OutputType, new: ApiType) -> Unit, + crossinline shouldFetch: (OutputType) -> Boolean = { true }, + crossinline filterResult: (OutputType) -> OutputType = { it } ) = networkBoundResource( mutex = mutex, isResultEmpty = isResultEmpty, @@ -234,14 +255,14 @@ inline fun networkBoundResource( ) @JvmName("networkBoundResourceWithMap") -inline fun networkBoundResource( +inline fun networkBoundResource( mutex: Mutex = Mutex(), - crossinline isResultEmpty: (MappedResultType) -> Boolean, - crossinline query: () -> Flow, - crossinline fetch: suspend () -> RequestType, - crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit, - crossinline shouldFetch: (ResultType) -> Boolean = { true }, - crossinline mapResult: (ResultType) -> MappedResultType, + crossinline isResultEmpty: (OutputType) -> Boolean, + crossinline query: () -> Flow, + crossinline fetch: suspend () -> ApiType, + crossinline saveFetchResult: suspend (old: DatabaseType, new: ApiType) -> Unit, + crossinline shouldFetch: (DatabaseType) -> Boolean = { true }, + crossinline mapResult: (DatabaseType) -> OutputType, ) = flow { emit(Resource.Loading()) From 34d34a050afc1ce81ef465c8ef47e04368484695 Mon Sep 17 00:00:00 2001 From: Michael <5672750+mibac138@users.noreply.github.com> Date: Sun, 17 Mar 2024 21:06:40 +0100 Subject: [PATCH 36/43] Add widget updating on data sync (#2487) --------- Co-authored-by: Faierbel --- .../repositories/LuckyNumberRepository.kt | 7 ++++ .../data/repositories/TimetableRepository.kt | 10 +++++- .../LuckyNumberWidgetProvider.kt | 6 +++- .../timetablewidget/TimetableWidgetFactory.kt | 9 ++++- .../wulkanowy/utils/AppWidgetUpdater.kt | 34 +++++++++++++++++++ .../repositories/LuckyNumberRemoteTest.kt | 7 +++- .../repositories/TimetableRepositoryTest.kt | 7 +++- 7 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/io/github/wulkanowy/utils/AppWidgetUpdater.kt diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt index 3636cb51..dfafe6c8 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt @@ -6,6 +6,8 @@ import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntity import io.github.wulkanowy.data.networkBoundResource +import io.github.wulkanowy.ui.modules.luckynumberwidget.LuckyNumberWidgetProvider +import io.github.wulkanowy.utils.AppWidgetUpdater import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.sync.Mutex @@ -18,6 +20,7 @@ import javax.inject.Singleton class LuckyNumberRepository @Inject constructor( private val luckyNumberDb: LuckyNumberDao, private val wulkanowySdkFactory: WulkanowySdkFactory, + private val appWidgetUpdater: AppWidgetUpdater, ) { private val saveFetchResultMutex = Mutex() @@ -26,6 +29,7 @@ class LuckyNumberRepository @Inject constructor( student: Student, forceRefresh: Boolean, notify: Boolean = false, + isFromAppWidget: Boolean = false ) = networkBoundResource( mutex = saveFetchResultMutex, isResultEmpty = { it == null }, @@ -44,6 +48,9 @@ class LuckyNumberRepository @Inject constructor( oldItems = listOfNotNull(oldLuckyNumber), newItems = listOf(newLuckyNumber.apply { if (notify) isNotified = false }), ) + if (!isFromAppWidget) { + appWidgetUpdater.updateAllAppWidgetsByProvider(LuckyNumberWidgetProvider::class) + } } } ) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt index 33578999..60c562e1 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt @@ -13,6 +13,8 @@ import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.data.pojos.TimetableFull import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper +import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider +import io.github.wulkanowy.utils.AppWidgetUpdater import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.monday @@ -26,6 +28,7 @@ import java.time.LocalDate import javax.inject.Inject import javax.inject.Singleton + @Singleton class TimetableRepository @Inject constructor( private val timetableDb: TimetableDao, @@ -34,6 +37,7 @@ class TimetableRepository @Inject constructor( private val wulkanowySdkFactory: WulkanowySdkFactory, private val schedulerHelper: TimetableNotificationSchedulerHelper, private val refreshHelper: AutoRefreshHelper, + private val appWidgetUpdater: AppWidgetUpdater, ) { private val saveFetchResultMutex = Mutex() @@ -52,7 +56,8 @@ class TimetableRepository @Inject constructor( forceRefresh: Boolean, refreshAdditional: Boolean = false, notify: Boolean = false, - timetableType: TimetableType = TimetableType.NORMAL + timetableType: TimetableType = TimetableType.NORMAL, + isFromAppWidget: Boolean = false ) = networkBoundResource( mutex = saveFetchResultMutex, isResultEmpty = { @@ -83,6 +88,9 @@ class TimetableRepository @Inject constructor( refreshDayHeaders(timetableOld.headers, timetableNew.headers) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end)) + if (!isFromAppWidget) { + appWidgetUpdater.updateAllAppWidgetsByProvider(TimetableWidgetProvider::class) + } }, filterResult = { (timetable, additional, headers) -> TimetableFull( diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt index 1ab079a3..e6de1781 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt @@ -145,7 +145,11 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { } if (currentStudent != null) { - luckyNumberRepository.getLuckyNumber(currentStudent, forceRefresh = false) + luckyNumberRepository.getLuckyNumber( + student = currentStudent, + forceRefresh = false, + isFromAppWidget = true + ) .toFirstResult() .dataOrThrow } else null diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt index 4cfc0322..1eb33dbc 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt @@ -104,7 +104,14 @@ class TimetableWidgetFactory( private suspend fun getLessons( student: Student, semester: Semester, date: LocalDate ): List { - val timetable = timetableRepository.getTimetable(student, semester, date, date, false) + val timetable = timetableRepository.getTimetable( + student = student, + semester = semester, + start = date, + end = date, + forceRefresh = false, + isFromAppWidget = true + ) val lessons = timetable.toFirstResult().dataOrThrow.lessons return lessons.sortedBy { it.number } } diff --git a/app/src/main/java/io/github/wulkanowy/utils/AppWidgetUpdater.kt b/app/src/main/java/io/github/wulkanowy/utils/AppWidgetUpdater.kt new file mode 100644 index 00000000..1b54f40c --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/utils/AppWidgetUpdater.kt @@ -0,0 +1,34 @@ +package io.github.wulkanowy.utils + +import android.appwidget.AppWidgetManager +import android.content.BroadcastReceiver +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import dagger.hilt.android.qualifiers.ApplicationContext +import timber.log.Timber +import javax.inject.Inject +import kotlin.reflect.KClass + +class AppWidgetUpdater @Inject constructor( + @ApplicationContext private val context: Context, + private val appWidgetManager: AppWidgetManager +) { + + fun updateAllAppWidgetsByProvider(providerClass: KClass) { + try { + val ids = appWidgetManager.getAppWidgetIds(ComponentName(context, providerClass.java)) + if (ids.isEmpty()) return + + val intent = Intent(context, providerClass.java) + .apply { + action = AppWidgetManager.ACTION_APPWIDGET_UPDATE + putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids) + } + + context.sendBroadcast(intent) + } catch (e: Exception) { + Timber.e(e, "Failed to update all widgets for provider $providerClass") + } + } +} diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/LuckyNumberRemoteTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/LuckyNumberRemoteTest.kt index 854d5d54..560eb3e7 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/LuckyNumberRemoteTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/LuckyNumberRemoteTest.kt @@ -8,6 +8,7 @@ import io.github.wulkanowy.data.mappers.mapToEntity import io.github.wulkanowy.data.toFirstResult import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.utils.AppWidgetUpdater import io.mockk.MockKAnnotations import io.mockk.Runs import io.mockk.coEvery @@ -32,6 +33,9 @@ class LuckyNumberRemoteTest { @MockK private lateinit var luckyNumberDb: LuckyNumberDao + @MockK(relaxed = true) + private lateinit var appWidgetUpdater: AppWidgetUpdater + private val student = getStudentEntity() private lateinit var luckyNumberRepository: LuckyNumberRepository @@ -44,7 +48,8 @@ class LuckyNumberRemoteTest { fun setUp() { MockKAnnotations.init(this) - luckyNumberRepository = LuckyNumberRepository(luckyNumberDb, wulkanowySdkFactory) + luckyNumberRepository = + LuckyNumberRepository(luckyNumberDb, wulkanowySdkFactory, appWidgetUpdater) } @Test diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt index a1460543..2a45f175 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt @@ -12,6 +12,7 @@ import io.github.wulkanowy.getSemesterEntity import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper +import io.github.wulkanowy.utils.AppWidgetUpdater import io.github.wulkanowy.utils.AutoRefreshHelper import io.mockk.MockKAnnotations import io.mockk.Runs @@ -53,6 +54,9 @@ class TimetableRepositoryTest { @MockK(relaxUnitFun = true) private lateinit var refreshHelper: AutoRefreshHelper + @MockK(relaxed = true) + private lateinit var appWidgetUpdater: AppWidgetUpdater + private val student = getStudentEntity() private val semester = getSemesterEntity() @@ -74,7 +78,8 @@ class TimetableRepositoryTest { timetableHeaderDao, wulkanowySdkFactory, timetableNotificationSchedulerHelper, - refreshHelper + refreshHelper, + appWidgetUpdater ) } From d01fe9c370720f3baba52701b3e467d603d81cf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Tue, 19 Mar 2024 22:11:02 +0100 Subject: [PATCH 37/43] Bump sdk to 2.5.2-SNAPSHOT --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 31de5104..7ecfc818 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -195,7 +195,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.5.1' + implementation 'io.github.wulkanowy:sdk:2.5.2-SNAPSHOT' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' From b31c7e1720552c340fca3cf58b5073a9f82926ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Mon, 11 Mar 2024 11:44:59 +0100 Subject: [PATCH 38/43] Fix task description color crash (#2475) --- .gitignore | 4 ++++ .../io/github/wulkanowy/ui/base/BaseActivity.kt | 17 +++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 980085e3..69d3ad5d 100644 --- a/.gitignore +++ b/.gitignore @@ -67,6 +67,10 @@ captures/ .idea/discord.xml .idea/migrations.xml .idea/androidTestResultsUserPreferences.xml +.idea/copilot +.idea/deploymentTargetDropDown.xml +.idea/deploymentTargetSelector.xml +.idea/kotlinc.xml # Keystore files *.jks diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt index 10735dab..922c3536 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.ui.base import android.app.ActivityManager +import android.os.Build import android.os.Bundle import android.view.View import android.widget.Toast @@ -45,11 +46,19 @@ abstract class BaseActivity, VB : ViewBinding> : themeManager.applyActivityTheme(this) super.onCreate(savedInstanceState) supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleLogger, true) + applyCustomTaskDescription() + } - @Suppress("DEPRECATION") - setTaskDescription( - ActivityManager.TaskDescription(null, null, getThemeAttrColor(R.attr.colorSurface)) - ) + @Suppress("DEPRECATION") + private fun applyCustomTaskDescription() { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) return + try { + val newColor = getThemeAttrColor(R.attr.colorSurface) + val taskDescription = ActivityManager.TaskDescription(null, null, newColor) + setTaskDescription(taskDescription) + } catch (e: Exception) { + Timber.e(e) + } } override fun showError(text: String, error: Throwable) { From 377c288e9ef7a4ea956318161c5641b65b79c358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Mon, 11 Mar 2024 11:45:15 +0100 Subject: [PATCH 39/43] Add missing onDetachView in AutDialog (#2476) --- .../java/io/github/wulkanowy/ui/modules/auth/AuthDialog.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthDialog.kt index fa29df47..0f7c4234 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthDialog.kt @@ -78,4 +78,9 @@ class AuthDialog : BaseDialogFragment(), AuthView { override fun showDescriptionWithName(name: String) { binding.authDescription.text = getString(R.string.auth_description, name).parseAsHtml() } + + override fun onDestroyView() { + presenter.onDetachView() + super.onDestroyView() + } } From 78e28ad791b7aa23234362f931f5d7577d6f6e3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Mon, 11 Mar 2024 11:45:28 +0100 Subject: [PATCH 40/43] Remove savedInstance in MessagePreviewFragment (#2477) --- .../mailboxchooser/MailboxChooserDialog.kt | 1 - .../message/preview/MessagePreviewFragment.kt | 11 +++-------- .../message/preview/MessagePreviewPresenter.kt | 13 +++++++++---- .../github/wulkanowy/utils/BundleExtension.kt | 17 +++++++++-------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserDialog.kt index 8bd84f2b..11d3c6c1 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/mailboxchooser/MailboxChooserDialog.kt @@ -47,7 +47,6 @@ class MailboxChooserDialog : BaseDialogFragment(), } - @Suppress("UNCHECKED_CAST") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) presenter.onAttachView( diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt index ebdb96a4..8e7c7276 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt @@ -82,10 +82,10 @@ class MessagePreviewFragment : get() = getString(R.string.message_not_exists) companion object { - const val MESSAGE_ID_KEY = "message_id" + private const val MESSAGE_ARG_KEY = "message" fun newInstance(message: Message) = MessagePreviewFragment().apply { - arguments = bundleOf(MESSAGE_ID_KEY to message) + arguments = bundleOf(MESSAGE_ARG_KEY to message) } } @@ -101,7 +101,7 @@ class MessagePreviewFragment : messageContainer = binding.messagePreviewContainer presenter.onAttachView( view = this, - message = (savedInstanceState ?: arguments)?.serializable(MESSAGE_ID_KEY), + message = requireArguments().serializable(MESSAGE_ARG_KEY), ) } @@ -233,11 +233,6 @@ class MessagePreviewFragment : (activity as MainActivity).popView() } - override fun onSaveInstanceState(outState: Bundle) { - outState.putSerializable(MESSAGE_ID_KEY, presenter.messageWithAttachments?.message) - super.onSaveInstanceState(outState) - } - override fun onDestroyView() { presenter.onDetachView() super.onDestroyView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt index 9bb0d32a..3b3b2b42 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt @@ -3,10 +3,15 @@ package io.github.wulkanowy.ui.modules.message.preview import android.annotation.SuppressLint import androidx.core.text.parseAsHtml import io.github.wulkanowy.R -import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.MessageWithAttachment import io.github.wulkanowy.data.enums.MessageFolder +import io.github.wulkanowy.data.flatResourceFlow +import io.github.wulkanowy.data.logResourceStatus +import io.github.wulkanowy.data.onResourceData +import io.github.wulkanowy.data.onResourceError +import io.github.wulkanowy.data.onResourceNotLoading +import io.github.wulkanowy.data.onResourceSuccess import io.github.wulkanowy.data.repositories.MessageRepository import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository @@ -28,17 +33,17 @@ class MessagePreviewPresenter @Inject constructor( private val analytics: AnalyticsHelper ) : BasePresenter(errorHandler, studentRepository) { - var messageWithAttachments: MessageWithAttachment? = null + private var messageWithAttachments: MessageWithAttachment? = null private lateinit var lastError: Throwable private var retryCallback: () -> Unit = {} - fun onAttachView(view: MessagePreviewView, message: Message?) { + fun onAttachView(view: MessagePreviewView, message: Message) { super.onAttachView(view) view.initView() errorHandler.showErrorMessage = ::showErrorViewOnError - loadData(requireNotNull(message)) + loadData(message) } private fun onMessageLoadRetry(message: Message) { diff --git a/app/src/main/java/io/github/wulkanowy/utils/BundleExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/BundleExtension.kt index d3c9f800..b1742b4f 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/BundleExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/BundleExtension.kt @@ -4,30 +4,31 @@ import android.content.Intent import android.os.Build import android.os.Bundle import android.os.Parcelable +import androidx.core.os.BundleCompat import java.io.Serializable +// Even though API was introduced in 33, we use 34 as 33 is bugged in some scenarios. + inline fun Bundle.serializable(key: String): T = when { - Build.VERSION.SDK_INT >= 33 -> getSerializable(key, T::class.java)!! + Build.VERSION.SDK_INT >= 34 -> getSerializable(key, T::class.java)!! else -> @Suppress("DEPRECATION") getSerializable(key) as T } inline fun Bundle.nullableSerializable(key: String): T? = when { - Build.VERSION.SDK_INT >= 33 -> getSerializable(key, T::class.java) + Build.VERSION.SDK_INT >= 34 -> getSerializable(key, T::class.java) else -> @Suppress("DEPRECATION") getSerializable(key) as T? } @Suppress("UNCHECKED_CAST") -inline fun Bundle.parcelableArray(key: String): Array? = when { - Build.VERSION.SDK_INT >= 33 -> getParcelableArray(key, T::class.java) - else -> @Suppress("DEPRECATION") getParcelableArray(key) as Array? -} +inline fun Bundle.parcelableArray(key: String): Array? = + BundleCompat.getParcelableArray(this, key, T::class.java) as Array? inline fun Intent.serializable(key: String): T = when { - Build.VERSION.SDK_INT >= 33 -> getSerializableExtra(key, T::class.java)!! + Build.VERSION.SDK_INT >= 34 -> getSerializableExtra(key, T::class.java)!! else -> @Suppress("DEPRECATION") getSerializableExtra(key) as T } inline fun Intent.nullableSerializable(key: String): T? = when { - Build.VERSION.SDK_INT >= 33 -> getSerializableExtra(key, T::class.java) + Build.VERSION.SDK_INT >= 34 -> getSerializableExtra(key, T::class.java) else -> @Suppress("DEPRECATION") getSerializableExtra(key) as T? } From 8623b53357be1dff37ce77f6fa3b672cb24bff1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Mon, 11 Mar 2024 23:38:17 +0100 Subject: [PATCH 41/43] Fix lateness color in attendance (#2481) --- .../modules/attendance/AttendanceAdapter.kt | 25 +++++++++++-------- app/src/main/res/values/colors.xml | 5 ++-- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt index 4e9baac3..f5689ec8 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.ui.modules.attendance +import android.content.res.ColorStateList import android.graphics.Typeface import android.view.LayoutInflater import android.view.View @@ -33,17 +34,17 @@ class AttendanceAdapter @Inject constructor() : ) override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + val context = holder.binding.root.context val item = items[position] with(holder.binding) { attendanceItemNumber.text = item.number.toString() - attendanceItemSubject.text = item.subject.ifBlank { - root.context.getString(R.string.all_no_data) - } + attendanceItemSubject.text = item.subject + .ifBlank { context.getString(R.string.all_no_data) } attendanceItemDescription.setText(item.descriptionRes) attendanceItemDescription.setTextColor( - root.context.getThemeAttrColor( + context.getThemeAttrColor( when { item.absence && !item.excused -> R.attr.colorAttendanceAbsence item.lateness && !item.excused -> R.attr.colorAttendanceLateness @@ -61,13 +62,15 @@ class AttendanceAdapter @Inject constructor() : attendanceItemAlert.isVisible = item.let { (it.absence && !it.excused) || (it.lateness && !it.excused) } - attendanceItemAlert.setColorFilter(root.context.getThemeAttrColor( - when{ - item.absence && !item.excused -> R.attr.colorAttendanceAbsence - item.lateness && !item.excused -> R.attr.colorAttendanceLateness - else -> android.R.attr.colorPrimary - } - )) + attendanceItemAlert.imageTintList = ColorStateList.valueOf( + context.getThemeAttrColor( + when { + item.absence && !item.excused -> R.attr.colorAttendanceAbsence + item.lateness && !item.excused -> R.attr.colorAttendanceLateness + else -> android.R.attr.colorPrimary + } + ) + ) attendanceItemNumber.visibility = View.GONE attendanceItemExcuseInfo.visibility = View.GONE attendanceItemExcuseCheckbox.visibility = View.GONE diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 8ad27ad8..f31a5f94 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -46,14 +46,15 @@ #d32f2f #e57373 + #ff8f00 #ffd54f #d32f2f #e57373 - #cd2a01 - #f05d0e + #ff8f00 + #ffd54f #1f000000 #1fffffff From d789aa718e8fda3bec328334812f456dacd15bd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Wed, 20 Mar 2024 01:49:55 +0100 Subject: [PATCH 42/43] Change AuthDialog condition to isAuth flag (#2495) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mikołaj Pich --- .../62.json | 2547 +++++++++++++++++ .../github/wulkanowy/data/db/AppDatabase.kt | 5 +- .../wulkanowy/data/db/dao/StudentDao.kt | 4 + .../wulkanowy/data/db/entities/Student.kt | 7 + .../data/db/entities/StudentIsAuthorized.kt | 16 + .../data/mappers/RegisterUserMapper.kt | 24 +- .../wulkanowy/data/pojos/RegisterUser.kt | 2 + .../data/repositories/StudentRepository.kt | 24 + .../github/wulkanowy/ui/base/ErrorHandler.kt | 4 +- .../ui/modules/main/MainPresenter.kt | 10 + .../io/github/wulkanowy/utils/SdkExtension.kt | 1 + .../io/github/wulkanowy/TestEnityCreator.kt | 4 +- .../domain/GetMailboxByStudentUseCaseTest.kt | 2 + .../modules/grade/GradeAverageProviderTest.kt | 4 +- .../LoginStudentSelectPresenterTest.kt | 2 + .../ui/modules/main/MainPresenterTest.kt | 12 +- 16 files changed, 2651 insertions(+), 17 deletions(-) create mode 100644 app/schemas/io.github.wulkanowy.data.db.AppDatabase/62.json create mode 100644 app/src/main/java/io/github/wulkanowy/data/db/entities/StudentIsAuthorized.kt diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/62.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/62.json new file mode 100644 index 00000000..ab63c679 --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/62.json @@ -0,0 +1,2547 @@ +{ + "formatVersion": 1, + "database": { + "version": 62, + "identityHash": "ee2464d218b254ca868667c0fc756c0b", + "entities": [ + { + "tableName": "Students", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scrapper_base_url` TEXT NOT NULL, `scrapper_domain_suffix` TEXT NOT NULL DEFAULT '', `mobile_base_url` TEXT NOT NULL, `login_type` TEXT NOT NULL, `login_mode` TEXT NOT NULL, `certificate_key` TEXT NOT NULL, `private_key` TEXT NOT NULL, `is_parent` INTEGER NOT NULL, `email` TEXT NOT NULL, `password` TEXT NOT NULL, `symbol` TEXT NOT NULL, `student_id` INTEGER NOT NULL, `user_login_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `student_name` TEXT NOT NULL, `school_id` TEXT NOT NULL, `school_short` TEXT NOT NULL, `school_name` TEXT NOT NULL, `class_name` TEXT NOT NULL, `class_id` INTEGER NOT NULL, `is_current` INTEGER NOT NULL, `registration_date` INTEGER NOT NULL, `is_authorized` INTEGER NOT NULL DEFAULT 0, `is_edu_one` INTEGER NOT NULL DEFAULT 0, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `nick` TEXT NOT NULL, `avatar_color` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "scrapperBaseUrl", + "columnName": "scrapper_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scrapperDomainSuffix", + "columnName": "scrapper_domain_suffix", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "mobileBaseUrl", + "columnName": "mobile_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginType", + "columnName": "login_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginMode", + "columnName": "login_mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "certificateKey", + "columnName": "certificate_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "private_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isParent", + "columnName": "is_parent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "user_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "student_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolSymbol", + "columnName": "school_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "school_short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolName", + "columnName": "school_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "className", + "columnName": "class_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isCurrent", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registrationDate", + "columnName": "registration_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAuthorized", + "columnName": "is_authorized", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isEduOne", + "columnName": "is_edu_one", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nick", + "columnName": "nick", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarColor", + "columnName": "avatar_color", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Students_email_symbol_student_id_school_id_class_id", + "unique": true, + "columnNames": [ + "email", + "symbol", + "student_id", + "school_id", + "class_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Semesters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `kindergarten_diary_id` INTEGER NOT NULL DEFAULT 0, `diary_name` TEXT NOT NULL, `school_year` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `semester_name` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_current` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "kindergartenDiaryId", + "columnName": "kindergarten_diary_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "diaryName", + "columnName": "diary_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolYear", + "columnName": "school_year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterName", + "columnName": "semester_name", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "kindergarten_diary_id", + "semester_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `diary_id`, `kindergarten_diary_id`, `semester_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Exams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `group` TEXT NOT NULL, `type` TEXT NOT NULL, `description` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Timetable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `number` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `subjectOld` TEXT NOT NULL, `group` TEXT NOT NULL, `room` TEXT NOT NULL, `roomOld` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacherOld` TEXT NOT NULL, `info` TEXT NOT NULL, `student_plan` INTEGER NOT NULL, `changes` INTEGER NOT NULL, `canceled` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subjectOld", + "columnName": "subjectOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "room", + "columnName": "room", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roomOld", + "columnName": "roomOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherOld", + "columnName": "teacherOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "info", + "columnName": "info", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isStudentPlan", + "columnName": "student_plan", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "changes", + "columnName": "changes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canceled", + "columnName": "canceled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Attendance", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `time_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `excusable` INTEGER NOT NULL, `excuse_status` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeId", + "columnName": "time_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excusable", + "columnName": "excusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excuseStatus", + "columnName": "excuse_status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AttendanceSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `subject_id` INTEGER NOT NULL, `month` INTEGER NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `absence_excused` INTEGER NOT NULL, `absence_for_school_reasons` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `lateness_excused` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subject_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "month", + "columnName": "month", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceExcused", + "columnName": "absence_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceForSchoolReasons", + "columnName": "absence_for_school_reasons", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latenessExcused", + "columnName": "lateness_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Grades", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `entry` TEXT NOT NULL, `value` REAL NOT NULL, `modifier` REAL NOT NULL, `comment` TEXT NOT NULL, `color` TEXT NOT NULL, `grade_symbol` TEXT NOT NULL, `description` TEXT NOT NULL, `weight` TEXT NOT NULL, `weightValue` REAL NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entry", + "columnName": "entry", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "modifier", + "columnName": "modifier", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gradeSymbol", + "columnName": "grade_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weightValue", + "columnName": "weightValue", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `subject` TEXT NOT NULL, `predicted_grade` TEXT NOT NULL, `final_grade` TEXT NOT NULL, `proposed_points` TEXT NOT NULL, `final_points` TEXT NOT NULL, `points_sum` TEXT NOT NULL, `average` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_predicted_grade_notified` INTEGER NOT NULL, `is_final_grade_notified` INTEGER NOT NULL, `predicted_grade_last_change` INTEGER NOT NULL, `final_grade_last_change` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "predictedGrade", + "columnName": "predicted_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalGrade", + "columnName": "final_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "proposedPoints", + "columnName": "proposed_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalPoints", + "columnName": "final_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pointsSum", + "columnName": "points_sum", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "average", + "columnName": "average", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPredictedGradeNotified", + "columnName": "is_predicted_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFinalGradeNotified", + "columnName": "is_final_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "predictedGradeLastChange", + "columnName": "predicted_grade_last_change", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "finalGradeLastChange", + "columnName": "final_grade_last_change", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradePartialStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `class_average` TEXT NOT NULL, `student_average` TEXT NOT NULL, `class_amounts` TEXT NOT NULL, `student_amounts` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAverage", + "columnName": "class_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAverage", + "columnName": "student_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAmounts", + "columnName": "class_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAmounts", + "columnName": "student_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesPointsStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `others` REAL NOT NULL, `student` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "others", + "columnName": "others", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "student", + "columnName": "student", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradeSemesterStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `amounts` TEXT NOT NULL, `student_grade` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amounts", + "columnName": "amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentGrade", + "columnName": "student_grade", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `message_global_key` TEXT NOT NULL, `mailbox_key` TEXT NOT NULL, `message_id` INTEGER NOT NULL, `correspondents` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `read_by` INTEGER, `unread_by` INTEGER, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `content` TEXT NOT NULL, `sender` TEXT, `recipients` TEXT)", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "messageGlobalKey", + "columnName": "message_global_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mailboxKey", + "columnName": "mailbox_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "correspondents", + "columnName": "correspondents", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasAttachments", + "columnName": "has_attachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recipients", + "columnName": "recipients", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageAttachments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`message_global_key` TEXT NOT NULL, `url` TEXT NOT NULL, `filename` TEXT NOT NULL, PRIMARY KEY(`message_global_key`, `url`, `filename`))", + "fields": [ + { + "fieldPath": "messageGlobalKey", + "columnName": "message_global_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "message_global_key", + "url", + "filename" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `category` TEXT NOT NULL, `category_type` INTEGER NOT NULL, `is_points_show` INTEGER NOT NULL, `points` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryType", + "columnName": "category_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPointsShow", + "columnName": "is_points_show", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Homework", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `attachments` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_done` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL, `is_added_by_user` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDone", + "columnName": "is_done", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Subjects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "LuckyNumbers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `lucky_number` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "luckyNumber", + "columnName": "lucky_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CompletedLesson", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `topic` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `substitution` TEXT NOT NULL, `absence` TEXT NOT NULL, `resources` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "topic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "substitution", + "columnName": "substitution", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "resources", + "columnName": "resources", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Mailboxes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`globalKey` TEXT NOT NULL, `email` TEXT NOT NULL, `symbol` TEXT NOT NULL, `schoolId` TEXT NOT NULL, `fullName` TEXT NOT NULL, `userName` TEXT NOT NULL, `studentName` TEXT NOT NULL, `schoolNameShort` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`globalKey`))", + "fields": [ + { + "fieldPath": "globalKey", + "columnName": "globalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolId", + "columnName": "schoolId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "studentName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolNameShort", + "columnName": "schoolNameShort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "globalKey" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Recipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mailboxGlobalKey` TEXT NOT NULL, `studentMailboxGlobalKey` TEXT NOT NULL, `fullName` TEXT NOT NULL, `userName` TEXT NOT NULL, `schoolShortName` TEXT NOT NULL, `type` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "mailboxGlobalKey", + "columnName": "mailboxGlobalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentMailboxGlobalKey", + "columnName": "studentMailboxGlobalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "schoolShortName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MobileDevices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_login_id` INTEGER NOT NULL, `device_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Teachers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `short_name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "School", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL, `contact` TEXT NOT NULL, `headmaster` TEXT NOT NULL, `pedagogue` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contact", + "columnName": "contact", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "headmaster", + "columnName": "headmaster", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pedagogue", + "columnName": "pedagogue", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Conferences", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `subject` TEXT NOT NULL, `agenda` TEXT NOT NULL, `present_on_conference` TEXT NOT NULL, `conference_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agenda", + "columnName": "agenda", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presentOnConference", + "columnName": "present_on_conference", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conference_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableAdditional", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `repeat_id` BLOB DEFAULT NULL, `is_added_by_user` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatId", + "columnName": "repeat_id", + "affinity": "BLOB", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "StudentInfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `full_name` TEXT NOT NULL, `first_name` TEXT NOT NULL, `second_name` TEXT NOT NULL, `surname` TEXT NOT NULL, `birth_date` INTEGER NOT NULL, `birth_place` TEXT NOT NULL, `gender` TEXT NOT NULL, `has_polish_citizenship` INTEGER NOT NULL, `family_name` TEXT NOT NULL, `parents_names` TEXT NOT NULL, `address` TEXT NOT NULL, `registered_address` TEXT NOT NULL, `correspondence_address` TEXT NOT NULL, `phone_number` TEXT NOT NULL, `cell_phone_number` TEXT NOT NULL, `email` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `first_guardian_full_name` TEXT, `first_guardian_kinship` TEXT, `first_guardian_address` TEXT, `first_guardian_phones` TEXT, `first_guardian_email` TEXT, `second_guardian_full_name` TEXT, `second_guardian_kinship` TEXT, `second_guardian_address` TEXT, `second_guardian_phones` TEXT, `second_guardian_email` TEXT)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "full_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "first_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondName", + "columnName": "second_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "surname", + "columnName": "surname", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "birthDate", + "columnName": "birth_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthPlace", + "columnName": "birth_place", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gender", + "columnName": "gender", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPolishCitizenship", + "columnName": "has_polish_citizenship", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "familyName", + "columnName": "family_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentsNames", + "columnName": "parents_names", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "registeredAddress", + "columnName": "registered_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "correspondenceAddress", + "columnName": "correspondence_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phoneNumber", + "columnName": "phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cellPhoneNumber", + "columnName": "cell_phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "firstGuardian.fullName", + "columnName": "first_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.kinship", + "columnName": "first_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.address", + "columnName": "first_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.phones", + "columnName": "first_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.email", + "columnName": "first_guardian_email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.fullName", + "columnName": "second_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.kinship", + "columnName": "second_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.address", + "columnName": "second_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.phones", + "columnName": "second_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.email", + "columnName": "second_guardian_email", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableHeaders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SchoolAnnouncements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_login_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `author` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `type` TEXT NOT NULL, `destination` TEXT NOT NULL DEFAULT '{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}', `date` INTEGER NOT NULL, `data` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "destination", + "columnName": "destination", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}'" + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AdminMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `version_name` INTEGER, `version_max` INTEGER, `target_register_host` TEXT, `target_flavor` TEXT, `destination_url` TEXT, `priority` TEXT NOT NULL, `types` TEXT NOT NULL DEFAULT '[]', `is_ok_visible` INTEGER NOT NULL DEFAULT 0, `is_x_visible` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "versionMin", + "columnName": "version_name", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMax", + "columnName": "version_max", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "targetRegisterHost", + "columnName": "target_register_host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "targetFlavor", + "columnName": "target_flavor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "destinationUrl", + "columnName": "destination_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "priority", + "columnName": "priority", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "types", + "columnName": "types", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'[]'" + }, + { + "fieldPath": "isOkVisible", + "columnName": "is_ok_visible", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isXVisible", + "columnName": "is_x_visible", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MutedMessageSenders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`author` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesDescriptive", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `description` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ee2464d218b254ca868667c0fc756c0b')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt index 208daf75..3189fc31 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt @@ -174,6 +174,7 @@ import javax.inject.Singleton AutoMigration(from = 58, to = 59), AutoMigration(from = 59, to = 60), AutoMigration(from = 60, to = 61), + AutoMigration(from = 61, to = 62), ], version = AppDatabase.VERSION_SCHEMA, exportSchema = true @@ -182,7 +183,7 @@ import javax.inject.Singleton abstract class AppDatabase : RoomDatabase() { companion object { - const val VERSION_SCHEMA = 61 + const val VERSION_SCHEMA = 62 fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( Migration2(), @@ -309,6 +310,6 @@ abstract class AppDatabase : RoomDatabase() { abstract val adminMessagesDao: AdminMessageDao abstract val mutedMessageSendersDao: MutedMessageSendersDao - + abstract val gradeDescriptiveDao: GradeDescriptiveDao } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt index d9326ff6..95abb399 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt @@ -9,6 +9,7 @@ import androidx.room.Transaction import androidx.room.Update import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.db.entities.StudentIsAuthorized import io.github.wulkanowy.data.db.entities.StudentName import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar import javax.inject.Singleton @@ -23,6 +24,9 @@ abstract class StudentDao { @Delete abstract suspend fun delete(student: Student) + @Update(entity = Student::class) + abstract suspend fun update(studentIsAuthorized: StudentIsAuthorized) + @Update(entity = Student::class) abstract suspend fun update(studentNickAndAvatar: StudentNickAndAvatar) diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Student.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Student.kt index e1116733..23e4439a 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Student.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Student.kt @@ -78,6 +78,13 @@ data class Student( @ColumnInfo(name = "registration_date") val registrationDate: Instant, + + @ColumnInfo(name = "is_authorized", defaultValue = "0") + val isAuthorized: Boolean, + + @ColumnInfo(name = "is_edu_one", defaultValue = "0") + val isEduOne: Boolean, + ) : Serializable { @PrimaryKey(autoGenerate = true) diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/StudentIsAuthorized.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/StudentIsAuthorized.kt new file mode 100644 index 00000000..a1290177 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/StudentIsAuthorized.kt @@ -0,0 +1,16 @@ +package io.github.wulkanowy.data.db.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import java.io.Serializable + +@Entity +data class StudentIsAuthorized( + @ColumnInfo(name = "is_authorized", defaultValue = "0") + val isAuthorized: Boolean, +) : Serializable { + + @PrimaryKey + var id: Long = 0 +} diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/RegisterUserMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/RegisterUserMapper.kt index 72c4861c..7e6a8166 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/RegisterUserMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/RegisterUserMapper.kt @@ -34,17 +34,19 @@ fun SdkRegisterUser.mapToPojo(password: String?) = RegisterUser( error = it.error, students = it.subjects .filterIsInstance() - .map { registerSubject -> + .map { registerStudent -> RegisterStudent( - studentId = registerSubject.studentId, - studentName = registerSubject.studentName, - studentSecondName = registerSubject.studentSecondName, - studentSurname = registerSubject.studentSurname, - className = registerSubject.className, - classId = registerSubject.classId, - isParent = registerSubject.isParent, - semesters = registerSubject.semesters - .mapToEntities(registerSubject.studentId), + studentId = registerStudent.studentId, + studentName = registerStudent.studentName, + studentSecondName = registerStudent.studentSecondName, + studentSurname = registerStudent.studentSurname, + className = registerStudent.className, + classId = registerStudent.classId, + isParent = registerStudent.isParent, + isAuthorized = registerStudent.isAuthorized, + isEduOne = registerStudent.isEduOne, + semesters = registerStudent.semesters + .mapToEntities(registerStudent.studentId), ) }, ) @@ -84,6 +86,8 @@ fun RegisterStudent.mapToStudentWithSemesters( password = user.password.orEmpty(), isCurrent = false, registrationDate = Instant.now(), + isAuthorized = this.isAuthorized, + isEduOne = this.isEduOne, ).apply { avatarColor = colors.random() }, diff --git a/app/src/main/java/io/github/wulkanowy/data/pojos/RegisterUser.kt b/app/src/main/java/io/github/wulkanowy/data/pojos/RegisterUser.kt index 98bf1402..dec6ebec 100644 --- a/app/src/main/java/io/github/wulkanowy/data/pojos/RegisterUser.kt +++ b/app/src/main/java/io/github/wulkanowy/data/pojos/RegisterUser.kt @@ -45,4 +45,6 @@ data class RegisterStudent( val classId: Int, val isParent: Boolean, val semesters: List, + val isAuthorized: Boolean, + val isEduOne: Boolean ) : java.io.Serializable diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt index e063840c..353e7ddd 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt @@ -6,6 +6,7 @@ import io.github.wulkanowy.data.db.dao.SemesterDao import io.github.wulkanowy.data.db.dao.StudentDao import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.db.entities.StudentIsAuthorized import io.github.wulkanowy.data.db.entities.StudentName import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar import io.github.wulkanowy.data.db.entities.StudentWithSemesters @@ -14,6 +15,7 @@ import io.github.wulkanowy.data.mappers.mapToPojo import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.DispatchersProvider +import io.github.wulkanowy.utils.getCurrentOrLast import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.security.Scrambler import io.github.wulkanowy.utils.switchSemester @@ -100,6 +102,25 @@ class StudentRepository @Inject constructor( return student } + suspend fun checkCurrentStudentAuthorizationStatus() { + val student = getCurrentStudent() + + if (!student.isAuthorized) { + val currentSemester = semesterDb.loadAll( + studentId = student.studentId, + classId = student.classId, + ).getCurrentOrLast() + val initializedSdk = sdk.init(student).switchSemester(currentSemester) + val isAuthorized = initializedSdk.getCurrentStudent()?.isAuthorized ?: false + + if (isAuthorized) { + studentDb.update(StudentIsAuthorized(isAuthorized = true).apply { + id = student.id + }) + } else throw NoAuthorizationException() + } + } + suspend fun getCurrentStudent(decryptPass: Boolean = true): Student { val student = studentDb.loadCurrent() ?: throw NoCurrentStudentException() @@ -176,3 +197,6 @@ class StudentRepository @Inject constructor( } } } + +class NoAuthorizationException : Exception() + diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt b/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt index 7109f1ff..00a2ab22 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt @@ -3,7 +3,7 @@ package io.github.wulkanowy.ui.base import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.data.exceptions.NoCurrentStudentException -import io.github.wulkanowy.sdk.scrapper.exception.AuthorizationRequiredException +import io.github.wulkanowy.data.repositories.NoAuthorizationException import io.github.wulkanowy.sdk.scrapper.exception.CloudflareVerificationException import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException @@ -40,7 +40,7 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co is ScramblerException -> onDecryptionFailed() is BadCredentialsException -> onExpiredCredentials() is NoCurrentStudentException -> onNoCurrentStudent() - is AuthorizationRequiredException -> onAuthorizationRequired() + is NoAuthorizationException -> onAuthorizationRequired() is CloudflareVerificationException -> onCaptchaVerificationRequired(error.originalUrl) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt index 5469fcad..67f729af 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt @@ -73,6 +73,7 @@ class MainPresenter @Inject constructor( syncManager.startPeriodicSyncWorker() checkAppSupport() + checkCurrentStudentAuthorizationStatus() analytics.logEvent("app_open", "destination" to initDestination.toString()) Timber.i("Main view was initialized with $initDestination") @@ -191,4 +192,13 @@ class MainPresenter @Inject constructor( view?.showStudentAvatar(currentStudent) } + + private fun checkCurrentStudentAuthorizationStatus() { + presenterScope.launch { + runCatching { studentRepository.checkCurrentStudentAuthorizationStatus() } + .onFailure { errorHandler.dispatch(it) } + + Timber.i("Current student authorization status checked") + } + } } diff --git a/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt index 9b6ca706..77f03fb8 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt @@ -12,6 +12,7 @@ fun Sdk.init(student: Student): Sdk { schoolSymbol = student.schoolSymbol studentId = student.studentId classId = student.classId + isEduOne = student.isEduOne emptyCookieJarInterceptor = true if (Sdk.Mode.valueOf(student.loginMode) == Sdk.Mode.HEBE) { diff --git a/app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt b/app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt index 9f5d731b..96b75c35 100644 --- a/app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt +++ b/app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt @@ -5,8 +5,8 @@ import io.github.wulkanowy.data.db.entities.MailboxType import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.sdk.Sdk -import java.time.LocalDate import java.time.Instant.now +import java.time.LocalDate import io.github.wulkanowy.sdk.pojo.Semester as SdkSemester fun getSemesterEntity(diaryId: Int = 1, semesterId: Int = 1, start: LocalDate = LocalDate.now(), end: LocalDate = LocalDate.now(), semesterName: Int = 1) = Semester( @@ -72,6 +72,8 @@ fun getStudentEntity(mode: Sdk.Mode = Sdk.Mode.HEBE) = Student( symbol = "", userLoginId = 1, userName = "", + isEduOne = false, + isAuthorized = false ).apply { id = 1 } diff --git a/app/src/test/java/io/github/wulkanowy/domain/GetMailboxByStudentUseCaseTest.kt b/app/src/test/java/io/github/wulkanowy/domain/GetMailboxByStudentUseCaseTest.kt index 34a8fe99..2a63c6b8 100644 --- a/app/src/test/java/io/github/wulkanowy/domain/GetMailboxByStudentUseCaseTest.kt +++ b/app/src/test/java/io/github/wulkanowy/domain/GetMailboxByStudentUseCaseTest.kt @@ -222,5 +222,7 @@ class GetMailboxByStudentUseCaseTest { symbol = "", userLoginId = 1, userName = userName, + isAuthorized = false, + isEduOne = false ) } diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt index 4f0f80fe..ad7bbe15 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt @@ -72,7 +72,9 @@ class GradeAverageProviderTest { className = "", classId = 1, isCurrent = true, - registrationDate = Instant.now() + registrationDate = Instant.now(), + isAuthorized = false, + isEduOne = false ) private val semesters = mutableListOf( diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt index 34965f00..d03337dc 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt @@ -71,6 +71,8 @@ class LoginStudentSelectPresenterTest { classId = 0, isParent = false, semesters = listOf(), + isEduOne = false, + isAuthorized = false, ) private val school = RegisterUnit( diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/main/MainPresenterTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/main/MainPresenterTest.kt index 460c8385..3e95774d 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/main/MainPresenterTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/main/MainPresenterTest.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.ui.modules.main +import io.github.wulkanowy.MainCoroutineRule import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.services.sync.SyncManager @@ -7,14 +8,23 @@ import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.utils.AdsHelper import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AppInfo -import io.mockk.* +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.clearMocks +import io.mockk.every import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.verify import kotlinx.serialization.json.Json import org.junit.Before +import org.junit.Rule import org.junit.Test class MainPresenterTest { + @get:Rule + val coroutineRule = MainCoroutineRule() + @MockK(relaxed = true) lateinit var errorHandler: ErrorHandler From 6047af9ff0c18b7e291cf2fe9ff551ae0d5eefea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Wed, 20 Mar 2024 02:18:00 +0100 Subject: [PATCH 43/43] Version 2.5.2 --- app/build.gradle | 8 ++++---- app/src/main/play/release-notes/pl-PL/default.txt | 12 ++++-------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 7ecfc818..e73784a5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,8 +27,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 34 - versionCode 150 - versionName "2.5.1" + versionCode 151 + versionName "2.5.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" @@ -165,7 +165,7 @@ play { track = 'production' releaseStatus = ReleaseStatus.IN_PROGRESS userFraction = 0.50d - updatePriority = 1 + updatePriority = 3 enabled.set(false) } @@ -195,7 +195,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.5.2-SNAPSHOT' + implementation 'io.github.wulkanowy:sdk:2.5.2' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index 2a57977f..6eafb6a9 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,11 +1,7 @@ -Wersja 2.5.1 +Wersja 2.5.2 -— dodaliśmy wyświetlanie ogłoszeń -— dodaliśmy opcję przywracania wiadomości z kosza -— 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 +— naprawiliśmy omyłkowe wyświetlanie ekranu z wymaganą autoryzacją numerem PESEL +— naprawiliśmy kilka problemów ze stabilnością +— poprawiliśmy wyświetlanie kolorów we frekwencji Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases