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

Merge branch 'release/1.8.0'

This commit is contained in:
Mikołaj Pich 2022-11-16 20:31:09 +01:00
commit 4bce35f810
80 changed files with 6771 additions and 555 deletions

View File

@ -1,10 +1,4 @@
[English version of README](README.en.md)
[Deutsche Version von README](README.de.md)
[Polska wersja README](README.md)
[Slovenská verzia README](README.sk.md)
Česká verze / [Deutsche Version](README.de.md) / [English version](README.en.md) / [Polska wersja](README.md) / [Slovenská verzia](README.sk.md)
# Wulkanowy
@ -13,6 +7,7 @@
[![Discord](https://img.shields.io/discord/390889354199040011.svg?style=flat-square)](https://discord.gg/vccAQBr)
[![F-Droid](https://img.shields.io/f-droid/v/io.github.wulkanowy.svg?style=flat-square)](https://f-droid.org/packages/io.github.wulkanowy/)
[![Last release](https://img.shields.io/github/release/wulkanowy/wulkanowy.svg?logo=github&style=flat-square)](https://github.com/wulkanowy/wulkanowy/releases)
[![Crowdin](https://badges.crowdin.net/wulkanowy2/localized.svg)](https://translate.wulkanowy.net.pl)
Neoficiální klient deníku VULCAN UONET+ pro žáka a rodiče
@ -57,7 +52,7 @@ Aktuální verzi si můžete stáhnout z Google Play, F-Droid nebo Huawei AppGal
Můžete si také stáhnout [vývojovou verzi](https://wulkanowy.github.io/#download), která zahrnuje nové funkce připravované pro příští vydání
## Postaveno s
## Postaveno s pomocí
* [Wulkanowy SDK](https://github.com/wulkanowy/sdk)

View File

@ -1,6 +1,4 @@
[Polska wersja README](README.md)
[English version of README](README.en.md)
[Česká verze](README.cs.md) / Deutsche Version / [English version](README.en.md) / [Polska wersja](README.md) / [Slovenská verzia](README.sk.md)
# Wulkanowy
@ -9,6 +7,7 @@
[![Discord](https://img.shields.io/discord/390889354199040011.svg?style=flat-square)](https://discord.gg/vccAQBr)
[![F-Droid](https://img.shields.io/f-droid/v/io.github.wulkanowy.svg?style=flat-square)](https://f-droid.org/packages/io.github.wulkanowy/)
[![Last release](https://img.shields.io/github/release/wulkanowy/wulkanowy.svg?logo=github&style=flat-square)](https://github.com/wulkanowy/wulkanowy/releases)
[![Crowdin](https://badges.crowdin.net/wulkanowy2/localized.svg)](https://translate.wulkanowy.net.pl)
Inoffizieller Android VULCAN UONET+ Registrierungsclient für Schüler und ihre Eltern

View File

@ -1,10 +1,4 @@
[Polska wersja README](README.md)
[Deutsche Version von README](README.de.md)
[Česká verze README](README.cs.md)
[Slovenská verzia README](README.sk.md)
[Česká verze](README.cs.md) / [Deutsche Version](README.de.md) / English version / [Polska wersja](README.md) / [Slovenská verzia](README.sk.md)
# Wulkanowy
@ -13,6 +7,7 @@
[![Discord](https://img.shields.io/discord/390889354199040011.svg?style=flat-square)](https://discord.gg/vccAQBr)
[![F-Droid](https://img.shields.io/f-droid/v/io.github.wulkanowy.svg?style=flat-square)](https://f-droid.org/packages/io.github.wulkanowy/)
[![Last release](https://img.shields.io/github/release/wulkanowy/wulkanowy.svg?logo=github&style=flat-square)](https://github.com/wulkanowy/wulkanowy/releases)
[![Crowdin](https://badges.crowdin.net/wulkanowy2/localized.svg)](https://translate.wulkanowy.net.pl)
Unofficial android VULCAN UONET+ register client for both students and their parents

View File

@ -1,10 +1,4 @@
[English version of README](README.en.md)
[Deutsche Version von README](README.de.md)
[Česká verze README](README.cs.md)
[Slovenská verzia README](README.sk.md)
[Česká verze](README.cs.md) / [Deutsche Version](README.de.md) / [English version](README.en.md) / Polska wersja / [Slovenská verzia](README.sk.md)
# Wulkanowy
@ -13,6 +7,7 @@
[![Discord](https://img.shields.io/discord/390889354199040011.svg?style=flat-square)](https://discord.gg/vccAQBr)
[![F-Droid](https://img.shields.io/f-droid/v/io.github.wulkanowy.svg?style=flat-square)](https://f-droid.org/packages/io.github.wulkanowy/)
[![Last release](https://img.shields.io/github/release/wulkanowy/wulkanowy.svg?logo=github&style=flat-square)](https://github.com/wulkanowy/wulkanowy/releases)
[![Crowdin](https://badges.crowdin.net/wulkanowy2/localized.svg)](https://translate.wulkanowy.net.pl)
Nieoficjalny klient dziennika VULCAN UONET+ dla ucznia i rodzica

View File

@ -1,10 +1,4 @@
[English version of README](README.en.md)
[Deutsche Version von README](README.de.md)
[Polska wersja README](README.md)
[Česká verze README](README.cs.md)
[Česká verze](README.cs.md) / [Deutsche Version](README.de.md) / [English version](README.en.md) / [Polska wersja](README.md) / Slovenská verzia
# Wulkanowy
@ -13,6 +7,7 @@
[![Discord](https://img.shields.io/discord/390889354199040011.svg?style=flat-square)](https://discord.gg/vccAQBr)
[![F-Droid](https://img.shields.io/f-droid/v/io.github.wulkanowy.svg?style=flat-square)](https://f-droid.org/packages/io.github.wulkanowy/)
[![Last release](https://img.shields.io/github/release/wulkanowy/wulkanowy.svg?logo=github&style=flat-square)](https://github.com/wulkanowy/wulkanowy/releases)
[![Crowdin](https://badges.crowdin.net/wulkanowy2/localized.svg)](https://translate.wulkanowy.net.pl)
Neoficiálny klient denníka VULCAN UONET+ pre žiaka a rodičov
@ -57,7 +52,7 @@ Aktuálnu verziu si môžete stiahnuť z Google Play, F-Droid alebo Huawei AppGa
Môžete si tiež stiahnuť [vývojovú verziu](https://wulkanowy.github.io/#download), ktorá zahrňuje nové funkcie pripravované pre budúce vydanie
## Postavené s
## Postavené s pomocou
* [Wulkanowy SDK](https://github.com/wulkanowy/sdk)

View File

@ -23,8 +23,8 @@ android {
testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21
targetSdkVersion 32
versionCode 114
versionName "1.7.5"
versionCode 115
versionName "1.8.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "app_name", "Wulkanowy"
@ -160,8 +160,8 @@ kapt {
play {
defaultToAppBundles = false
track = 'production'
// releaseStatus = com.github.triplet.gradle.androidpublisher.ReleaseStatus.IN_PROGRESS
// userFraction = 0.05d
releaseStatus = com.github.triplet.gradle.androidpublisher.ReleaseStatus.IN_PROGRESS
userFraction = 0.25d
updatePriority = 4
enabled.set(false)
}
@ -181,24 +181,24 @@ ext {
android_hilt = "1.0.0"
room = "2.4.3"
chucker = "3.5.2"
mockk = "1.12.7"
mockk = "1.13.2"
coroutines = "1.6.4"
}
dependencies {
implementation "io.github.wulkanowy:sdk:1.7.5"
implementation "io.github.wulkanowy:sdk:1.8.0"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.8'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.0"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
implementation "androidx.core:core-ktx:1.8.0"
implementation 'androidx.core:core-splashscreen:1.0.0'
implementation "androidx.activity:activity-ktx:1.5.1"
implementation "androidx.appcompat:appcompat:1.5.0"
implementation "androidx.fragment:fragment-ktx:1.5.2"
implementation "androidx.annotation:annotation:1.4.0"
implementation "androidx.appcompat:appcompat:1.5.1"
implementation "androidx.fragment:fragment-ktx:1.5.4"
implementation "androidx.annotation:annotation:1.5.0"
implementation "androidx.preference:preference-ktx:1.2.0"
implementation "androidx.recyclerview:recyclerview:1.2.1"
@ -206,10 +206,10 @@ dependencies {
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
implementation "com.google.android.material:material:1.6.1"
implementation "com.google.android.material:material:1.7.0"
implementation "com.github.wulkanowy:material-chips-input:2.3.1"
implementation "com.github.PhilJay:MPAndroidChart:v3.1.0"
implementation 'com.github.lopspower:CircularImageView:4.2.0'
implementation 'com.github.lopspower:CircularImageView:4.3.0'
implementation "androidx.work:work-runtime-ktx:$work_manager"
playImplementation "androidx.work:work-gcm:$work_manager"
@ -236,21 +236,21 @@ dependencies {
implementation "at.favre.lib:slf4j-timber:1.0.1"
implementation 'com.github.bastienpaulfr:Treessence:1.0.5'
implementation "com.mikepenz:aboutlibraries-core:$about_libraries"
implementation "io.coil-kt:coil:2.2.0"
implementation "io.coil-kt:coil:2.2.2"
implementation "io.github.wulkanowy:AppKillerManager:3.0.0"
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
implementation 'com.fredporciuncula:flow-preferences:1.8.0'
playImplementation platform('com.google.firebase:firebase-bom:30.3.2')
playImplementation platform('com.google.firebase:firebase-bom:31.0.3')
playImplementation 'com.google.firebase:firebase-analytics-ktx'
playImplementation 'com.google.firebase:firebase-messaging:'
playImplementation 'com.google.firebase:firebase-crashlytics:'
playImplementation 'com.google.android.play:core:1.10.3'
playImplementation 'com.google.android.play:core-ktx:1.8.1'
playImplementation 'com.google.android.gms:play-services-ads:21.1.0'
playImplementation 'com.google.android.gms:play-services-ads:21.3.0'
hmsImplementation 'com.huawei.hms:hianalytics:6.7.0.300'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.7.1.300'
hmsImplementation 'com.huawei.hms:hianalytics:6.8.0.300'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.7.3.300'
releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker"
@ -263,10 +263,10 @@ dependencies {
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines"
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
testImplementation 'org.robolectric:robolectric:4.8.2'
testImplementation "androidx.test:runner:1.4.0"
testImplementation "androidx.test.ext:junit:1.1.3"
testImplementation "androidx.test:core:1.4.0"
testImplementation 'org.robolectric:robolectric:4.9'
testImplementation "androidx.test:runner:1.5.1"
testImplementation "androidx.test.ext:junit:1.1.4"
testImplementation "androidx.test:core:1.5.0"
testImplementation "androidx.room:room-testing:$room"
testImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
kaptTest "com.google.dagger:hilt-android-compiler:$hilt_version"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -8,15 +8,7 @@ import javax.inject.Singleton
@Suppress("UNUSED_PARAMETER")
class AnalyticsHelper @Inject constructor() {
fun logEvent(name: String, vararg params: Pair<String, Any?>) {
// do nothing
}
fun setCurrentScreen(activity: Activity, name: String?) {
// do nothing
}
fun popCurrentScreen(name: String?) {
// do nothing
}
fun logEvent(name: String, vararg params: Pair<String, Any?>) = Unit
fun setCurrentScreen(activity: Activity, name: String?) = Unit
fun popCurrentScreen(name: String?) = Unit
}

View File

@ -3,26 +3,38 @@ package io.github.wulkanowy.utils
import android.app.Activity
import android.content.Context
import android.os.Bundle
import com.huawei.agconnect.crash.AGConnectCrash
import com.huawei.hms.analytics.HiAnalytics
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.repositories.PreferencesRepository
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AnalyticsHelper @Inject constructor(
@ApplicationContext private val context: Context
@ApplicationContext private val context: Context,
preferencesRepository: PreferencesRepository,
appInfo: AppInfo,
) {
private val analytics by lazy { HiAnalytics.getInstance(context) }
private val connectCrash by lazy { AGConnectCrash.getInstance() }
init {
if (!appInfo.isDebug) {
connectCrash.setUserId(preferencesRepository.installationId)
}
}
fun logEvent(name: String, vararg params: Pair<String, Any?>) {
Bundle().apply {
params.forEach {
if (it.second == null) return@forEach
when (it.second) {
is String, is String? -> putString(it.first, it.second as String)
is Int, is Int? -> putInt(it.first, it.second as Int)
is Boolean, is Boolean? -> putBoolean(it.first, it.second as Boolean)
params.forEach { (key, value) ->
if (value == null) return@forEach
when (value) {
is String -> putString(key, value)
is Int -> putInt(key, value)
is Boolean -> putBoolean(key, value)
}
}
analytics.onEvent(name, this)

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.utils
import android.util.Log
import com.huawei.agconnect.crash.AGConnectCrash
import fr.bipi.tressence.base.FormatterPriorityTree
import fr.bipi.tressence.common.StackTraceRecorder
class CrashLogTree : FormatterPriorityTree(Log.VERBOSE) {
@ -22,16 +23,10 @@ class CrashLogExceptionTree : FormatterPriorityTree(Log.ERROR, ExceptionFilter)
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
if (skipLog(priority, tag, message, t)) return
// Disabled due to a bug in the Huawei library
/*connectCrash.setCustomKey("priority", priority)
connectCrash.setCustomKey("tag", tag.orEmpty())
connectCrash.setCustomKey("message", message)
if (t != null) {
connectCrash.recordException(t)
} else {
connectCrash.recordException(StackTraceRecorder(format(priority, tag, message)))
}*/
}
}
}

View File

@ -47,6 +47,7 @@ import javax.inject.Singleton
AutoMigration(from = 44, to = 45),
AutoMigration(from = 46, to = 47),
AutoMigration(from = 47, to = 48),
AutoMigration(from = 51, to = 52),
],
version = AppDatabase.VERSION_SCHEMA,
exportSchema = true
@ -55,7 +56,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() {
companion object {
const val VERSION_SCHEMA = 51
const val VERSION_SCHEMA = 53
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(),
@ -105,6 +106,7 @@ abstract class AppDatabase : RoomDatabase() {
Migration49(),
Migration50(),
Migration51(),
Migration53(),
)
fun newInstance(

View File

@ -3,12 +3,16 @@ package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Query
import io.github.wulkanowy.data.db.entities.Mailbox
import kotlinx.coroutines.flow.Flow
import javax.inject.Singleton
@Singleton
@Dao
interface MailboxDao : BaseDao<Mailbox> {
@Query("SELECT * FROM Mailboxes WHERE userLoginId = :userLoginId ")
suspend fun loadAll(userLoginId: Int): List<Mailbox>
@Query("SELECT * FROM Mailboxes WHERE email = :email")
suspend fun loadAll(email: String): List<Mailbox>
@Query("SELECT * FROM Mailboxes WHERE email = :email AND symbol = :symbol AND schoolId = :schoolId")
fun loadAll(email: String, symbol: String, schoolId: String): Flow<List<Mailbox>>
}

View File

@ -16,4 +16,7 @@ interface MessagesDao : BaseDao<Message> {
@Query("SELECT * FROM Messages WHERE mailbox_key = :mailboxKey AND folder_id = :folder ORDER BY date DESC")
fun loadAll(mailboxKey: String, folder: Int): Flow<List<Message>>
@Query("SELECT * FROM Messages WHERE email = :email AND folder_id = :folder ORDER BY date DESC")
fun loadAll(folder: Int, email: String): Flow<List<Message>>
}

View File

@ -1,20 +1,27 @@
package io.github.wulkanowy.data.db.entities
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
@Parcelize
@Entity(tableName = "Mailboxes")
data class Mailbox(
@PrimaryKey
val globalKey: String,
val email: String,
val symbol: String,
val schoolId: String,
val fullName: String,
val userName: String,
val userLoginId: Int,
val studentName: String,
val schoolNameShort: String,
val type: MailboxType,
)
) : java.io.Serializable, Parcelable
enum class MailboxType {
STUDENT,

View File

@ -9,6 +9,9 @@ import java.time.Instant
@Entity(tableName = "Messages")
data class Message(
@ColumnInfo(name = "email")
val email: String,
@ColumnInfo(name = "message_global_key")
val messageGlobalKey: String,
@ -29,6 +32,12 @@ data class Message(
var unread: Boolean,
@ColumnInfo(name = "read_by")
val readBy: Int?,
@ColumnInfo(name = "unread_by")
val unreadBy: Int?,
@ColumnInfo(name = "has_attachments")
val hasAttachments: Boolean
) : Serializable {

View File

@ -0,0 +1,57 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration53 : Migration(52, 53) {
override fun migrate(database: SupportSQLiteDatabase) {
createMailboxTable(database)
recreateMessagesTable(database)
}
private fun createMailboxTable(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE IF EXISTS Mailboxes")
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `Mailboxes` (
`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`)
)""".trimIndent()
)
}
private fun recreateMessagesTable(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE IF EXISTS Messages")
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `Messages` (
`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
)""".trimIndent()
)
}
}

View File

@ -10,9 +10,11 @@ fun List<SdkMailbox>.mapToEntities(student: Student) = map {
globalKey = it.globalKey,
fullName = it.fullName,
userName = it.userName,
userLoginId = student.userLoginId,
studentName = it.studentName,
schoolNameShort = it.schoolNameShort,
type = MailboxType.valueOf(it.type.name),
email = student.email,
symbol = student.symbol,
schoolId = student.schoolSymbol,
)
}

View File

@ -6,17 +6,22 @@ import io.github.wulkanowy.sdk.pojo.Message as SdkMessage
import io.github.wulkanowy.sdk.pojo.MessageAttachment as SdkMessageAttachment
import io.github.wulkanowy.sdk.pojo.Recipient as SdkRecipient
fun List<SdkMessage>.mapToEntities(mailbox: Mailbox) = map {
fun List<SdkMessage>.mapToEntities(student: Student, mailbox: Mailbox?, allMailboxes: List<Mailbox>) = map {
Message(
messageGlobalKey = it.globalKey,
mailboxKey = mailbox.globalKey,
mailboxKey = mailbox?.globalKey ?: allMailboxes.find { box ->
box.fullName == it.mailbox
}?.globalKey!!,
email = student.email,
messageId = it.id,
correspondents = it.correspondents,
subject = it.subject.trim(),
date = it.dateZoned.toInstant(),
folderId = it.folderId,
unread = it.unread,
hasAttachments = it.hasAttachments
unreadBy = it.unreadBy,
readBy = it.readBy,
hasAttachments = it.hasAttachments,
).apply {
content = it.content.orEmpty()
}

View File

@ -1,85 +0,0 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.db.dao.MailboxDao
import io.github.wulkanowy.data.db.entities.Mailbox
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
@Singleton
class MailboxRepository @Inject constructor(
private val mailboxDao: MailboxDao,
private val sdk: Sdk,
private val refreshHelper: AutoRefreshHelper,
) {
private val cacheKey = "mailboxes"
suspend fun refreshMailboxes(student: Student) {
val new = sdk.init(student).getMailboxes().mapToEntities(student)
val old = mailboxDao.loadAll(student.userLoginId)
mailboxDao.deleteAll(old uniqueSubtract new)
mailboxDao.insertAll(new uniqueSubtract old)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
}
suspend fun getMailbox(student: Student): Mailbox {
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student))
val mailboxes = mailboxDao.loadAll(student.userLoginId)
val mailbox = mailboxes.filterByStudent(student)
return if (isExpired || mailbox == null) {
refreshMailboxes(student)
val newMailbox = mailboxDao.loadAll(student.userLoginId).filterByStudent(student)
requireNotNull(newMailbox) {
"Mailbox for ${student.userName} - ${student.studentName} not found! Saved mailboxes: $mailboxes"
}
newMailbox
} else mailbox
}
private fun List<Mailbox>.filterByStudent(student: Student): Mailbox? {
val normalizedStudentName = student.studentName.normalizeStudentName()
return find {
it.studentName.normalizeStudentName() == normalizedStudentName
} ?: singleOrNull {
it.studentName.getFirstAndLastPart() == normalizedStudentName.getFirstAndLastPart()
} ?: singleOrNull {
it.studentName.getUnauthorizedVersion() == normalizedStudentName
}
}
private fun String.normalizeStudentName(): String {
return trim().split(" ")
.filter { it.isNotBlank() }
.joinToString(" ") { part ->
part.lowercase().replaceFirstChar { it.uppercase() }
}
}
private fun String.getFirstAndLastPart(): String {
val parts = normalizeStudentName().split(" ")
val endParts = parts.filterIndexed { i, _ ->
i == 0 || parts.size - 1 == i
}
return endParts.joinToString(" ")
}
private fun String.getUnauthorizedVersion(): String {
return normalizeStudentName().split(" ")
.joinToString(" ") {
it.first() + "*".repeat(it.length - 1)
}
}
}

View File

@ -5,6 +5,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.db.dao.MailboxDao
import io.github.wulkanowy.data.db.dao.MessageAttachmentDao
import io.github.wulkanowy.data.db.dao.MessagesDao
import io.github.wulkanowy.data.db.entities.*
@ -15,6 +16,8 @@ import io.github.wulkanowy.data.mappers.mapFromEntities
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.data.pojos.MessageDraft
import io.github.wulkanowy.data.toFirstResult
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
@ -40,16 +43,18 @@ class MessageRepository @Inject constructor(
private val refreshHelper: AutoRefreshHelper,
private val sharedPrefProvider: SharedPrefProvider,
private val json: Json,
private val mailboxDao: MailboxDao,
private val getMailboxByStudentUseCase: GetMailboxByStudentUseCase,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "message"
private val messagesCacheKey = "message"
private val mailboxCacheKey = "mailboxes"
@Suppress("UNUSED_PARAMETER")
fun getMessages(
student: Student,
mailbox: Mailbox,
mailbox: Mailbox?,
folder: MessageFolder,
forceRefresh: Boolean,
notify: Boolean = false,
@ -58,16 +63,20 @@ class MessageRepository @Inject constructor(
isResultEmpty = { it.isEmpty() },
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(
key = getRefreshKey(cacheKey, student, folder)
key = getRefreshKey(messagesCacheKey, mailbox, folder)
)
it.isEmpty() || forceRefresh || isExpired
},
query = { messagesDb.loadAll(mailbox.globalKey, folder.id) },
query = {
if (mailbox == null) {
messagesDb.loadAll(folder.id, student.email)
} else messagesDb.loadAll(mailbox.globalKey, folder.id)
},
fetch = {
sdk.init(student).getMessages(
folder = Folder.valueOf(folder.name),
mailboxKey = mailbox.globalKey,
).mapToEntities(mailbox)
mailboxKey = mailbox?.globalKey,
).mapToEntities(student, mailbox, mailboxDao.loadAll(student.email))
},
saveFetchResult = { old, new ->
messagesDb.deleteAll(old uniqueSubtract new)
@ -75,7 +84,9 @@ class MessageRepository @Inject constructor(
it.isNotified = !notify
})
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student, folder))
refreshHelper.updateLastRefreshTimestamp(
getRefreshKey(messagesCacheKey, mailbox, folder)
)
}
)
@ -90,7 +101,9 @@ class MessageRepository @Inject constructor(
Timber.d("Message content in db empty: ${it.message.content.isBlank()}")
it.message.unread || it.message.content.isBlank()
},
query = { messagesDb.loadMessageWithAttachment(message.messageGlobalKey) },
query = {
messagesDb.loadMessageWithAttachment(message.messageGlobalKey)
},
fetch = {
sdk.init(student).getMessageDetails(it!!.message.messageGlobalKey, markAsRead)
},
@ -113,8 +126,10 @@ class MessageRepository @Inject constructor(
}
)
fun getMessagesFromDatabase(mailbox: Mailbox): Flow<List<Message>> {
return messagesDb.loadAll(mailbox.globalKey, RECEIVED.id)
fun getMessagesFromDatabase(student: Student, mailbox: Mailbox?): Flow<List<Message>> {
return if (mailbox == null) {
messagesDb.loadAll(RECEIVED.id, student.email)
} else messagesDb.loadAll(mailbox.globalKey, RECEIVED.id)
}
suspend fun updateMessages(messages: List<Message>) {
@ -136,7 +151,7 @@ class MessageRepository @Inject constructor(
)
}
suspend fun deleteMessages(student: Student, mailbox: Mailbox, messages: List<Message>) {
suspend fun deleteMessages(student: Student, mailbox: Mailbox?, messages: List<Message>) {
val firstMessage = messages.first()
sdk.init(student).deleteMessages(
messages = messages.map { it.messageGlobalKey },
@ -169,6 +184,34 @@ class MessageRepository @Inject constructor(
deleteMessages(student, mailbox, listOf(message))
}
suspend fun getMailboxes(student: Student, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
isResultEmpty = { it.isEmpty() },
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(
key = getRefreshKey(mailboxCacheKey, student),
)
it.isEmpty() || isExpired || forceRefresh
},
query = { mailboxDao.loadAll(student.email, student.symbol, student.schoolSymbol) },
fetch = { sdk.init(student).getMailboxes().mapToEntities(student) },
saveFetchResult = { old, new ->
mailboxDao.deleteAll(old uniqueSubtract new)
mailboxDao.insertAll(new uniqueSubtract old)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(mailboxCacheKey, student))
}
)
suspend fun getMailboxByStudent(student: Student): Mailbox? {
val mailbox = getMailboxByStudentUseCase(student)
return if (mailbox == null) {
getMailboxes(student, forceRefresh = true).toFirstResult()
getMailboxByStudentUseCase(student)
} else mailbox
}
var draftMessage: MessageDraft?
get() = sharedPrefProvider.getString(context.getString(R.string.pref_key_message_draft))
?.let { json.decodeFromString(it) }

View File

@ -10,17 +10,16 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.enums.*
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import io.github.wulkanowy.ui.modules.grade.GradeAverageMode
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.time.Instant
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
@OptIn(ExperimentalCoroutinesApi::class)
@Singleton
class PreferencesRepository @Inject constructor(
@ApplicationContext val context: Context,
@ -316,6 +315,16 @@ class PreferencesRepository @Inject constructor(
putBoolean(context.getString(R.string.pref_key_ads_enabled), value)
}
var installationId: String
get() = sharedPref.getString(PREF_KEY_INSTALLATION_ID, null).orEmpty()
private set(value) = sharedPref.edit { putString(PREF_KEY_INSTALLATION_ID, value) }
init {
if (installationId.isEmpty()) {
installationId = UUID.randomUUID().toString()
}
}
private fun getLong(id: Int, default: Int) = getLong(context.getString(id), default)
private fun getLong(id: String, default: Int) =
@ -331,23 +340,14 @@ class PreferencesRepository @Inject constructor(
private fun getBoolean(id: String, default: Int) =
sharedPref.getBoolean(id, context.resources.getBoolean(default))
private fun getBoolean(id: Int, default: Boolean) =
sharedPref.getBoolean(context.getString(id), default)
private companion object {
private const val PREF_KEY_INSTALLATION_ID = "installation_id"
private const val PREF_KEY_DASHBOARD_ITEMS_POSITION = "dashboard_items_position"
private const val PREF_KEY_IN_APP_REVIEW_COUNT = "in_app_review_count"
private const val PREF_KEY_IN_APP_REVIEW_DATE = "in_app_review_date"
private const val PREF_KEY_IN_APP_REVIEW_DONE = "in_app_review_done"
private const val PREF_KEY_APP_SUPPORT_SHOWN = "app_support_shown"
private const val PREF_KEY_PERSONALIZED_ADS_ENABLED = "personalized_ads_enabled"
private const val PREF_KEY_ADMIN_DISMISSED_MESSAGE_IDS = "admin_message_dismissed_ids"
}
}

View File

@ -33,9 +33,11 @@ class RecipientRepository @Inject constructor(
suspend fun getRecipients(
student: Student,
mailbox: Mailbox,
type: MailboxType
mailbox: Mailbox?,
type: MailboxType,
): List<Recipient> {
mailbox ?: return emptyList()
val cached = recipientDb.loadAll(type, mailbox.globalKey)
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student))
@ -47,11 +49,15 @@ class RecipientRepository @Inject constructor(
suspend fun getMessageSender(
student: Student,
mailbox: Mailbox,
message: Message
): List<Recipient> = sdk.init(student)
.getMessageReplayDetails(message.messageGlobalKey)
.sender
.let(::listOf)
.mapToEntities(mailbox.globalKey)
mailbox: Mailbox?,
message: Message,
): List<Recipient> {
mailbox ?: return emptyList()
return sdk.init(student)
.getMessageReplayDetails(message.messageGlobalKey)
.sender
.let(::listOf)
.mapToEntities(mailbox.globalKey)
}
}

View File

@ -0,0 +1,52 @@
package io.github.wulkanowy.domain.messages
import io.github.wulkanowy.data.db.dao.MailboxDao
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Student
import javax.inject.Inject
class GetMailboxByStudentUseCase @Inject constructor(
private val mailboxDao: MailboxDao,
) {
suspend operator fun invoke(student: Student): Mailbox? {
return mailboxDao.loadAll(student.email)
.filterByStudent(student)
}
private fun List<Mailbox>.filterByStudent(student: Student): Mailbox? {
val normalizedStudentName = student.studentName.normalizeStudentName()
return find {
it.studentName.normalizeStudentName() == normalizedStudentName
} ?: singleOrNull {
it.studentName.getFirstAndLastPart() == normalizedStudentName.getFirstAndLastPart()
} ?: singleOrNull {
it.studentName.getUnauthorizedVersion() == normalizedStudentName
}
}
private fun String.normalizeStudentName(): String {
return trim().split(" ")
.filter { it.isNotBlank() }
.joinToString(" ") { part ->
part.lowercase().replaceFirstChar { it.uppercase() }
}
}
private fun String.getFirstAndLastPart(): String {
val parts = normalizeStudentName().split(" ")
val endParts = parts.filterIndexed { i, _ ->
i == 0 || parts.size - 1 == i
}
return endParts.joinToString(" ")
}
private fun String.getUnauthorizedVersion(): String {
return normalizeStudentName().split(" ")
.joinToString(" ") {
it.first() + "*".repeat(it.length - 1)
}
}
}

View File

@ -8,7 +8,6 @@ import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.GroupNotificationData
import io.github.wulkanowy.data.pojos.NotificationData
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.splash.SplashActivity
import io.github.wulkanowy.utils.getPlural
import javax.inject.Inject

View File

@ -3,7 +3,6 @@ package io.github.wulkanowy.services.sync.works
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED
import io.github.wulkanowy.data.repositories.MailboxRepository
import io.github.wulkanowy.data.repositories.MessageRepository
import io.github.wulkanowy.data.waitForResult
import io.github.wulkanowy.services.sync.notifications.NewMessageNotification
@ -12,12 +11,11 @@ import javax.inject.Inject
class MessageWork @Inject constructor(
private val messageRepository: MessageRepository,
private val mailboxRepository: MailboxRepository,
private val newMessageNotification: NewMessageNotification,
) : Work {
override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) {
val mailbox = mailboxRepository.getMailbox(student)
val mailbox = messageRepository.getMailboxByStudent(student)
messageRepository.getMessages(
student = student,
mailbox = mailbox,
@ -26,7 +24,7 @@ class MessageWork @Inject constructor(
notify = notify
).waitForResult()
messageRepository.getMessagesFromDatabase(mailbox).first()
messageRepository.getMessagesFromDatabase(student, mailbox).first()
.filter { !it.isNotified && it.unread }.let {
if (it.isNotEmpty()) newMessageNotification.notify(it, student)
messageRepository.updateMessages(it.onEach { message -> message.isNotified = true })

View File

@ -1,22 +1,23 @@
package io.github.wulkanowy.services.sync.works
import io.github.wulkanowy.data.dataOrNull
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.data.repositories.MailboxRepository
import io.github.wulkanowy.data.repositories.MessageRepository
import io.github.wulkanowy.data.repositories.RecipientRepository
import io.github.wulkanowy.data.toFirstResult
import javax.inject.Inject
class RecipientWork @Inject constructor(
private val mailboxRepository: MailboxRepository,
private val messageRepository: MessageRepository,
private val recipientRepository: RecipientRepository
) : Work {
override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) {
mailboxRepository.refreshMailboxes(student)
val mailbox = mailboxRepository.getMailbox(student)
recipientRepository.refreshRecipients(student, mailbox, MailboxType.EMPLOYEE)
val mailboxes = messageRepository.getMailboxes(student, forceRefresh = true).toFirstResult()
mailboxes.dataOrNull?.forEach {
recipientRepository.refreshRecipients(student, it, MailboxType.EMPLOYEE)
}
}
}

View File

@ -4,7 +4,6 @@ import android.app.Dialog
import android.content.ClipData
import android.content.ClipboardManager
import android.os.Bundle
import android.view.LayoutInflater
import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
import androidx.appcompat.app.AlertDialog
@ -15,6 +14,7 @@ import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.DialogErrorBinding
import io.github.wulkanowy.utils.*
import javax.inject.Inject
@ -25,6 +25,9 @@ class ErrorDialog : DialogFragment() {
@Inject
lateinit var appInfo: AppInfo
@Inject
lateinit var preferencesRepository: PreferencesRepository
companion object {
private const val ARGUMENT_KEY = "error"
@ -36,7 +39,7 @@ class ErrorDialog : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val error = requireArguments().getSerializable(ARGUMENT_KEY) as Throwable
val binding = DialogErrorBinding.inflate(LayoutInflater.from(context))
val binding = DialogErrorBinding.inflate(layoutInflater)
binding.bindErrorDetails(error)
return getAlertDialog(binding, error).apply {
@ -99,7 +102,8 @@ class ErrorDialog : DialogFragment() {
R.string.about_feedback_template,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
"${appInfo.versionName}-${appInfo.buildFlavor}"
"${appInfo.versionName}-${appInfo.buildFlavor}",
preferencesRepository.installationId,
) + "\n" + content,
onActivityNotFound = {
requireContext().openInternetBrowser(

View File

@ -6,6 +6,7 @@ import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.FragmentAboutBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.about.contributor.ContributorFragment
@ -30,6 +31,9 @@ class AboutFragment : BaseFragment<FragmentAboutBinding>(R.layout.fragment_about
@Inject
lateinit var appInfo: AppInfo
@Inject
lateinit var preferencesRepository: PreferencesRepository
override val versionRes: Triple<String, String, Drawable?>?
get() = context?.run {
val buildTimestamp =
@ -185,7 +189,8 @@ class AboutFragment : BaseFragment<FragmentAboutBinding>(R.layout.fragment_about
R.string.about_feedback_template,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
"${appInfo.versionName}-${appInfo.buildFlavor}"
"${appInfo.versionName}-${appInfo.buildFlavor}",
preferencesRepository.installationId,
),
onActivityNotFound = {
requireContext().openInternetBrowser(

View File

@ -25,7 +25,6 @@ class DashboardPresenter @Inject constructor(
private val gradeRepository: GradeRepository,
private val semesterRepository: SemesterRepository,
private val messageRepository: MessageRepository,
private val mailboxRepository: MailboxRepository,
private val attendanceSummaryRepository: AttendanceSummaryRepository,
private val timetableRepository: TimetableRepository,
private val homeworkRepository: HomeworkRepository,
@ -228,7 +227,7 @@ class DashboardPresenter @Inject constructor(
private fun loadHorizontalGroup(student: Student, forceRefresh: Boolean) {
flow {
val semester = semesterRepository.getCurrentSemester(student)
val mailbox = mailboxRepository.getMailbox(student)
val mailbox = messageRepository.getMailboxByStudent(student)
val selectedTiles = preferencesRepository.selectedDashboardTiles
val flowSuccess = flowOf(Resource.Success(null))

View File

@ -19,9 +19,12 @@ val debugMessageItems = listOf(
private fun generateMessage(sender: String, subject: String) = Message(
subject = subject,
messageId = 123,
email = "",
date = Instant.now(),
folderId = 0,
unread = true,
readBy = 2,
unreadBy = 2,
hasAttachments = false,
messageGlobalKey = "",
correspondents = sender,

View File

@ -10,6 +10,7 @@ import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.ItemGradeSummaryBinding
import io.github.wulkanowy.databinding.ScrollableHeaderGradeSummaryBinding
import io.github.wulkanowy.sdk.scrapper.grades.isGradeValid
import io.github.wulkanowy.utils.calcFinalAverage
import java.util.Locale
import javax.inject.Inject
@ -61,7 +62,7 @@ class GradeSummaryAdapter @Inject constructor(
if (items.isEmpty()) return
val context = binding.root.context
val finalItemsCount = items.count { it.finalGrade.matches("[0-6][+-]?".toRegex()) }
val finalItemsCount = items.count { isGradeValid(it.finalGrade) }
val calculatedItemsCount = items.count { value -> value.average != 0.0 }
val allItemsCount = items.count { !it.subject.equals("zachowanie", true) }
val finalAverage = items.calcFinalAverage(

View File

@ -10,6 +10,7 @@ import androidx.core.widget.doOnTextChanged
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.FragmentLoginFormBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.login.LoginActivity
@ -32,6 +33,9 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
@Inject
lateinit var appInfo: AppInfo
@Inject
lateinit var preferencesRepository: PreferencesRepository
companion object {
fun newInstance() = LoginFormFragment()
}
@ -260,8 +264,9 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
R.string.login_email_text,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
appInfo.versionName,
"${appInfo.versionName}-${appInfo.buildFlavor}",
"$formHostValue/$formHostSymbol",
preferencesRepository.installationId,
lastError
)
)

View File

@ -9,6 +9,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.FragmentLoginStudentSelectBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.login.LoginActivity
@ -32,6 +33,9 @@ class LoginStudentSelectFragment :
@Inject
lateinit var appInfo: AppInfo
@Inject
lateinit var preferencesRepository: PreferencesRepository
companion object {
const val ARG_STUDENTS = "STUDENTS"
@ -111,10 +115,12 @@ class LoginStudentSelectFragment :
email = "wulkanowyinc@gmail.com",
subject = requireContext().getString(R.string.login_email_subject),
body = requireContext().getString(
R.string.login_email_text, appInfo.systemModel,
R.string.login_email_text,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
appInfo.versionName,
"${appInfo.versionName}-${appInfo.buildFlavor}",
"Select users to log in",
preferencesRepository.installationId,
lastError
)
)

View File

@ -13,6 +13,7 @@ import androidx.core.widget.doOnTextChanged
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.FragmentLoginSymbolBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.login.LoginActivity
@ -34,6 +35,9 @@ class LoginSymbolFragment :
@Inject
lateinit var appInfo: AppInfo
@Inject
lateinit var preferencesRepository: PreferencesRepository
companion object {
private const val SAVED_LOGIN_DATA = "LOGIN_DATA"
@ -159,8 +163,9 @@ class LoginSymbolFragment :
R.string.login_email_text,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
appInfo.versionName,
"${appInfo.versionName}-${appInfo.buildFlavor}",
"$host/${binding.loginSymbolName.text}",
preferencesRepository.installationId,
lastError
)
)

View File

@ -0,0 +1,81 @@
package io.github.wulkanowy.ui.modules.message.mailboxchooser
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil.ItemCallback
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.MailboxType
import io.github.wulkanowy.databinding.ItemMailboxChooserBinding
import javax.inject.Inject
class MailboxChooserAdapter @Inject constructor() :
ListAdapter<MailboxChooserItem, MailboxChooserAdapter.ItemViewHolder>(Differ) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder(
ItemMailboxChooserBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
)
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
holder.bind(getItem(position))
}
class ItemViewHolder(
private val binding: ItemMailboxChooserBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: MailboxChooserItem) {
with(binding) {
mailboxItemName.text = item.mailbox?.getFirstLine()
?: root.resources.getString(R.string.message_chip_all_mailboxes)
mailboxItemSchool.text = item.mailbox?.getSecondLine()
mailboxItemSchool.isVisible = !item.isAll
root.setOnClickListener { item.onClickListener(item.mailbox) }
}
}
private fun Mailbox.getFirstLine() = buildString {
if (studentName.isNotBlank() && studentName != userName) {
append(studentName)
append(" - ")
}
append(userName)
}
private fun Mailbox.getSecondLine() = buildString {
append(schoolNameShort)
append(" - ")
append(getMailboxType(type))
}
private fun getMailboxType(type: MailboxType): String = when (type) {
MailboxType.STUDENT -> R.string.message_mailbox_type_student
MailboxType.PARENT -> R.string.message_mailbox_type_parent
MailboxType.GUARDIAN -> R.string.message_mailbox_type_guardian
MailboxType.EMPLOYEE -> R.string.message_mailbox_type_employee
MailboxType.UNKNOWN -> null
}.let { it?.let { it1 -> binding.root.resources.getString(it1) }.orEmpty() }
}
private object Differ : ItemCallback<MailboxChooserItem>() {
override fun areItemsTheSame(
oldItem: MailboxChooserItem,
newItem: MailboxChooserItem
): Boolean {
return oldItem.mailbox?.globalKey == newItem.mailbox?.globalKey
}
override fun areContentsTheSame(
oldItem: MailboxChooserItem,
newItem: MailboxChooserItem
): Boolean {
return oldItem == newItem
}
}
}

View File

@ -0,0 +1,75 @@
package io.github.wulkanowy.ui.modules.message.mailboxchooser
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.setFragmentResult
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.databinding.DialogMailboxChooserBinding
import io.github.wulkanowy.ui.base.BaseDialogFragment
import javax.inject.Inject
@AndroidEntryPoint
class MailboxChooserDialog : BaseDialogFragment<DialogMailboxChooserBinding>(), MailboxChooserView {
@Inject
lateinit var presenter: MailboxChooserPresenter
@Inject
lateinit var mailboxAdapter: MailboxChooserAdapter
companion object {
const val LISTENER_KEY = "mailbox_selected"
const val MAILBOX_KEY = "selected_mailbox"
const val REQUIRED_KEY = "is_mailbox_required"
fun newInstance(mailboxes: List<Mailbox>, isMailboxRequired: Boolean, folder: String) =
MailboxChooserDialog().apply {
arguments = bundleOf(
MAILBOX_KEY to mailboxes.toTypedArray(),
REQUIRED_KEY to isMailboxRequired,
LISTENER_KEY to folder,
)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = DialogMailboxChooserBinding.inflate(inflater).apply { binding = this }.root
@Suppress("UNCHECKED_CAST")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
presenter.onAttachView(
view = this,
requireMailbox = requireArguments().getBoolean(REQUIRED_KEY, false),
mailboxes = requireArguments().getParcelableArray(MAILBOX_KEY).orEmpty()
.toList() as List<Mailbox>,
)
}
override fun initView() {
binding.accountQuickDialogRecycler.adapter = mailboxAdapter
}
override fun submitData(items: List<MailboxChooserItem>) {
mailboxAdapter.submitList(items)
}
override fun onMailboxSelected(item: Mailbox?) {
setFragmentResult(
requestKey = requireArguments().getString(LISTENER_KEY).orEmpty(),
result = bundleOf(MAILBOX_KEY to item),
)
dismiss()
}
}

View File

@ -0,0 +1,9 @@
package io.github.wulkanowy.ui.modules.message.mailboxchooser
import io.github.wulkanowy.data.db.entities.Mailbox
data class MailboxChooserItem(
val mailbox: Mailbox? = null,
val isAll: Boolean = false,
val onClickListener: (Mailbox?) -> Unit,
)

View File

@ -0,0 +1,38 @@
package io.github.wulkanowy.ui.modules.message.mailboxchooser
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import timber.log.Timber
import javax.inject.Inject
class MailboxChooserPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository
) : BasePresenter<MailboxChooserView>(errorHandler, studentRepository) {
fun onAttachView(view: MailboxChooserView, mailboxes: List<Mailbox>, requireMailbox: Boolean) {
super.onAttachView(view)
view.initView()
Timber.i("Mailbox chooser view was initialized")
view.submitData(getMailboxItems(mailboxes, requireMailbox))
}
private fun getMailboxItems(
mailboxes: List<Mailbox>,
requireMailbox: Boolean,
): List<MailboxChooserItem> = buildList {
if (!requireMailbox) {
add(MailboxChooserItem(isAll = true, onClickListener = ::onMailboxSelect))
}
addAll(mailboxes.map {
MailboxChooserItem(mailbox = it, isAll = false, onClickListener = ::onMailboxSelect)
})
}
fun onMailboxSelect(item: Mailbox?) {
view?.onMailboxSelected(item)
}
}

View File

@ -0,0 +1,13 @@
package io.github.wulkanowy.ui.modules.message.mailboxchooser
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.ui.base.BaseView
interface MailboxChooserView : BaseView {
fun initView()
fun submitData(items: List<MailboxChooserItem>)
fun onMailboxSelected(item: Mailbox?)
}

View File

@ -1,6 +1,5 @@
package io.github.wulkanowy.ui.modules.message.preview
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -74,15 +73,20 @@ class MessagePreviewAdapter @Inject constructor() :
}
}
@SuppressLint("SetTextI18n")
private fun bindMessage(holder: MessageViewHolder, message: Message) {
val context = holder.binding.root.context
val recipientCount = (message.unreadBy ?: 0) + (message.readBy ?: 0)
val isReceived = message.unreadBy == null
val readTextValue = when {
!message.unread -> R.string.all_yes
else -> R.string.all_no
val readText = when {
recipientCount > 1 -> {
context.getString(R.string.message_read_by, message.readBy, recipientCount)
}
message.readBy == 1 || (isReceived && !message.unread) -> {
context.getString(R.string.message_read, context.getString(R.string.all_yes))
}
else -> context.getString(R.string.message_read, context.getString(R.string.all_no))
}
val readText = context.getString(R.string.message_read, context.getString(readTextValue))
with(holder.binding) {
messagePreviewSubject.text = message.subject.ifBlank {

View File

@ -6,7 +6,6 @@ import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageAttachment
import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.repositories.MailboxRepository
import io.github.wulkanowy.data.repositories.MessageRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
@ -21,7 +20,6 @@ class MessagePreviewPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val messageRepository: MessageRepository,
private val mailboxRepository: MailboxRepository,
private val analytics: AnalyticsHelper
) : BasePresenter<MessagePreviewView>(errorHandler, studentRepository) {
@ -187,8 +185,8 @@ class MessagePreviewPresenter @Inject constructor(
presenterScope.launch {
runCatching {
val student = studentRepository.getCurrentStudent(decryptPass = true)
val mailbox = mailboxRepository.getMailbox(student)
messageRepository.deleteMessage(student, mailbox, message!!)
val mailbox = messageRepository.getMailboxByStudent(student)
messageRepository.deleteMessage(student, mailbox!!, message!!)
}
.onFailure {
retryCallback = { onMessageDelete() }

View File

@ -19,9 +19,13 @@ import androidx.core.text.toHtml
import androidx.core.widget.doOnTextChanged
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.databinding.ActivitySendMessageBinding
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog
import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog.Companion.MAILBOX_KEY
import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog.Companion.LISTENER_KEY
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.hideSoftInput
import io.github.wulkanowy.utils.showSoftInput
@ -100,6 +104,7 @@ class SendMessageActivity : BaseActivity<SendMessagePresenter, ActivitySendMessa
formSubjectValue = binding.sendMessageSubject.text.toString()
formContentValue =
binding.sendMessageMessageContent.text.toString().parseAsHtml().toString()
binding.sendMessageFrom.setOnClickListener { presenter.onOpenMailboxChooser() }
presenter.onAttachView(
view = this,
@ -107,6 +112,9 @@ class SendMessageActivity : BaseActivity<SendMessagePresenter, ActivitySendMessa
message = intent.getSerializableExtra(EXTRA_MESSAGE) as? Message,
reply = intent.getSerializableExtra(EXTRA_REPLY) as? Boolean
)
supportFragmentManager.setFragmentResultListener(LISTENER_KEY, this) { _, bundle ->
presenter.onMailboxSelected(bundle.getSerializable(MAILBOX_KEY) as? Mailbox)
}
}
@SuppressLint("ClickableViewAccessibility")
@ -205,6 +213,14 @@ class SendMessageActivity : BaseActivity<SendMessagePresenter, ActivitySendMessa
}
}
override fun showMailboxChooser(mailboxes: List<Mailbox>) {
MailboxChooserDialog.newInstance(
mailboxes = mailboxes,
isMailboxRequired = true,
folder = LISTENER_KEY,
).show(supportFragmentManager, "chooser")
}
override fun popView() {
finish()
}

View File

@ -1,15 +1,15 @@
package io.github.wulkanowy.ui.modules.message.send
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.*
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.logResourceStatus
import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.pojos.MessageDraft
import io.github.wulkanowy.data.repositories.*
import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.data.repositories.MessageRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.RecipientRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AnalyticsHelper
@ -28,7 +28,6 @@ class SendMessagePresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val messageRepository: MessageRepository,
private val mailboxRepository: MailboxRepository,
private val recipientRepository: RecipientRepository,
private val preferencesRepository: PreferencesRepository,
private val analytics: AnalyticsHelper
@ -36,10 +35,19 @@ class SendMessagePresenter @Inject constructor(
private val messageUpdateChannel = Channel<Unit>()
private var message: Message? = null
private var isReplay: Boolean? = null
private var mailboxes: List<Mailbox> = emptyList()
private var selectedMailbox: Mailbox? = null
fun onAttachView(view: SendMessageView, reason: String?, message: Message?, reply: Boolean?) {
super.onAttachView(view)
view.initView()
initializeSubjectStream()
this.message = message
this.isReplay = reply
Timber.i("Send message view was initialized")
loadData(message, reply)
with(view) {
@ -110,16 +118,31 @@ class SendMessagePresenter @Inject constructor(
return false
}
fun onOpenMailboxChooser() {
view?.showMailboxChooser(mailboxes)
}
fun onMailboxSelected(mailbox: Mailbox?) {
selectedMailbox = mailbox
loadData(message, isReplay)
}
private fun loadData(message: Message?, reply: Boolean?) {
resourceFlow {
val student = studentRepository.getCurrentStudent()
val mailbox = mailboxRepository.getMailbox(student)
if (selectedMailbox == null && mailboxes.isEmpty()) {
selectedMailbox = messageRepository.getMailboxByStudent(student)
mailboxes = messageRepository.getMailboxes(student, false).toFirstResult()
.dataOrNull.orEmpty()
}
Timber.i("Loading recipients started")
val recipients = createChips(
recipients = recipientRepository.getRecipients(
student = student,
mailbox = mailbox,
mailbox = selectedMailbox,
type = MailboxType.EMPLOYEE,
)
)
@ -130,7 +153,7 @@ class SendMessagePresenter @Inject constructor(
message != null && reply == true -> recipientRepository.getMessageSender(
student = student,
message = message,
mailbox = mailbox,
mailbox = selectedMailbox,
)
else -> emptyList()
}.let { createChips(it) }
@ -139,39 +162,42 @@ class SendMessagePresenter @Inject constructor(
messageRecipients.size
)
Triple(mailbox, recipients, messageRecipients)
recipients to messageRecipients
}
.logResourceStatus("load recipients")
.onEach {
when (it) {
is Resource.Loading -> view?.run {
showProgress(true)
showContent(false)
}
is Resource.Success -> it.data.let { (mailbox, recipientChips, selectedRecipientChips) ->
view?.run {
setMailbox(getMailboxName(mailbox))
setRecipients(recipientChips)
if (selectedRecipientChips.isNotEmpty()) setSelectedRecipients(
selectedRecipientChips
)
showContent(true)
}
}
is Resource.Error -> {
view?.showContent(true)
errorHandler.dispatch(it.error)
.onResourceLoading {
view?.run {
showProgress(true)
showContent(false)
}
}
.onResourceNotLoading {
view?.run { showProgress(false) }
}
.onResourceError {
view?.showContent(true)
errorHandler.dispatch(it)
}
.onResourceSuccess {
it.let { (recipientChips, selectedRecipientChips) ->
view?.run {
setMailbox(getMailboxName(selectedMailbox))
setRecipients(recipientChips)
if (selectedRecipientChips.isNotEmpty()) setSelectedRecipients(
selectedRecipientChips
)
showContent(true)
}
}
}.onResourceNotLoading {
view?.run { showProgress(false) }
}.launch()
}
.launch()
}
private fun sendMessage(subject: String, content: String, recipients: List<Recipient>) {
val mailbox = selectedMailbox ?: return
resourceFlow {
val student = studentRepository.getCurrentStudent()
val mailbox = mailboxRepository.getMailbox(student)
messageRepository.sendMessage(
student = student,
subject = subject,
@ -222,18 +248,21 @@ class SendMessagePresenter @Inject constructor(
}
}
private fun getMailboxName(mailbox: Mailbox): String {
private fun getMailboxName(mailbox: Mailbox?): String {
mailbox ?: return ""
// username - accountType [\n student name - ] (school short name)
return buildString {
append(mailbox.userName)
append(" - ")
append(getMailboxType(mailbox.type))
appendLine()
if (mailbox.type == MailboxType.PARENT) {
append(" - ")
append(mailbox.studentName)
append(" - ")
}
append(" - ")
append("(${mailbox.schoolNameShort})")
}
}
@ -267,9 +296,9 @@ class SendMessagePresenter @Inject constructor(
private fun saveDraftMessage() {
messageRepository.draftMessage = MessageDraft(
view?.formRecipientsData!!,
view?.formSubjectValue!!,
view?.formContentValue!!
recipients = view?.formRecipientsData!!,
subject = view?.formSubjectValue!!,
content = view?.formContentValue!!,
)
}

View File

@ -61,4 +61,5 @@ interface SendMessageView : BaseView {
fun getMessageBackupDialogStringWithRecipients(recipients: String): String
fun clearDraft()
fun showMailboxChooser(mailboxes: List<Mailbox>)
}

View File

@ -1,28 +1,34 @@
package io.github.wulkanowy.ui.modules.message.tab
import android.content.res.ColorStateList
import android.graphics.Typeface
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.CompoundButton
import androidx.core.view.isVisible
import androidx.core.widget.ImageViewCompat
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.ItemMessageBinding
import io.github.wulkanowy.databinding.ItemMessageChipsBinding
import io.github.wulkanowy.utils.getCompatColor
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.toFormattedString
import javax.inject.Inject
class MessageTabAdapter @Inject constructor() :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var onItemClickListener: (MessageTabDataItem.MessageItem, position: Int) -> Unit = { _, _ -> }
lateinit var onItemClickListener: (MessageTabDataItem.MessageItem, position: Int) -> Unit
var onLongItemClickListener: (MessageTabDataItem.MessageItem) -> Unit = {}
lateinit var onLongItemClickListener: (MessageTabDataItem.MessageItem) -> Unit
var onHeaderClickListener: (CompoundButton, Boolean) -> Unit = { _, _ -> }
lateinit var onHeaderClickListener: (CompoundButton, Boolean) -> Unit
var onChangesDetectedListener = {}
lateinit var onMailboxClickListener: () -> Unit
lateinit var onChangesDetectedListener: () -> Unit
private var items = mutableListOf<MessageTabDataItem>()
@ -46,12 +52,12 @@ class MessageTabAdapter @Inject constructor() :
val inflater = LayoutInflater.from(parent.context)
return when (MessageItemViewType.values()[viewType]) {
MessageItemViewType.MESSAGE -> ItemViewHolder(
ItemMessageBinding.inflate(inflater, parent, false)
)
MessageItemViewType.FILTERS -> HeaderViewHolder(
ItemMessageChipsBinding.inflate(inflater, parent, false)
)
MessageItemViewType.MESSAGE -> ItemViewHolder(
ItemMessageBinding.inflate(inflater, parent, false)
)
}
}
@ -66,6 +72,20 @@ class MessageTabAdapter @Inject constructor() :
val item = items[position] as MessageTabDataItem.FilterHeader
with(holder.binding) {
chipMailbox.text = item.selectedMailbox
?: root.context.getString(R.string.message_chip_all_mailboxes)
chipMailbox.chipBackgroundColor = ColorStateList.valueOf(
if (item.selectedMailbox == null) {
root.context.getCompatColor(R.color.mtrl_choice_chip_background_color)
} else root.context.getThemeAttrColor(android.R.attr.colorPrimary, 64)
)
chipMailbox.setTextColor(
if (item.selectedMailbox == null) {
root.context.getThemeAttrColor(android.R.attr.textColorPrimary)
} else root.context.getThemeAttrColor(android.R.attr.colorPrimary)
)
chipMailbox.setOnClickListener { onMailboxClickListener() }
if (item.onlyUnread == null) {
chipUnread.isVisible = false
} else {
@ -74,6 +94,7 @@ class MessageTabAdapter @Inject constructor() :
chipUnread.setOnCheckedChangeListener(onHeaderClickListener)
}
chipUnread.isEnabled = item.isEnabled
chipAttachments.isEnabled = item.isEnabled
chipAttachments.isChecked = item.onlyWithAttachments
chipAttachments.setOnCheckedChangeListener(onHeaderClickListener)
@ -85,21 +106,35 @@ class MessageTabAdapter @Inject constructor() :
val message = item.message
with(holder.binding) {
val style = if (message.unread) Typeface.BOLD else Typeface.NORMAL
val normalFont = Typeface.create("sans-serif", Typeface.NORMAL)
val boldFont = Typeface.create("sans-serif-black", Typeface.NORMAL)
val primaryColor = root.context.getThemeAttrColor(android.R.attr.textColorPrimary)
val secondaryColor = root.context.getThemeAttrColor(android.R.attr.textColorSecondary)
val currentFont = if (message.unread) boldFont else normalFont
val currentTextColor = if (message.unread) primaryColor else secondaryColor
with(messageItemAuthor) {
text = message.correspondents
setTypeface(null, style)
setTextColor(currentTextColor)
typeface = currentFont
}
messageItemSubject.run {
with(messageItemSubject) {
text = message.subject.ifBlank { context.getString(R.string.message_no_subject) }
setTypeface(null, style)
setTextColor(currentTextColor)
typeface = currentFont
}
messageItemDate.run {
with(messageItemDate) {
text = message.date.toFormattedString()
setTypeface(null, style)
setTextColor(currentTextColor)
typeface = currentFont
}
messageItemAttachmentIcon.isVisible = message.hasAttachments
with(messageItemAttachmentIcon) {
ImageViewCompat.setImageTintList(this, ColorStateList.valueOf(currentTextColor))
isVisible = message.hasAttachments
}
messageItemUnreadIndicator.isVisible = message.unread
root.setOnClickListener {
holder.bindingAdapterPosition.let {
@ -111,7 +146,7 @@ class MessageTabAdapter @Inject constructor() :
root.setOnLongClickListener {
onLongItemClickListener(item)
return@setOnLongClickListener true
true
}
with(messageItemCheckbox) {

View File

@ -11,6 +11,7 @@ sealed class MessageTabDataItem(val viewType: MessageItemViewType) {
) : MessageTabDataItem(MessageItemViewType.MESSAGE)
data class FilterHeader(
val selectedMailbox: String?,
val onlyUnread: Boolean?,
val onlyWithAttachments: Boolean,
val isEnabled: Boolean

View File

@ -10,15 +10,18 @@ import android.widget.CompoundButton
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.core.view.updatePadding
import androidx.fragment.app.setFragmentResultListener
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.databinding.FragmentMessageTabBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog
import io.github.wulkanowy.ui.modules.message.preview.MessagePreviewFragment
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.dpToPx
@ -104,6 +107,7 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
onItemClickListener = presenter::onMessageItemSelected
onLongItemClickListener = presenter::onMessageItemLongSelected
onHeaderClickListener = ::onChipChecked
onMailboxClickListener = presenter::onMailboxFilterSelected
onChangesDetectedListener = ::resetListPosition
}
@ -123,6 +127,12 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
messageTabErrorRetry.setOnClickListener { presenter.onRetry() }
messageTabErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}
setFragmentResultListener(requireArguments().getString(MESSAGE_TAB_FOLDER_ID)!!) { _, bundle ->
presenter.onMailboxSelected(
mailbox = bundle.getSerializable(MailboxChooserDialog.MAILBOX_KEY) as? Mailbox,
)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -246,6 +256,16 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
)
}
override fun showMailboxChooser(mailboxes: List<Mailbox>) {
(activity as? MainActivity)?.showDialogFragment(
MailboxChooserDialog.newInstance(
mailboxes = mailboxes,
isMailboxRequired = false,
folder = requireArguments().getString(MESSAGE_TAB_FOLDER_ID)!!,
)
)
}
override fun hideKeyboard() {
activity?.hideSoftInput()
}

View File

@ -1,9 +1,9 @@
package io.github.wulkanowy.ui.modules.message.tab
import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.repositories.MailboxRepository
import io.github.wulkanowy.data.repositories.MessageRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
@ -26,7 +26,6 @@ class MessageTabPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val messageRepository: MessageRepository,
private val mailboxRepository: MailboxRepository,
private val analytics: AnalyticsHelper
) : BasePresenter<MessageTabView>(errorHandler, studentRepository) {
@ -36,6 +35,9 @@ class MessageTabPresenter @Inject constructor(
private var lastSearchQuery = ""
private var mailboxes: List<Mailbox> = emptyList()
private var selectedMailbox: Mailbox? = null
private var messages = emptyList<Message>()
private val searchChannel = Channel<String>()
@ -122,8 +124,7 @@ class MessageTabPresenter @Inject constructor(
runCatching {
val student = studentRepository.getCurrentStudent(true)
val mailbox = mailboxRepository.getMailbox(student)
messageRepository.deleteMessages(student, mailbox, messageList)
messageRepository.deleteMessages(student, selectedMailbox, messageList)
}
.onFailure(errorHandler::dispatch)
.onSuccess { view?.showMessagesDeleted() }
@ -202,13 +203,28 @@ class MessageTabPresenter @Inject constructor(
}
}
fun onMailboxFilterSelected() {
view?.showMailboxChooser(mailboxes)
}
fun onMailboxSelected(mailbox: Mailbox?) {
selectedMailbox = mailbox
loadData(false)
}
private fun loadData(forceRefresh: Boolean) {
Timber.i("Loading $folder message data started")
flatResourceFlow {
val student = studentRepository.getCurrentStudent()
val mailbox = mailboxRepository.getMailbox(student)
messageRepository.getMessages(student, mailbox, folder, forceRefresh)
if (selectedMailbox == null && mailboxes.isEmpty()) {
selectedMailbox = messageRepository.getMailboxByStudent(student)
mailboxes = messageRepository.getMailboxes(student, forceRefresh).toFirstResult()
.dataOrNull.orEmpty()
}
messageRepository.getMessages(student, selectedMailbox, folder, forceRefresh)
}
.logResourceStatus("load $folder message")
.onResourceData {
@ -327,7 +343,16 @@ class MessageTabPresenter @Inject constructor(
MessageTabDataItem.FilterHeader(
onlyUnread = onlyUnread.takeIf { folder != MessageFolder.SENT },
onlyWithAttachments = onlyWithAttachments,
isEnabled = !isActionMode
isEnabled = !isActionMode,
selectedMailbox = selectedMailbox?.let {
buildString {
if (it.studentName.isNotBlank() && it.studentName != it.userName) {
append(it.studentName)
append(" - ")
}
append(it.userName)
}
},
)
)

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.message.tab
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.ui.base.BaseView
@ -46,4 +47,6 @@ interface MessageTabView : BaseView {
fun showActionMode(show: Boolean)
fun showRecyclerBottomPadding(show: Boolean)
fun showMailboxChooser(mailboxes: List<Mailbox>)
}

View File

@ -191,7 +191,7 @@ class TimetableAdapter @Inject constructor() :
)
} else {
timetableItemDescription.visibility = GONE
timetableItemRoom.visibility = VISIBLE
timetableItemRoom.isVisible = lesson.room.isNotBlank() || lesson.roomOld.isNotBlank()
timetableItemGroup.isVisible = item.showGroupsInPlan && lesson.group.isNotBlank()
timetableItemTeacher.visibility = VISIBLE
}

View File

@ -15,16 +15,17 @@ import java.net.ConnectException
import java.net.SocketException
import java.net.SocketTimeoutException
import java.net.UnknownHostException
import java.security.cert.CertPathValidatorException
import java.security.cert.CertificateExpiredException
import java.security.cert.CertificateNotYetValidException
import javax.net.ssl.SSLHandshakeException
fun Resources.getErrorString(error: Throwable): String = when (error) {
is UnknownHostException -> R.string.error_no_internet
is ConnectException,
is SocketException,
is SocketTimeoutException,
is InterruptedIOException,
is ConnectException,
is StreamResetException -> R.string.error_timeout
is NotLoggedInException -> R.string.error_login_failed
is PasswordChangeRequiredException -> R.string.error_password_change_required
@ -42,10 +43,10 @@ fun Resources.getErrorString(error: Throwable): String = when (error) {
fun Throwable.isShouldBeReported(): Boolean = when (this) {
is UnknownHostException,
is ConnectException,
is SocketException,
is SocketTimeoutException,
is InterruptedIOException,
is ConnectException,
is StreamResetException,
is ServiceUnavailableException,
is FeatureDisabledException,
@ -70,5 +71,6 @@ private fun Throwable?.isCausedByCertificateNotValidNow(): Boolean {
private fun Throwable?.isCertificateNotValidNow(): Boolean {
val isNotYetValid = this is CertificateNotYetValidException
val isExpired = this is CertificateExpiredException
return isNotYetValid || isExpired
val isInvalidPath = this is CertPathValidatorException
return isNotYetValid || isExpired || isInvalidPath
}

View File

@ -4,6 +4,7 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.enums.GradeColorTheme
import io.github.wulkanowy.sdk.scrapper.grades.getGradeValueWithModifier
import io.github.wulkanowy.sdk.scrapper.grades.isGradeValid
fun List<Grade>.calcAverage(isOptionalArithmeticAverage: Boolean): Double {
@ -20,20 +21,15 @@ fun List<Grade>.calcAverage(isOptionalArithmeticAverage: Boolean): Double {
}
fun List<GradeSummary>.calcFinalAverage(plusModifier: Double, minusModifier: Double) = asSequence()
.mapNotNull {
if (it.finalGrade.matches("[0-6][+-]?".toRegex())) {
when {
it.finalGrade.endsWith('+') -> {
it.finalGrade.removeSuffix("+").toDouble() + plusModifier
}
it.finalGrade.endsWith('-') -> {
it.finalGrade.removeSuffix("-").toDouble() - minusModifier
}
else -> {
it.finalGrade.toDouble()
}
}
} else null
.mapNotNull { summary ->
val (gradeValue, gradeModifier) = getGradeValueWithModifier(summary.finalGrade)
if (gradeValue == null || gradeModifier == null) return@mapNotNull null
when {
gradeModifier > 0 -> gradeValue + plusModifier
gradeModifier < 0 -> gradeValue - minusModifier
else -> gradeValue + 0.0
}
}
.average()
.let { if (it.isNaN()) 0.0 else it }

View File

@ -4,6 +4,7 @@ import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageFolder
@ -25,8 +26,8 @@ fun getRefreshKey(name: String, student: Student): String {
return "${name}_${student.userLoginId}"
}
fun getRefreshKey(name: String, student: Student, folder: MessageFolder): String {
return "${name}_${student.id}_${folder.id}"
fun getRefreshKey(name: String, mailbox: Mailbox?, folder: MessageFolder): String {
return "${name}_${mailbox?.globalKey ?: "all"}_${folder.id}"
}
class AutoRefreshHelper @Inject constructor(

View File

@ -1,7 +1,9 @@
Wersja 1.7.5
Wersja 1.8.0
- naprawiliśmy kilka błędów w obsłudze nowego modułu wiadomości
- naprawiliśmy wyświetlanie napisu "Brak ocen", jesli uczeń nie zdobył w danym semestrze jeszcze żadnych ocen
- naprawiliśmy logowanie do aplikacji rodzicom będącym jednocześnie nauczycielami
- naprawiliśmy liczenie średniej ucznia w ocenach klasy dla wykresu "Wszystkie"
- zmieniliśmy kolejność przycisków akcji w podglądzie wiadomości
- ulepszyliśmy oznaczenie nieodczytanych wiadomości
- naprawiliśmy pokazywanie informacji o odczytanej wiadomości w wysłanych
- dodaliśmy opcję ręcznego wybierania skrzynki pocztowej
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases

View File

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

View File

@ -55,17 +55,29 @@
android:id="@+id/sendMessageFrom"
android:layout_width="0dp"
android:layout_height="58dp"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginStart="8dp"
android:background="?selectableItemBackground"
android:gravity="center_vertical"
android:paddingStart="8dp"
android:paddingEnd="32dp"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/sendMessageFromHint"
app:layout_constraintTop_toTopOf="parent"
tools:text="Jan Kowalski" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:rotation="270"
android:src="@drawable/ic_chevron_left"
app:layout_constraintBottom_toBottomOf="@id/sendMessageFrom"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/sendMessageFrom"
app:tint="?android:textColorSecondary"
tools:ignore="ContentDescription" />
<View
android:id="@+id/sendMessageFromDivider"
android:layout_width="match_parent"

View File

@ -21,7 +21,7 @@
<LinearLayout
android:id="@+id/gradeDialogValueLayout"
android:layout_width="101dp"
android:layout_height="121dp"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:ignore="UselessParent">
<TextView
android:id="@+id/account_quick_dialog_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="24dp"
android:paddingLeft="24dp"
android:paddingEnd="24dp"
android:paddingRight="24dp"
android:text="@string/message_mailbox_chooser_title"
android:textSize="20sp"
android:textStyle="bold"
app:firstBaselineToTopHeight="40dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/account_quick_dialog_recycler"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:layout_weight="1"
android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="3"
tools:listitem="@layout/item_mailbox_chooser" />
</LinearLayout>
</RelativeLayout>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:orientation="horizontal"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp"
tools:context=".ui.modules.message.mailboxchooser.MailboxChooserAdapter">
<TextView
android:id="@+id/mailboxItemName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="12dp"
android:ellipsize="end"
android:maxLines="1"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/full_names" />
<TextView
android:id="@+id/mailboxItemSchool"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="12dp"
android:layout_marginTop="3dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/mailboxItemName"
tools:text="@tools:sample/lorem/random" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -30,6 +30,7 @@
android:layout_marginEnd="10dp"
android:ellipsize="end"
android:singleLine="true"
android:textColor="?android:textColorSecondary"
android:textSize="15sp"
app:layout_constraintEnd_toStartOf="@+id/messageItemDate"
app:layout_constraintStart_toEndOf="@id/messageItemCheckbox"
@ -40,10 +41,13 @@
android:id="@+id/messageItemDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:gravity="end"
android:textColor="?android:textColorSecondary"
android:textSize="13sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@id/messageItemUnreadIndicator"
app:layout_constraintTop_toTopOf="parent"
app:layout_goneMarginEnd="0dp"
tools:text="@tools:sample/date/ddmmyy" />
<TextView
@ -69,9 +73,21 @@
android:layout_height="16dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/messageItemSubject"
app:layout_constraintEnd_toEndOf="@id/messageItemDate"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@drawable/ic_attachment"
app:tint="?colorOnBackground"
app:tint="?android:textColorSecondary"
tools:ignore="ContentDescription"
tools:visibility="visible" />
<ImageView
android:id="@+id/messageItemUnreadIndicator"
android:layout_width="10dp"
android:layout_height="10dp"
android:src="@drawable/ic_circle"
app:layout_constraintBottom_toBottomOf="@id/messageItemDate"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/messageItemDate"
app:tint="?colorPrimary"
tools:ignore="ContentDescription"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,21 +1,30 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/messageChipsLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingTop="10dp"
android:paddingRight="16dp"
tools:context=".ui.modules.message.tab.MessageTabAdapter">
<com.google.android.material.chip.ChipGroup
android:id="@+id/messageChipGroup"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingLeft="16dp"
android:paddingTop="10dp"
android:paddingRight="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintTop_toTopOf="parent"
app:singleLine="true">
<com.google.android.material.chip.Chip
android:id="@+id/chip_mailbox"
style="@style/Widget.MaterialComponents.Chip.Action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/message_chip_all_mailboxes" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_unread"
@ -37,4 +46,4 @@
app:checkedIconEnabled="true"
app:checkedIconTint="@color/mtrl_choice_chip_text_color" />
</com.google.android.material.chip.ChipGroup>
</androidx.constraintlayout.widget.ConstraintLayout>
</HorizontalScrollView>

View File

@ -8,20 +8,6 @@
android:title="@string/message_reply"
app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="ifRoom" />
<item
android:id="@+id/messagePreviewMenuForward"
android:icon="@drawable/ic_menu_message_forward"
android:orderInCategory="1"
android:title="@string/message_forward"
app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="ifRoom" />
<item
android:id="@+id/messagePreviewMenuDelete"
android:icon="@drawable/ic_menu_message_delete"
android:orderInCategory="1"
android:title="@string/message_move_to_trash"
app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="ifRoom" />
<item
android:id="@+id/messagePreviewMenuShare"
android:icon="@drawable/ic_menu_message_share"
@ -36,4 +22,18 @@
android:title="@string/message_print"
app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="ifRoom" />
<item
android:id="@+id/messagePreviewMenuForward"
android:icon="@drawable/ic_menu_message_forward"
android:orderInCategory="1"
android:title="@string/message_forward"
app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="ifRoom" />
<item
android:id="@+id/messagePreviewMenuDelete"
android:icon="@drawable/ic_menu_message_delete"
android:orderInCategory="1"
android:title="@string/message_move_to_trash"
app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="ifRoom" />
</menu>

View File

@ -309,9 +309,11 @@
<string name="message_not_exists">Zpráva neexistuje</string>
<string name="message_required_recipients">Musíte vybrat alespoň 1 příjemce</string>
<string name="message_content_min_length">Obsah zprávy musí mít alespoň 3 znaky</string>
<string name="message_chip_all_mailboxes">Všechny poštovní schránky</string>
<string name="message_chip_only_unread">Pouze nepřečtené</string>
<string name="message_chip_only_with_attachments">Pouze s přílohami</string>
<string name="message_read">Přečtena: %s</string>
<string name="message_read_by">Přečtena přes: %1$d z %2$d osob</string>
<plurals name="message_number_item">
<item quantity="one">%1$d zpráva</item>
<item quantity="few">%1$d zprávy</item>
@ -339,6 +341,7 @@
<item quantity="other">%1$d vybraných</item>
</plurals>
<string name="message_messages_deleted">Zprávy odstraněné</string>
<string name="message_mailbox_chooser_title">Vyberte poštovní schránku</string>
<!--Note-->
<string name="note_no_items">Žádné informace o poznámkách</string>
<string name="note_points">Body</string>
@ -735,7 +738,7 @@
<string name="pref_ads_consent_description">Volbu můžete kdykoliv změnit v nastavení aplikace. Můžeme použít Vaše data k zobrazení reklam šitých pro vás nebo pomocí méně vašich dat zobrazovat nepřizpůsobené reklamy. Podrobnosti naleznete v našich Zásadách ochrany osobních údajů</string>
<string name="pref_ads_summary_personalized">Přizpůsobené reklamy</string>
<string name="pref_ads_summary_non_personalized">Nepřizpůsobené reklamy</string>
<string name="pref_ads_over_18_years_old">Mám ukončené 18 let</string>
<string name="pref_ads_over_18_years_old">Je mi více než 18 let</string>
<string name="pref_ads_option_personalized">Ano, přizpůsobené reklamy</string>
<string name="pref_ads_option_non_personalized">Ano, nepřizpůsobené reklamy</string>
<string name="pref_settings_advanced_title">Pokročilé</string>

View File

@ -275,9 +275,11 @@
<string name="message_not_exists">Nachricht nicht vorhanden</string>
<string name="message_required_recipients">Sie müssen mindestens 1 Empfänger auswählen.</string>
<string name="message_content_min_length">Der Inhalt der Nachricht muss mindestens 3 Zeichen lang sein.</string>
<string name="message_chip_all_mailboxes">All mailboxes</string>
<string name="message_chip_only_unread">Nur ungelesen</string>
<string name="message_chip_only_with_attachments">Nur mit Anhängen</string>
<string name="message_read">Lesen: %s</string>
<string name="message_read_by">Read by: %1$d of %2$d people</string>
<plurals name="message_number_item">
<item quantity="one">%1$d Nachricht</item>
<item quantity="other">%1$d Nachrichten</item>
@ -297,6 +299,7 @@
<item quantity="other">%1$d ausgewählt</item>
</plurals>
<string name="message_messages_deleted">Nachrichten gelöscht</string>
<string name="message_mailbox_chooser_title">Choose mailbox</string>
<!--Note-->
<string name="note_no_items">Keine Informationen über Eintragen</string>
<string name="note_points">Punkte</string>
@ -632,10 +635,10 @@
<string name="pref_other_fill_message_content">Antwort mit Nachrichtenhistorie</string>
<string name="pref_other_optional_arithmetic_average">Arithmetisches Mittel anzeigen, wenn keine Gewichte angegeben sind</string>
<string name="pref_ads_support_category_name">Unterstützung</string>
<string name="pref_ads_privacy_policy">Privacy Policy</string>
<string name="pref_ads_agreements">Agreements</string>
<string name="pref_ads_consent">Consent to processing of data related to ads</string>
<string name="pref_ads_show_in_app">Show ads in app</string>
<string name="pref_ads_privacy_policy">Datenschutz-Bestimmungen</string>
<string name="pref_ads_agreements">Vereinbarungen</string>
<string name="pref_ads_consent">Zustimmung zur Verarbeitung von Daten im Zusammenhang mit Anzeigen</string>
<string name="pref_ads_show_in_app">Anzeigen in der App anzeigen</string>
<string name="pref_ads_support">Einzelanzeige ansehen, um Projekt zu unterstützen</string>
<string name="pref_ads_privacy_title">Einwilligung in die Datenverarbeitung</string>
<string name="pref_ads_privacy_description">Um eine Anzeige zu sehen, müssen Sie mit den Datenverarbeitungsbedingungen unserer Datenschutzerklärung einverstanden sein</string>
@ -647,9 +650,9 @@
<string name="pref_ads_consent_description">Sie können Ihre Wahl jederzeit in den App-Einstellungen ändern. Wir verwenden Ihre Daten, um auf Sie zugeschnittene Anzeigen anzuzeigen oder unter Verwendung weniger Ihrer Daten nicht personalisierte Werbung anzuzeigen. Bitte lesen Sie unsere Datenschutzerklärung für Details</string>
<string name="pref_ads_summary_personalized">Personalisierte Werbung</string>
<string name="pref_ads_summary_non_personalized">keine personalisierte Werbung</string>
<string name="pref_ads_over_18_years_old">I am over 18 years old</string>
<string name="pref_ads_option_personalized">Yes, personalized ads</string>
<string name="pref_ads_option_non_personalized">Yes, non-personalized ads</string>
<string name="pref_ads_over_18_years_old">Ich bin über 18 Jahre alt</string>
<string name="pref_ads_option_personalized">Ja, personalisierte Werbung</string>
<string name="pref_ads_option_non_personalized">Ja, nicht personalisierte Werbung</string>
<string name="pref_settings_advanced_title">Erweitert</string>
<string name="pref_settings_appearance_title">Aussehen &amp; Verhalten</string>
<string name="pref_settings_notifications_title">Benachrichtigungen</string>

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string-array name="app_theme_entries" tools:ignore="InconsistentArrays">
<item>Light</item>
<item>Dark</item>
<item>Black (AMOLED)</item>
</string-array>
<string-array name="app_language_entries">
<item>System language</item>
<item>Polski</item>
<item>English</item>
<item>Pусский</item>
<item>Українська</item>
<item>Deutsch</item>
<item>Čeština</item>
<item>Slovenčina</item>
</string-array>
<string-array name="services_interval_entries">
<item>15 minutes</item>
<item>30 minutes</item>
<item>1 hour</item>
<item>2 hours</item>
<item>6 hours</item>
<item>12 hours</item>
<item>24 hours</item>
</string-array>
<string-array name="grade_modifier_entries">
<item>0,00</item>
<item>0,25</item>
<item>0,33</item>
<item>0,5</item>
<item>0,75</item>
</string-array>
<string-array name="grade_sorting_mode_entries">
<item>Alphabetically</item>
<item>By date</item>
<item>By average</item>
</string-array>
<string-array name="grade_color_scheme_entries">
<item>Dzienniczek+</item>
<item>Wulkanowy</item>
<item>Grade colors in register</item>
</string-array>
<string-array name="default_expand_grade_entries">
<item>Up to 1 at once</item>
<item>Always expanded</item>
<item>Unlimited expansions</item>
</string-array>
<string-array name="grade_average_mode_entries">
<item>Average of grades only from selected semester</item>
<item>Average of averages from both semesters</item>
<item>Average of grades from the whole year</item>
</string-array>
<string-array name="dashboard_tile_entries">
<item>Lucky number</item>
<item>Unread messages</item>
<item>Attendance</item>
<item>Lessons</item>
<item>Grades</item>
<item>Homework</item>
<item>School announcements</item>
<item>Exams</item>
<item>Conferences</item>
</string-array>
</resources>

View File

@ -0,0 +1,718 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Activity/Fragment title-->
<string name="login_title">Login</string>
<string name="main_title">Wulkanowy</string>
<string name="grade_title">Grades</string>
<string name="attendance_title">Attendance</string>
<string name="exam_title">Exams</string>
<string name="timetable_title">Timetable</string>
<string name="settings_title">Settings</string>
<string name="more_title">More</string>
<string name="about_title">About</string>
<string name="logviewer_title">Log viewer</string>
<string name="debug_title">Debug</string>
<string name="notification_debug_title">Notification debug</string>
<string name="contributors_title">Contributors</string>
<string name="license_title">Licenses</string>
<string name="message_title">Messages</string>
<string name="send_message_title">New message</string>
<string name="add_homework_title">New homework</string>
<string name="note_title">Notes and achievements</string>
<string name="homework_title">Homework</string>
<string name="account_title">Accounts manager</string>
<string name="account_quick_title">Select account</string>
<string name="account_details_title">Account details</string>
<string name="student_info_title">Student info</string>
<string name="dashboard_title">Dashboard</string>
<string name="notifications_center_title">Notifications center</string>
<!--Subtitles-->
<string name="grade_subtitle">Semester %1$d, %2$d/%3$d</string>
<!--Login-->
<string name="login_header_default">Sign in with the student or parent account</string>
<string name="login_header_symbol">Enter the symbol from the register page for account: &lt;b&gt;%1$s&lt;/b&gt;</string>
<string name="login_nickname_hint">Username</string>
<string name="login_email_hint">Email</string>
<string name="login_login_pesel_email_hint">Login, PESEL or e-mail</string>
<string name="login_password_hint">Password</string>
<string name="login_host_hint">UONET+ register variant</string>
<string name="login_type_api">Mobile API</string>
<string name="login_type_scrapper">Scraper</string>
<string name="login_type_hybrid">Hybrid</string>
<string name="login_token_hint">Token</string>
<string name="login_pin_hint">PIN</string>
<string name="login_symbol_hint">Symbol</string>
<string name="login_sign_in">Sign in</string>
<string name="login_invalid_password">Password too short</string>
<string name="login_incorrect_password_default">Login details are incorrect</string>
<string name="login_incorrect_password">%1$s. Make sure the correct UONET+ register variation is selected below</string>
<string name="login_invalid_pin">Invalid PIN</string>
<string name="login_invalid_token">Invalid token</string>
<string name="login_expired_token">Token expired</string>
<string name="login_invalid_email">Invalid email</string>
<string name="login_invalid_login">Use the assigned login instead of email</string>
<string name="login_invalid_custom_email">Use the assigned login or email in @%1$s</string>
<string name="login_invalid_symbol">Invalid symbol</string>
<string name="login_incorrect_symbol">Student not found. Validate the symbol and the chosen variation of the UONET+ register</string>
<string name="login_duplicate_student">Selected student is already logged in</string>
<string name="login_symbol_helper">The symbol can be found on the register page in&#160;<b>Uczeń</b> →&#160;<b>Dostęp Mobilny</b>&#160;<b>Zarejestruj urządzenie mobilne</b>.\n\nMake sure that you have set the appropriate register variant in the <b>UONET+ register variant</b> field on the previous screen</string>
<string name="login_select_student">Select students to log in to the application</string>
<string name="login_advanced">Other options</string>
<string name="login_advanced_warning_mobile_api">In this mode, a lucky number does not work, a class grade stats, summary of attendance, excuse for absence, completed lessons, school information and preview of the list of registered devices</string>
<string name="login_advanced_warning_scraper">This mode displays the same data as it appears on the register website</string>
<string name="login_advanced_warning_hybrid">The combination of the best features of the other two modes. It works faster than scraper and provides features not available in the Mobile API mode. It is in the experimental phase</string>
<string name="login_privacy_policy">Privacy policy</string>
<string name="login_contact_header">Trouble signing in? Contact us!</string>
<string name="login_contact_email">Email</string>
<string name="login_contact_discord">Discord</string>
<string name="login_email_intent_title">Send email</string>
<string name="login_recover_warning">Make sure you select the correct UONET+ register variation!</string>
<string name="login_recover_button">I forgot my password</string>
<string name="login_recover_title">Recover your account</string>
<string name="login_recover">Recover</string>
<string name="login_signed_in">Student is already signed in</string>
<string name="login_host_standard">Standard</string>
<!--Main-->
<string name="main_account_picker">Account manager</string>
<string name="main_log_in">Log in</string>
<string name="main_session_expired">Session expired</string>
<string name="main_session_relogin">Session expired, log in again</string>
<string name="main_support_title">Application support</string>
<string name="main_support_description">Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time</string>
<string name="main_support_positive">Enable ads</string>
<!--Grade-->
<string name="grade_header">Grade</string>
<string name="grade_semester">Semester %d</string>
<string name="grade_switch_semester">Change semester</string>
<string name="grade_no_items">No grades</string>
<string name="grade_weight">Weight</string>
<string name="grade_weight_value">Weight: %s</string>
<string name="grade_comment">Comment</string>
<string name="grade_number_new_items">Number of new ratings: %1$d</string>
<string name="grade_average">Average: %1$.2f</string>
<string name="grade_points_sum">Points: %s</string>
<string name="grade_no_average">No average</string>
<string name="grade_summary_points">Total points</string>
<string name="grade_summary_final_grade">Final grade</string>
<string name="grade_summary_predicted_grade">Predicted grade</string>
<string name="grade_summary_calculated_average">Calculated average</string>
<string name="grade_summary_calculated_average_help_dialog_title">How does Calculated Average work?</string>
<string name="grade_summary_calculated_average_help_dialog_message">The Calculated Average is the arithmetic average calculated from the subjects averages. It allows you to know the approximate final average. It is calculated in a way selected by the user in the application settings. It is recommended that you choose the appropriate option. This is because the calculation of school averages differs. Additionally, if your school reports the average of the subjects on the Vulcan page, the application downloads them and does not calculate these averages. This can be changed by forcing the calculation of the average in the application settings.\n\n<b>Average of grades only from selected semester</b>:\n1. Calculating the weighted average for each subject in a given semester\n2.Adding calculated averages\n3. Calculation of the arithmetic average of the summed averages\n\n<b>Average of averages from both semesters</b>:\n1.Calculating the weighted average for each subject in semester 1 and 2\n2. Calculating the arithmetic average of the calculated averages for semesters 1 and 2 for each subject.\n3. Adding calculated averages\n4. Calculation of the arithmetic average of the summed averages\n\n<b>Average of grades from the whole year:</b>\n1. Calculating weighted average over the year for each subject. The final average in the 1st semester is irrelevant.\n2. Adding calculated averages\n3. Calculating the arithmetic average of summed averages</string>
<string name="grade_summary_final_average_help_dialog_title">How does the Final Average work?</string>
<string name="grade_summary_final_average_help_dialog_message">The Final Average is the arithmetic average calculated from all currently available final grades in the given semester.\n\nThe calculation scheme consists of the following steps:\n1. Summing up the final grades given by teachers\n2. Divide by the number of subjects that have already been graded</string>
<string name="grade_summary_final_average">Final average</string>
<string name="grade_summary_from_subjects">from %1$d of %2$d subjects</string>
<string name="grade_menu_summary">Summary</string>
<string name="grade_menu_statistics">Class</string>
<string name="grade_menu_read">Mark as read</string>
<string name="grade_statistics_partial">Partial</string>
<string name="grade_statistics_semester">Semester</string>
<string name="grade_statistics_points">Points</string>
<string name="grade_statistics_legend">Legend</string>
<string name="grade_statistics_class_average">Class average: %1$s</string>
<string name="grade_statistics_student_average">Your average: %1$s</string>
<string name="grade_statistics_student_grade">Your grade: %1$s</string>
<string name="grade_statistics_average_class">Class</string>
<string name="grade_statistics_average_student">Student</string>
<plurals name="grade_number_item">
<item quantity="one">%d grade</item>
<item quantity="other">%d grades</item>
</plurals>
<plurals name="grade_new_items">
<item quantity="one">New grade</item>
<item quantity="other">New grades</item>
</plurals>
<plurals name="grade_new_items_predicted">
<item quantity="one">New predicted grade</item>
<item quantity="other">New predicted grades</item>
</plurals>
<plurals name="grade_new_items_final">
<item quantity="one">New final grade</item>
<item quantity="other">New final grades</item>
</plurals>
<plurals name="grade_notify_new_items">
<item quantity="one">You received %1$d grade</item>
<item quantity="other">You received %1$d grades</item>
</plurals>
<plurals name="grade_notify_new_items_predicted">
<item quantity="one">You received %1$d predicted grade</item>
<item quantity="other">You received %1$d predicted grades</item>
</plurals>
<plurals name="grade_notify_new_items_final">
<item quantity="one">You received %1$d final grade</item>
<item quantity="other">You received %1$d final grades</item>
</plurals>
<!--Timetable-->
<string name="timetable_lesson">Lesson</string>
<string name="timetable_room">Room</string>
<string name="timetable_group">Group</string>
<string name="timetable_time">Hours</string>
<string name="timetable_changes">Changes</string>
<string name="timetable_no_items">No lessons this day</string>
<string name="timetable_minutes">%s min</string>
<string name="timetable_seconds">%s sec</string>
<string name="timetable_time_left">%1$s left</string>
<string name="timetable_time_until">in %1$s</string>
<string name="timetable_finished">Finished</string>
<string name="timetable_now">Now: %s</string>
<string name="timetable_next">Next: %s</string>
<string name="timetable_later">Later: %s</string>
<string name="timetable_notify_lesson">%1$s lesson %2$d - %3$s</string>
<string name="timetable_notify_change_room">Change of room from %1$s to %2$s</string>
<string name="timetable_notify_change_teacher">Change of teacher from %1$s to %2$s</string>
<string name="timetable_notify_change_subject">Change of subject from %1$s to %2$s</string>
<plurals name="timetable_notify_new_items_title">
<item quantity="one">Timetable change</item>
<item quantity="other">Timetable changes</item>
</plurals>
<plurals name="timetable_notify_new_items">
<item quantity="one">%1$s - %2$d change in timetable</item>
<item quantity="other">%1$s - %2$d changes in timetable</item>
</plurals>
<plurals name="timetable_notify_new_items_group">
<item quantity="one">%1$d change in timetable</item>
<item quantity="other">%1$d changes in timetable</item>
</plurals>
<plurals name="timetable_number_item">
<item quantity="one">%d change</item>
<item quantity="other">%d changes</item>
</plurals>
<!--Completed lessons-->
<string name="completed_lessons_title">Completed lessons</string>
<string name="completed_lessons_button">Show completed lessons</string>
<string name="completed_lessons_no_items">No info about completed lessons</string>
<string name="completed_lessons_topic">Topic</string>
<string name="completed_lessons_absence">Absence</string>
<string name="completed_lessons_resources">Resources</string>
<!--Additional lessons-->
<string name="additional_lessons_title">Additional lessons</string>
<string name="additional_lessons_button">Show additional lessons</string>
<string name="additional_lessons_no_items">No info about additional lessons</string>
<string name="additional_lessons_add">New lesson</string>
<string name="additional_lessons_add_title">New additional lesson</string>
<string name="additional_lessons_add_success">Additional lesson added successfully</string>
<string name="additional_lessons_delete_success">Additional lesson deleted successfully</string>
<string name="additional_lessons_repeat">Repeat weekly</string>
<string name="additional_lessons_delete_title">Delete additional lesson</string>
<string name="additional_lessons_delete_one">Just this lesson</string>
<string name="additional_lessons_delete_series">All in the series</string>
<string name="additional_lessons_start">Start time</string>
<string name="additional_lessons_end">End time</string>
<string name="additional_lessons_end_time_error">End time must be greater than start time</string>
<!--Attendance-->
<string name="attendance_summary_button">Attendance summary</string>
<string name="attendance_absence_school">Absent for school reasons</string>
<string name="attendance_absence_excused">Excused absence</string>
<string name="attendance_absence_unexcused">Unexcused absence</string>
<string name="attendance_exemption">Exemption</string>
<string name="attendance_excused_lateness">Excused lateness</string>
<string name="attendance_unexcused_lateness">Unexcused lateness</string>
<string name="attendance_present">Present</string>
<string name="attendance_deleted">Deleted</string>
<string name="attendance_unknown">Unknown</string>
<string name="attendance_number">Number of lesson</string>
<string name="attendance_no_items">No entries</string>
<string name="attendance_excuse_dialog_reason">Absence reason (optional)</string>
<string name="attendance_excuse_dialog_submit">Send</string>
<string name="attendance_excuse_success">Absence excuse request sent successfully!</string>
<string name="attendance_excuse_no_selection">You must select at least one absence!</string>
<string name="attendance_excuse_title">Excuse</string>
<plurals name="attendance_notify_new_items_title">
<item quantity="one">New attendance</item>
<item quantity="other">New attendance</item>
</plurals>
<plurals name="attendance_notify_new_items">
<item quantity="one">%1$d new attendance</item>
<item quantity="other">%1$d attendance</item>
</plurals>
<plurals name="attendance_number_item">
<item quantity="one">%d attendance</item>
<item quantity="other">%d attendance</item>
</plurals>
<!--Attendance summary-->
<string name="attendance_summary_total">Total</string>
<!--Exam-->
<string name="exam_no_items">No exams this week</string>
<string name="exam_type">Type</string>
<string name="exam_entry_date">Entry date</string>
<plurals name="exam_notify_new_item_title">
<item quantity="one">New exam</item>
<item quantity="other">New exams</item>
</plurals>
<plurals name="exam_notify_new_item_content">
<item quantity="one">%d new exam</item>
<item quantity="other">%d new exams</item>
</plurals>
<plurals name="exam_number_item">
<item quantity="one">%d exam</item>
<item quantity="other">%d exams</item>
</plurals>
<!--Message-->
<string name="message_inbox">Inbox</string>
<string name="message_sent">Sent</string>
<string name="message_trash">Trash</string>
<string name="message_no_subject">(no subject)</string>
<string name="message_no_items">No messages</string>
<string name="message_from">From:</string>
<string name="message_to">To:</string>
<string name="message_date">Date: %1$s</string>
<string name="message_reply">Reply</string>
<string name="message_forward">Forward</string>
<string name="message_select_all">Select all</string>
<string name="message_unselect_all">Unselect all</string>
<string name="message_move_to_trash">Move to trash</string>
<string name="message_delete_forever">Delete permanently</string>
<string name="message_delete_success">Message deleted successfully</string>
<string name="message_mailbox_type_student">student</string>
<string name="message_mailbox_type_parent">parent</string>
<string name="message_mailbox_type_guardian">guardian</string>
<string name="message_mailbox_type_employee">employee</string>
<string name="message_share">Share</string>
<string name="message_print">Print</string>
<string name="message_subject">Subject</string>
<string name="message_content">Content</string>
<string name="message_send_successful">Message sent successfully</string>
<string name="message_not_exists">Message does not exist</string>
<string name="message_required_recipients">You need to choose at least 1 recipient</string>
<string name="message_content_min_length">The message content must be at least 3 characters</string>
<string name="message_chip_all_mailboxes">All mailboxes</string>
<string name="message_chip_only_unread">Only unread</string>
<string name="message_chip_only_with_attachments">Only with attachments</string>
<string name="message_read">Read: %s</string>
<string name="message_read_by">Read by: %1$d of %2$d people</string>
<plurals name="message_number_item">
<item quantity="one">%1$d message</item>
<item quantity="other">%1$d messages</item>
</plurals>
<plurals name="message_new_items">
<item quantity="one">New message</item>
<item quantity="other">New messages</item>
</plurals>
<string name="message_restore_dialog">Do you want to restore draft message?</string>
<string name="message_restore_dialog_with_recipients">Do you want to restore draft message with recipients: %s?</string>
<plurals name="message_notify_new_items">
<item quantity="one">You received %1$d message</item>
<item quantity="other">You received %1$d messages</item>
</plurals>
<plurals name="message_selected_messages_count">
<item quantity="one">%1$d selected</item>
<item quantity="other">%1$d selected</item>
</plurals>
<string name="message_messages_deleted">Messages deleted</string>
<string name="message_mailbox_chooser_title">Choose mailbox</string>
<!--Note-->
<string name="note_no_items">No info about notes</string>
<string name="note_points">Points</string>
<plurals name="note_number_item">
<item quantity="one">%d note</item>
<item quantity="other">%d notes</item>
</plurals>
<plurals name="note_new_items">
<item quantity="one">New note</item>
<item quantity="other">New notes</item>
</plurals>
<plurals name="note_notify_new_items">
<item quantity="one">You received %1$d note</item>
<item quantity="other">You received %1$d notes</item>
</plurals>
<!--Praise-->
<plurals name="praise_number_item">
<item quantity="one">%d praise</item>
<item quantity="other">%d praises</item>
</plurals>
<plurals name="praise_new_items">
<item quantity="one">New praise</item>
<item quantity="other">New praises</item>
</plurals>
<plurals name="praise_notify_new_items">
<item quantity="one">You received %1$d praise</item>
<item quantity="other">You received %1$d praises</item>
</plurals>
<!--Neutral notes-->
<plurals name="neutral_note_number_item">
<item quantity="one">%d neutral note</item>
<item quantity="other">%d neutral notes</item>
</plurals>
<plurals name="neutral_note_new_items">
<item quantity="one">New neutral note</item>
<item quantity="other">New neutral notes</item>
</plurals>
<plurals name="neutral_note_notify_new_items">
<item quantity="one">You received %1$d neutral note</item>
<item quantity="other">You received %1$d neutral notes</item>
</plurals>
<!--Homework-->
<string name="homework_no_items">No info about homework</string>
<string name="homework_mark_as_done">Mark as done</string>
<string name="homework_mark_as_undone">Mark as undone</string>
<string name="homework_add">Add homework</string>
<string name="homework_add_success">Homework added successfully</string>
<string name="homework_delete_success">Homework deleted successfully</string>
<string name="homework_attachments">Attachments</string>
<plurals name="homework_notify_new_item_title">
<item quantity="one">New homework</item>
<item quantity="other">New homework</item>
</plurals>
<plurals name="homework_notify_new_item_content">
<item quantity="one">You received %d new homework</item>
<item quantity="other">You received %d new homework</item>
</plurals>
<plurals name="homework_number_item">
<item quantity="one">%d homework</item>
<item quantity="other">%d homework</item>
</plurals>
<!--Lucky number-->
<string name="lucky_number_title">Lucky number</string>
<string name="lucky_number_header">Today\'s lucky number is</string>
<string name="lucky_number_empty">No info about the lucky number</string>
<string name="lucky_number_notify_new_item_title">Lucky number for today</string>
<string name="lucky_number_notify_new_item">Today\'s lucky number is: %s</string>
<string name="lucky_number_history_button">Show history</string>
<!--Lucky number history-->
<string name="lucky_number_history_title">Lucky number history</string>
<string name="lucky_number_history_empty">No info about lucky numbers</string>
<!--Mobile devices-->
<string name="mobile_devices_title">Mobile devices</string>
<string name="mobile_devices_no_items">No devices</string>
<string name="mobile_devices_unregister">Deregister</string>
<string name="mobile_device_removed">Device removed</string>
<string name="mobile_device_qr">QR code</string>
<string name="mobile_device_token">Token</string>
<string name="mobile_device_symbol">Symbol</string>
<string name="mobile_device_pin">PIN</string>
<!--School and teachers-->
<string name="schoolandteachers_title">School and teachers</string>
<!--School-->
<string name="school_title">School</string>
<string name="school_no_info">No info about school</string>
<string name="school_name">School name</string>
<string name="school_address">School address</string>
<string name="school_telephone">Telephone</string>
<string name="school_headmaster">Name of headmaster</string>
<string name="school_pedagogue">Name of pedagogue</string>
<string name="school_address_button">Show on map</string>
<string name="school_telephone_button">Call</string>
<!--Teacher-->
<string name="teachers_title">Teachers</string>
<string name="teacher_no_items">No info about teachers</string>
<string name="teacher_no_subject">No subject</string>
<!--Conference-->
<string name="conferences_title">Conferences</string>
<string name="conference_no_items">No info about conferences</string>
<plurals name="conference_number_item">
<item quantity="one">%d conference</item>
<item quantity="other">%d conferences</item>
</plurals>
<plurals name="conference_notify_new_item_title">
<item quantity="one">New conference</item>
<item quantity="other">New conferences</item>
</plurals>
<plurals name="conference_notify_new_items">
<item quantity="one">You have %1$d new conference</item>
<item quantity="other">You have %1$d new conferences</item>
</plurals>
<string name="conferences_present">Present at conference</string>
<string name="conference_agenda">Agenda</string>
<!--Director information-->
<string name="school_announcement_title">School announcements</string>
<string name="school_announcement_no_items">No school announcements</string>
<plurals name="school_announcement_number_item">
<item quantity="one">%d school announcement</item>
<item quantity="other">%d school announcements</item>
</plurals>
<plurals name="school_announcement_notify_new_item_title">
<item quantity="one">New school announcement</item>
<item quantity="other">New school announcements</item>
</plurals>
<plurals name="school_announcement_notify_new_items">
<item quantity="one">You have %1$d new school announcement</item>
<item quantity="other">You have %1$d new school announcements</item>
</plurals>
<!--Account-->
<string name="account_add_new">Add account</string>
<string name="account_logout">Logout</string>
<string name="account_confirm">Do you want to log out this student?</string>
<string name="account_logout_student">Student logout</string>
<string name="account_type_student">Student account</string>
<string name="account_type_parent">Parent account</string>
<string name="account_details_edit">Edit data</string>
<string name="account_quick_manager">Accounts manager</string>
<string name="account_select_student">Select student</string>
<string name="account_family">Family</string>
<string name="account_contact">Contact</string>
<string name="account_address">Residence details</string>
<string name="account_personal_data">Personal information</string>
<!--About-->
<string name="about_version">App version</string>
<string name="about_contributor">Contributors</string>
<string name="about_contributor_summary">List of Wulkanowy developers</string>
<string name="about_feedback">Report a bug</string>
<string name="about_feedback_summary">Send a bug report via e-mail</string>
<string name="about_faq">FAQ</string>
<string name="about_faq_summary">Read Frequently Asked Questions</string>
<string name="about_discord">Discord server</string>
<string name="about_discord_summary">Join the Wulkanowy community</string>
<string name="about_facebook">Facebook fanpage</string>
<string name="about_twitter">Twitter page</string>
<string name="about_twitter_summary">Follow us on twitter</string>
<string name="about_facebook_summary">Like our facebook fanpage</string>
<string name="about_privacy">Privacy policy</string>
<string name="about_privacy_summary">Rules for collecting personal data</string>
<string name="about_system">System settings</string>
<string name="about_system_summary">Open system settings</string>
<string name="about_homepage">Homepage</string>
<string name="about_homepage_summary">Visit the website and help develop the application</string>
<string name="about_licenses">Licenses</string>
<string name="about_licenses_summary">Licenses of libraries used in the application</string>
<!--Licenses-->
<string name="license_dialog_title">License</string>
<!--Contributor-->
<string name="contributor_avatar_description">Avatar</string>
<string name="contributor_see_more">See more on GitHub</string>
<!--Student info-->
<string name="student_info_empty">No info about student or student family</string>
<string name="student_info_first_name">Name</string>
<string name="student_info_second_name">Second name</string>
<string name="student_info_gender">Gender</string>
<string name="student_info_polish_citizenship">Polish citizenship</string>
<string name="student_info_family_name">Family name</string>
<string name="student_info_parents_name">Mother\'s and father\'s names</string>
<string name="student_info_phone">Phone</string>
<string name="student_info_cellphone">Cellphone</string>
<string name="student_info_email">E-mail</string>
<string name="student_info_address">Address of residence</string>
<string name="student_info_registered_address">Address of registration</string>
<string name="student_info_correspondence_address">Correspondence address</string>
<string name="student_info_full_name">Surname and first name</string>
<string name="student_info_kinship">Degree of kinship</string>
<string name="student_info_guardian_address">Address</string>
<string name="student_info_phones">Phones</string>
<string name="student_info_male">Male</string>
<string name="student_info_female">Female</string>
<string name="student_info_last_name">Last name</string>
<string name="student_info_guardian">Guardian</string>
<!--Account edit-->
<string name="account_edit_nick_hint">Nick</string>
<string name="account_edit_header">Add nick</string>
<string name="account_edit_avatar_title">Choose avatar color</string>
<!--Log viewer-->
<string name="logviewer_share">Share logs</string>
<string name="logviewer_refresh">Refresh</string>
<!--Dashboard-->
<string name="dashboard_timetable_title">Lessons</string>
<string name="dashboard_timetable_title_tomorrow">(Tomorrow)</string>
<string name="dashboard_timetable_title_today_and_tomorrow">(Today and tomorrow)</string>
<string name="dashboard_timetable_first_lesson_title_moment">In a moment:</string>
<string name="dashboard_timetable_first_lesson_title_soon">Soon:</string>
<string name="dashboard_timetable_first_lesson_title_first">First:</string>
<string name="dashboard_timetable_first_lesson_title_now">Now:</string>
<string name="dashboard_timetable_second_lesson_value_end">End of lessons</string>
<string name="dashboard_timetable_second_lessons_title">Next:</string>
<string name="dashboard_timetable_third_title">Later:</string>
<plurals name="dashboard_timetable_third_value">
<item quantity="one">%1$d more lesson</item>
<item quantity="other">%1$d more lessons</item>
</plurals>
<string name="dashboard_timetable_third_time">until %1$s</string>
<string name="dashboard_timetable_no_lessons">No upcoming lessons</string>
<string name="dashboard_timetable_error">An error occurred while loading the lessons</string>
<string name="dashboard_homework_title">Homework</string>
<string name="dashboard_homework_no_homework">No homework to do</string>
<string name="dashboard_homework_error">An error occurred while loading the homework</string>
<plurals name="dashboard_homework_more">
<item quantity="one">%1$d more homework</item>
<item quantity="other">%1$d more homework</item>
</plurals>
<string name="dashboard_homework_time">due %1$s</string>
<string name="dashboard_grade_title">Last grades</string>
<string name="dashboard_grade_no_grade">No new grades</string>
<string name="dashboard_grade_error">An error occurred while loading the grades</string>
<string name="dashboard_announcements_title">School announcements</string>
<string name="dashboard_announcements_no_announcements">No current announcements</string>
<string name="dashboard_announcements_error">An error occurred while loading the announcements</string>
<plurals name="dashboard_announcements_more">
<item quantity="one">%1$d more announcement</item>
<item quantity="other">%1$d more announcements</item>
</plurals>
<string name="dashboard_exams_title">Exams</string>
<string name="dashboard_exams_no_exams">No upcoming exams</string>
<string name="dashboard_exams_error">An error occurred while loading the exams</string>
<plurals name="dashboard_exams_more">
<item quantity="one">%1$d more exam</item>
<item quantity="other">%1$d more exams</item>
</plurals>
<string name="dashboard_conferences_title">Conferences</string>
<string name="dashboard_conferences_no_conferences">No upcoming conferences</string>
<string name="dashboard_conferences_error">An error occurred while loading the conferences</string>
<plurals name="dashboard_conference_more">
<item quantity="one">%1$d more conference</item>
<item quantity="other">%1$d more conferences</item>
</plurals>
<string name="dashboard_horizontal_group_error">An error occurred while loading data</string>
<string name="dashboard_horizontal_group_no_data">None</string>
<!--Error dialog-->
<string name="dialog_error_check_update">Check for updates</string>
<string name="dialog_error_check_update_message">Before reporting a bug, check first if an update with the bug fix is available</string>
<!--Generic-->
<string name="all_content">Content</string>
<string name="all_retry">Retry</string>
<string name="all_description">Description</string>
<string name="all_no_description">No description</string>
<string name="all_teacher">Teacher</string>
<string name="all_date">Date</string>
<string name="all_entry_date">Entry date</string>
<string name="all_color">Color</string>
<string name="all_details">Details</string>
<string name="all_category">Category</string>
<string name="all_close">Close</string>
<string name="all_no_data">No data</string>
<string name="all_subject">Subject</string>
<string name="all_prev">Prev</string>
<string name="all_next">Next</string>
<string name="all_search">Search</string>
<string name="all_search_hint">Search…</string>
<string name="all_yes">Yes</string>
<string name="all_no">No</string>
<string name="all_save">Save</string>
<string name="all_title">Title</string>
<string name="all_add">Add</string>
<string name="all_copied">Copied</string>
<string name="all_undo">Undo</string>
<string name="all_change">Change</string>
<string name="all_add_to_calendar">Add to calendar</string>
<!--Timetable Widget-->
<string name="widget_timetable_no_items">No lessons</string>
<string name="widget_timetable_theme_title">Choose theme</string>
<string name="widget_timetable_theme_light">Light</string>
<string name="widget_timetable_theme_dark">Dark</string>
<string name="widget_timetable_theme_system">System Theme</string>
<!--Preferences-->
<string name="pref_view_header">App</string>
<string name="pref_view_list">Default view</string>
<string name="pref_view_grade_average_mode">Calculated average options</string>
<string name="pref_view_grade_average_force_calc">Force average calculation by app</string>
<string name="pref_view_present">Show presence</string>
<string name="pref_view_app_theme">Theme</string>
<string name="pref_view_expand_grade">Grades expanding</string>
<string name="pref_view_timetable_show_timers">Mark current lesson</string>
<string name="pref_view_timetable_show_groups">Show groups next to subjects</string>
<string name="pref_view_grade_statistics_list">Show chart list in class grades</string>
<string name="pref_view_subjects_without_grades">Show subjects without grades</string>
<string name="pref_view_grade_color_scheme">Grades color scheme</string>
<string name="pref_view_grade_sorting_mode">Subjects sorting</string>
<string name="pref_view_app_language">Language</string>
<string name="pref_notify_header">Notifications</string>
<string name="pref_notify_header_other">Other</string>
<string name="pref_notify_switch">Show notifications</string>
<string name="pref_notify_upcoming_lessons_switch">Show upcoming lesson notifications</string>
<string name="pref_notify_upcoming_lessons_persistent_switch">Make upcoming lesson notification persistent</string>
<string name="pref_notify_upcoming_lessons_persistent_summary">Turn off when notification is not showing in your watch/band</string>
<string name="pref_notify_open_system_settings">Open system notification settings</string>
<string name="pref_notify_fix_sync_issues">Fix synchronization &amp; notifications issues</string>
<string name="pref_notify_fix_sync_issues_message">Your device may have data synchronization issues and with notifications.\n\nTo fix them, you need to add Wulkanowy to the autostart and turn off battery optimization/saving in the phone settings.</string>
<string name="pref_notify_debug_switch">Show debug notifications</string>
<string name="pref_notify_disabled_summary">Synchronization is disabled</string>
<string name="pref_notify_notifications_piggyback_header">Official app notifications</string>
<string name="pref_notify_notifications_piggyback">Capture official app notifications</string>
<string name="pref_notify_notifications_piggyback_cancel_original">Remove official app notifications after capture</string>
<string name="pref_notification_piggyback_popup_title">Capture notifications</string>
<string name="pref_notification_piggyback_popup_description">With this feature you can gain a substitute of push notifications like in the official app. All you need to do is allow Wulkanowy to receive all notifications in your system settings.\n\nHow it works?\nWhen you get a notification in Dziennik VULCAN, Wulkanowy will be notified (that\'s what these extra permissions are for) and will trigger a sync so that can send its own notification.\n\nFOR ADVANCED USERS ONLY</string>
<string name="pref_notification_exact_alarm_popup_title">Upcoming lesson notifications</string>
<string name="pref_notification_exact_alarm_popup_descriptions">You must allow the Wulkanowy app to set alarms and reminders in your system settings to use this feature.</string>
<string name="pref_notification_go_to_settings">Go to settings</string>
<string name="pref_services_header">Synchronization</string>
<string name="pref_services_switch">Automatic update</string>
<string name="pref_services_suspended">Suspended on holidays</string>
<string name="pref_services_interval">Updates interval</string>
<string name="pref_services_wifi">Wi-Fi only</string>
<string name="pref_services_force_sync">Sync now</string>
<string name="pref_services_message_sync_success">Synced!</string>
<string name="pref_services_message_sync_failed">Sync failed</string>
<string name="pref_services_sync_in_progress">Sync in progress</string>
<string name="pref_services_last_full_sync_date">Last full sync: %s</string>
<string name="pref_other_grade_modifier_plus">Value of the plus</string>
<string name="pref_other_grade_modifier_minus">Value of the minus</string>
<string name="pref_other_fill_message_content">Reply with message history</string>
<string name="pref_other_optional_arithmetic_average">Show arithmetic average when no weights provided</string>
<string name="pref_ads_support_category_name">Support</string>
<string name="pref_ads_privacy_policy">Privacy Policy</string>
<string name="pref_ads_agreements">Agreements</string>
<string name="pref_ads_consent">Consent to processing of data related to ads</string>
<string name="pref_ads_show_in_app">Show ads in app</string>
<string name="pref_ads_support">Watch single ad to support project</string>
<string name="pref_ads_privacy_title">Consent to data processing</string>
<string name="pref_ads_privacy_description">To view an advertisement you must agree to the data processing terms of our Privacy Policy</string>
<string name="pref_ads_privacy_agree">Agree</string>
<string name="pref_ads_privacy_link">Privacy policy</string>
<string name="pref_ads_loading">Ad is loading</string>
<string name="pref_ads_once_per_visit">Thank you for your support, come back later for more ads</string>
<string name="pref_ads_consent_title">Can we use your data to display ads?</string>
<string name="pref_ads_consent_description">You can change your choice anytime in the app settings. We may use your data to display ads tailored to you or, using less of your data, display non-personalized ads. Please see our Privacy Policy for details</string>
<string name="pref_ads_summary_personalized">Personalized ads</string>
<string name="pref_ads_summary_non_personalized">Non-personalized ads</string>
<string name="pref_ads_over_18_years_old">I am over 18 years old</string>
<string name="pref_ads_option_personalized">Yes, personalized ads</string>
<string name="pref_ads_option_non_personalized">Yes, non-personalized ads</string>
<string name="pref_settings_advanced_title">Advanced</string>
<string name="pref_settings_appearance_title">Appearance &amp; Behavior</string>
<string name="pref_settings_notifications_title">Notifications</string>
<string name="pref_settings_sync_title">Synchronization</string>
<string name="pref_settings_ads_title">Advertisements</string>
<string name="pref_grades_appearance_header">Grades</string>
<string name="pref_dashboard_appearance_header">Dashboard</string>
<string name="pref_dashboard_appearance_tiles_title">Tiles visibility</string>
<string name="pref_attendance_appearance_view">Attendance</string>
<string name="pref_timetable_appearance_view">Timetable</string>
<string name="pref_grades_advanced_header">Grades</string>
<string name="pref_counted_average_advanced_header">Calculated average</string>
<string name="pref_messages_advanced_header">Messages</string>
<string name="pref_appearance_category">Appearance &amp; Behavior</string>
<string name="pref_appearance_category_summary">Languages, themes, subjects sorting</string>
<string name="pref_notifications_category_summary">App notifications, fix problems</string>
<string name="pref_notifications_category">Notifications</string>
<string name="pref_sync_category">Synchronization</string>
<string name="pref_sync_category_summary">Automatic update, synchronization interval</string>
<string name="pref_advanced_category_summary">Plus and minus values, average calculation</string>
<string name="pref_advanced_category">Advanced</string>
<string name="pref_about_category_summary">App version, contributors, social portals</string>
<string name="pref_ads_category_summary">Displaying advertisements, project support</string>
<!--Notification Channels-->
<string name="channel_new_grades">New grades</string>
<string name="channel_new_homework">New homework</string>
<string name="channel_new_conference">New conferences</string>
<string name="channel_new_exam">New exams</string>
<string name="channel_lucky_number">Lucky number</string>
<string name="channel_new_message">New messages</string>
<string name="channel_new_notes">New notes</string>
<string name="channel_new_school_announcement">New school announcements</string>
<string name="channel_push">Push notifications</string>
<string name="channel_upcoming_lessons">Upcoming lessons</string>
<string name="channel_debug">Debug</string>
<string name="channel_change_timetable">Timetable change</string>
<string name="channel_new_attendance">New attendance</string>
<!--Colors-->
<string name="all_black">Black</string>
<string name="all_red">Red</string>
<string name="all_blue">Blue</string>
<string name="all_green">Green</string>
<string name="all_purple">Purple</string>
<string name="all_empty_color">No color</string>
<!--Update helper-->
<string name="update_download_started">Download of updates has started…</string>
<string name="update_download_success">An update has just been downloaded.</string>
<string name="update_download_success_button">Restart</string>
<string name="update_failed">Update failed! Wulkanowy may not function properly. Consider updating</string>
<!--Errors-->
<string name="error_no_internet">No internet connection</string>
<string name="error_invalid_device_datetime">An error occurred. Check your device clock</string>
<string name="error_timeout">Connection to register failed. Servers can be overloaded. Please try again later</string>
<string name="error_login_failed">Loading data failed. Please try again later</string>
<string name="error_password_change_required">Register password change required</string>
<string name="error_service_unavailable">Maintenance underway UONET + register. Try again later</string>
<string name="error_unknown_uonet">Unknown UONET + register error. Try again later</string>
<string name="error_unknown_app">Unknown application error. Please try again later</string>
<string name="error_unknown">An unexpected error occurred</string>
<string name="error_feature_disabled">Feature disabled by your school</string>
<string name="error_feature_not_available">Feature not available. Login in a mode other than Mobile API</string>
<string name="error_field_required">This field is required</string>
</resources>

View File

@ -309,9 +309,11 @@
<string name="message_not_exists">Wiadomość nie istnieje</string>
<string name="message_required_recipients">Musisz wybrać co najmniej 1 adresata</string>
<string name="message_content_min_length">Treść wiadomości musi zawierać co najmniej 3 znaki</string>
<string name="message_chip_all_mailboxes">Wszystkie skrzynki</string>
<string name="message_chip_only_unread">Tylko nieprzeczytane</string>
<string name="message_chip_only_with_attachments">Tylko z załącznikami</string>
<string name="message_read">Przeczytana: %s</string>
<string name="message_read_by">Przeczytana przez: %1$d z %2$d osób</string>
<plurals name="message_number_item">
<item quantity="one">%1$d wiadomość</item>
<item quantity="few">%1$d wiadomości</item>
@ -339,6 +341,7 @@
<item quantity="other">%1$d wybranych</item>
</plurals>
<string name="message_messages_deleted">Wiadomości zostały usunięte</string>
<string name="message_mailbox_chooser_title">Wybierz skrzynkę</string>
<!--Note-->
<string name="note_no_items">Brak informacji o uwagach</string>
<string name="note_points">Punkty</string>

View File

@ -77,9 +77,9 @@
<string name="main_log_in">Войти</string>
<string name="main_session_expired">Сеанс истёк</string>
<string name="main_session_relogin">Сеанс истёк, авторизуйтесь снова</string>
<string name="main_support_title">Application support</string>
<string name="main_support_description">Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time</string>
<string name="main_support_positive">Enable ads</string>
<string name="main_support_title">Поддержка приложения</string>
<string name="main_support_description">Вам нравится это приложение? Поддержите его разработку, включив неинвазивную рекламу, которую можно отключить в любое время</string>
<string name="main_support_positive">Включить рекламу</string>
<!--Grade-->
<string name="grade_header">Оценка</string>
<string name="grade_semester">%d семестр</string>
@ -97,7 +97,7 @@
<string name="grade_summary_predicted_grade">Ожидаемая оценка</string>
<string name="grade_summary_calculated_average">Рассчитанная средняя оценка</string>
<string name="grade_summary_calculated_average_help_dialog_title">Как работает \"Рассчитанная средняя оценка\"?</string>
<string name="grade_summary_calculated_average_help_dialog_message">The Calculated Average is the arithmetic average calculated from the subjects averages. It allows you to know the approximate final average. It is calculated in a way selected by the user in the application settings. It is recommended that you choose the appropriate option. This is because the calculation of school averages differs. Additionally, if your school reports the average of the subjects on the Vulcan page, the application downloads them and does not calculate these averages. This can be changed by forcing the calculation of the average in the application settings.\n\n<b>Average of grades only from selected semester</b>:\n1. Calculating the weighted average for each subject in a given semester\n2.Adding calculated averages\n3. Calculation of the arithmetic average of the summed averages\n\n<b>Average of averages from both semesters</b>:\n1.Calculating the weighted average for each subject in semester 1 and 2\n2. Calculating the arithmetic average of the calculated averages for semesters 1 and 2 for each subject.\n3. Adding calculated averages\n4. Calculation of the arithmetic average of the summed averages\n\n<b>Average of grades from the whole year:</b>\n1. Calculating weighted average over the year for each subject. The final average in the 1st semester is irrelevant.\n2. Adding calculated averages\n3. Calculating the arithmetic average of summed averages</string>
<string name="grade_summary_calculated_average_help_dialog_message">Рассчитанная средняя оценка - это среднее арифметическое, рассчитанное на основе средних оценок по предметам. Это позволяет узнать приблизительную итоговую среднюю оценку. Она рассчитывается способом, выбранным пользователем в настройках приложения. Рекомендуется выбрать подходящий вариант, так как каждая школа по разному считает среднюю оценку. Кроме того, если ваша школа выставляет средние оценки по предметам на странице Vulcan, приложение просто загрузит их. Это можно изменить, заставив приложение считать среднюю оценку в настройках.\n\n<b>Средняя из оценок выбранного семестра</b>:\n1. Вычисление средневзвешенного значения по каждому предмету за семестр\n2.Суммирование вычисленных значений\n3. Вычисление среднего арифметического суммированных значений\n\n<b>Средняя из средних оценок семестров</b>:\n1.Расчет средневзвешенного значения для каждого предмета в семестрах. \n2. Вычисление среднего арифметического из средневзвешенных значений для каждого предмета в семестрах.\n3. Суммирование средних арифметических\n4. Вычисление среднего арифматического из суммированных значений\n\n<b>Средняя из оценок со всего года:</b>\n1. Расчет средневзвешенного значения по каждому предмету за год. Итоговое среднее значение за 1 семестр не имеет значения.\n2. Суммирование вычисленных средних\n3. Расчет среднего арифметического суммированных чисел</string>
<string name="grade_summary_final_average_help_dialog_title">Как работает \"Итоговая средняя оценка\"?</string>
<string name="grade_summary_final_average_help_dialog_message">Итоговая средняя оценка - это среднее арифметическое, рассчитанное из всех имеющихся на данный момент итоговых оценок в семестре.\n\nРассчет происходит следующим образом:\n1. Суммирование итоговых оценок, выставленных преподавателями\n2. Полученная сумма делится на число предметов, по которым выставлены оценки</string>
<string name="grade_summary_final_average">Итоговая средняя оценка</string>
@ -297,10 +297,10 @@
<string name="message_move_to_trash">Перенести в корзину</string>
<string name="message_delete_forever">Удалить навсегда</string>
<string name="message_delete_success">Письмо успешно удалено</string>
<string name="message_mailbox_type_student">student</string>
<string name="message_mailbox_type_parent">parent</string>
<string name="message_mailbox_type_guardian">guardian</string>
<string name="message_mailbox_type_employee">employee</string>
<string name="message_mailbox_type_student">ученик</string>
<string name="message_mailbox_type_parent">родитель</string>
<string name="message_mailbox_type_guardian">опекун</string>
<string name="message_mailbox_type_employee">работник</string>
<string name="message_share">Поделиться</string>
<string name="message_print">Печать</string>
<string name="message_subject">Тема</string>
@ -309,9 +309,11 @@
<string name="message_not_exists">Письма не существует</string>
<string name="message_required_recipients">Вы должны выбрать как минимум одного получателя</string>
<string name="message_content_min_length">Текст сообщения должен содержать как минимум 3 знака</string>
<string name="message_chip_all_mailboxes">Все почтовые ящики</string>
<string name="message_chip_only_unread">Только непрочитанные</string>
<string name="message_chip_only_with_attachments">Только с вложениями</string>
<string name="message_read">Прочитано: %s</string>
<string name="message_read_by">Прочитано: %1$d из %2$d человек</string>
<plurals name="message_number_item">
<item quantity="one">%1$d сообщение</item>
<item quantity="few">%1$d сообщения</item>
@ -339,6 +341,7 @@
<item quantity="other">%1$d выбрано</item>
</plurals>
<string name="message_messages_deleted">Сообщение удалено</string>
<string name="message_mailbox_chooser_title">Выбрать почтовый ящик</string>
<!--Note-->
<string name="note_no_items">Нет записей о замечаниях и свершениях</string>
<string name="note_points">Баллы</string>
@ -720,10 +723,10 @@
<string name="pref_other_fill_message_content">Отвечать с историей сообщений</string>
<string name="pref_other_optional_arithmetic_average">Показывать среднее арифметическое при отсутствии стоимости</string>
<string name="pref_ads_support_category_name">Поддержка</string>
<string name="pref_ads_privacy_policy">Privacy Policy</string>
<string name="pref_ads_agreements">Agreements</string>
<string name="pref_ads_consent">Consent to processing of data related to ads</string>
<string name="pref_ads_show_in_app">Show ads in app</string>
<string name="pref_ads_privacy_policy">Политика приватности</string>
<string name="pref_ads_agreements">Соглашения</string>
<string name="pref_ads_consent">Согласие на обработку данных, связанных с объявлениями</string>
<string name="pref_ads_show_in_app">Показать рекламу в приложении</string>
<string name="pref_ads_support">Посмотреть рекламу для поддержки проекта</string>
<string name="pref_ads_privacy_title">Согласие на обработку данных</string>
<string name="pref_ads_privacy_description">Для просмотра рекламы вы должны согласиться с условиями обработки данных нашей Политики конфиденциальности</string>
@ -731,13 +734,13 @@
<string name="pref_ads_privacy_link">Политика конфиденциальности</string>
<string name="pref_ads_loading">Реклама загружается</string>
<string name="pref_ads_once_per_visit">Спасибо за вашу поддержку, возвращайтесь позже для дополнительной рекламы</string>
<string name="pref_ads_consent_title">Can we use your data to display ads?</string>
<string name="pref_ads_consent_description">You can change your choice anytime in the app settings. We may use your data to display ads tailored to you or, using less of your data, display non-personalized ads. Please see our Privacy Policy for details</string>
<string name="pref_ads_summary_personalized">Personalized ads</string>
<string name="pref_ads_summary_non_personalized">Non-personalized ads</string>
<string name="pref_ads_over_18_years_old">I am over 18 years old</string>
<string name="pref_ads_option_personalized">Yes, personalized ads</string>
<string name="pref_ads_option_non_personalized">Yes, non-personalized ads</string>
<string name="pref_ads_consent_title">Можем ли мы использовать ваши данные для показа рекламы?</string>
<string name="pref_ads_consent_description">Вы можете изменить свой выбор в любое время в настройках приложения. Мы можем использовать ваши данные для показа объявлений в соответствии с вашими пожеланиями или, используя меньше данных, отображать неперсональную рекламу. Пожалуйста, ознакомьтесь с нашей политикой конфиденциальности для подробностей</string>
<string name="pref_ads_summary_personalized">Персонализированная реклама</string>
<string name="pref_ads_summary_non_personalized">Неперсонализированная реклама</string>
<string name="pref_ads_over_18_years_old">Я старше 18 лет</string>
<string name="pref_ads_option_personalized">Да, персонализировать рекламу</string>
<string name="pref_ads_option_non_personalized">Да, не персонализировать рекламу</string>
<string name="pref_settings_advanced_title">Расширенные</string>
<string name="pref_settings_appearance_title">Внешний вид и поведение</string>
<string name="pref_settings_notifications_title">Уведомления</string>

View File

@ -309,9 +309,11 @@
<string name="message_not_exists">Správa neexistuje</string>
<string name="message_required_recipients">Musíte vybrať aspoň 1 príjemca</string>
<string name="message_content_min_length">Obsah správy musí mať aspoň 3 znaky</string>
<string name="message_chip_all_mailboxes">Všetky poštové schránky</string>
<string name="message_chip_only_unread">Iba neprečítané</string>
<string name="message_chip_only_with_attachments">Iba s prílohami</string>
<string name="message_read">Prečítaná: %s</string>
<string name="message_read_by">Prečítaná cez: %1$d z %2$d osôb</string>
<plurals name="message_number_item">
<item quantity="one">%1$d správa</item>
<item quantity="few">%1$d správy</item>
@ -339,6 +341,7 @@
<item quantity="other">%1$d vybraných</item>
</plurals>
<string name="message_messages_deleted">Správy odstránené</string>
<string name="message_mailbox_chooser_title">Vyberte poštovú schránku</string>
<!--Note-->
<string name="note_no_items">Žiadne informácie o poznámkach</string>
<string name="note_points">Body</string>
@ -735,7 +738,7 @@
<string name="pref_ads_consent_description">Voľbu môžete kedykoľvek zmeniť v nastavení aplikácie. Môžeme použiť vaše údaje na zobrazenie reklám šitých pre vás alebo pomocou menej vašich dát zobrazovať neprispôsobené reklamy. Podrobnosti nájdete v našich Zásadách ochrany osobných údajov</string>
<string name="pref_ads_summary_personalized">Prispôsobené reklamy</string>
<string name="pref_ads_summary_non_personalized">Neprispôsobené reklamy</string>
<string name="pref_ads_over_18_years_old">Mám ukončené 18 rokov</string>
<string name="pref_ads_over_18_years_old">Mám viac ako 18 rokov</string>
<string name="pref_ads_option_personalized">Áno, prispôsobené reklamy</string>
<string name="pref_ads_option_non_personalized">Áno, neprispôsobené reklamy</string>
<string name="pref_settings_advanced_title">Pokročilé</string>

View File

@ -77,9 +77,9 @@
<string name="main_log_in">Увійти</string>
<string name="main_session_expired">Минув термін дії сесії</string>
<string name="main_session_relogin">Минув термін дії сесії, авторизуйтеся знову</string>
<string name="main_support_title">Application support</string>
<string name="main_support_description">Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time</string>
<string name="main_support_positive">Enable ads</string>
<string name="main_support_title">Підтримка додатку</string>
<string name="main_support_description">Вам подобається цей додаток? Підтримайте його розвиток, увімкнувши неінвазивну рекламу, яку ви можете відключити в будь-який час</string>
<string name="main_support_positive">Увімкнути рекламу</string>
<!--Grade-->
<string name="grade_header">Оцінка</string>
<string name="grade_semester">%d семестр</string>
@ -97,7 +97,7 @@
<string name="grade_summary_predicted_grade">Передбачувана оцінка</string>
<string name="grade_summary_calculated_average">Розрахована середня оцінка</string>
<string name="grade_summary_calculated_average_help_dialog_title">Як працює \"Розрахована середня оцінка\"?</string>
<string name="grade_summary_calculated_average_help_dialog_message">The Calculated Average is the arithmetic average calculated from the subjects averages. It allows you to know the approximate final average. It is calculated in a way selected by the user in the application settings. It is recommended that you choose the appropriate option. This is because the calculation of school averages differs. Additionally, if your school reports the average of the subjects on the Vulcan page, the application downloads them and does not calculate these averages. This can be changed by forcing the calculation of the average in the application settings.\n\n<b>Average of grades only from selected semester</b>:\n1. Calculating the weighted average for each subject in a given semester\n2.Adding calculated averages\n3. Calculation of the arithmetic average of the summed averages\n\n<b>Average of averages from both semesters</b>:\n1.Calculating the weighted average for each subject in semester 1 and 2\n2. Calculating the arithmetic average of the calculated averages for semesters 1 and 2 for each subject.\n3. Adding calculated averages\n4. Calculation of the arithmetic average of the summed averages\n\n<b>Average of grades from the whole year:</b>\n1. Calculating weighted average over the year for each subject. The final average in the 1st semester is irrelevant.\n2. Adding calculated averages\n3. Calculating the arithmetic average of summed averages</string>
<string name="grade_summary_calculated_average_help_dialog_message">Розрахована середня оцінка - це середнє арифметичне, обчислене з середніх оцінок з предметів. Це дозволяє дізнатися приблизну кінцеву середню оцінку. Вона розраховується спосібом, обраним користувачем у налаштуваннях програми. Рекомендується вибрати відповідний варіант, тому що кожна школа по різному розраховує середню оцінку. Крім того, якщо у вашій школі повідомляється середня оцінка з предметів на сторінці Vulcan, програма тільки завантажує ці оцінки і не розраховує їх самостійно. Це можна змінити шляхом примусового розрахунку середньоЇ оцінки в налаштуваннях програми.\n\n<b>Середні оцінки тільки за обраний семестр</b>:\n1. Розрахунок середньозваженого числа для кожного предмета в даному семестрі\n2. Сумування розрахованих числ\n3. Розрахунок середнього арифметичного з сумованих чисел\n\n<b>Середнє значення з обох семестрів</b>:\n1. Обчислення середньозваженого числа для кожного предмета у 1 та 2 семестрі\n2. Обчислення середнього арифметичного з розрахованих середньозважених числ за 1 та 2 семестри для кожного предмета.\n3. Додавання розрахованих середніх\n4. Розрахунок середнього арифметичного підсумованих середніх значень\n\n<b>Середнє значення оцінок за весь рік: </b>\n1. Розрахунок середньозваженого числа за рік для кожного предмета. Підсумковий середній показник у 1-му семестрі не має значення.\n2. Сумування розрахованих середніх\n3. Обчислення середнього арифметичного з суммованих середніх</string>
<string name="grade_summary_final_average_help_dialog_title">Як працює \"Підсумкова середня оцінка\"?</string>
<string name="grade_summary_final_average_help_dialog_message">Підсумкове середнє значення - це середнє арифметичне, обчислене з усіх наявних наразі підсумкових оцінок у даному семестрі. \n\nСхема обчислення складається з таких кроків:\n1. Сумування підсумкових оцінок, виставленних викладачами\n2. Ділення на кількість предметів, з яких виставлені ці оцінки</string>
<string name="grade_summary_final_average">Підсумкова середня оцінка</string>
@ -220,7 +220,7 @@
<string name="additional_lessons_delete_series">Всі в серії</string>
<string name="additional_lessons_start">Час початку</string>
<string name="additional_lessons_end">Час завершення</string>
<string name="additional_lessons_end_time_error">Час завершення має бути більшим, ніж час початку</string>
<string name="additional_lessons_end_time_error">Час завершення має бути пізніше часу початку</string>
<!--Attendance-->
<string name="attendance_summary_button">Підсумок відвідуваності</string>
<string name="attendance_absence_school">Відсутність зі шкільних причин</string>
@ -297,10 +297,10 @@
<string name="message_move_to_trash">Перемістити до кошика</string>
<string name="message_delete_forever">Видалити назавжди</string>
<string name="message_delete_success">Лист було успішно видалено</string>
<string name="message_mailbox_type_student">student</string>
<string name="message_mailbox_type_parent">parent</string>
<string name="message_mailbox_type_guardian">guardian</string>
<string name="message_mailbox_type_employee">employee</string>
<string name="message_mailbox_type_student">учень</string>
<string name="message_mailbox_type_parent">родич</string>
<string name="message_mailbox_type_guardian">опікун</string>
<string name="message_mailbox_type_employee">працівник</string>
<string name="message_share">Поділитись</string>
<string name="message_print">Друк</string>
<string name="message_subject">Тема</string>
@ -309,9 +309,11 @@
<string name="message_not_exists">Такого листа не існує</string>
<string name="message_required_recipients">Необхідно обрати принаймні 1 адресата</string>
<string name="message_content_min_length">Зміст листа повинен складатися принаймні з 3 знаків</string>
<string name="message_chip_all_mailboxes">Усі поштові скриньки</string>
<string name="message_chip_only_unread">Лише непрочитані</string>
<string name="message_chip_only_with_attachments">Тільки з вкладеннями</string>
<string name="message_read">Прочитаний: %s</string>
<string name="message_read_by">Прочитано: %1$d з %2$d осіб</string>
<plurals name="message_number_item">
<item quantity="one">%1$d лист</item>
<item quantity="few">%1$d листи</item>
@ -338,7 +340,8 @@
<item quantity="many">%1$d вибрано</item>
<item quantity="other">%1$d вибрано</item>
</plurals>
<string name="message_messages_deleted">Листи видалені</string>
<string name="message_messages_deleted">Листи видалено</string>
<string name="message_mailbox_chooser_title">Вибрати поштову скриньку</string>
<!--Note-->
<string name="note_no_items">Немає інформації о зауваженнях</string>
<string name="note_points">Бали</string>
@ -720,10 +723,10 @@
<string name="pref_other_fill_message_content">Відповісти з історією повідомлень</string>
<string name="pref_other_optional_arithmetic_average">Вилічити середню аритметичну, якщо оцінка немає вартості</string>
<string name="pref_ads_support_category_name">Підтримка</string>
<string name="pref_ads_privacy_policy">Privacy Policy</string>
<string name="pref_ads_agreements">Agreements</string>
<string name="pref_ads_consent">Consent to processing of data related to ads</string>
<string name="pref_ads_show_in_app">Show ads in app</string>
<string name="pref_ads_privacy_policy">Політика конфіденційності</string>
<string name="pref_ads_agreements">Угоди</string>
<string name="pref_ads_consent">Згода на обробку даних, пов\'язаних з рекламою</string>
<string name="pref_ads_show_in_app">Показувати рекламу в додатку</string>
<string name="pref_ads_support">Подивіться одну рекламу для підтримки проєкту</string>
<string name="pref_ads_privacy_title">Згода в обробці даних</string>
<string name="pref_ads_privacy_description">Щоб переглянути рекламу, ви повинні погодитися з умовами обробки даних нашої Політики конфіденційності</string>
@ -731,13 +734,13 @@
<string name="pref_ads_privacy_link">Політика конфіденційності</string>
<string name="pref_ads_loading">Реклама завантажується</string>
<string name="pref_ads_once_per_visit">Дякуємо за вашу підтримку, повертайтеся пізніше для більшої кількості реклам</string>
<string name="pref_ads_consent_title">Can we use your data to display ads?</string>
<string name="pref_ads_consent_description">You can change your choice anytime in the app settings. We may use your data to display ads tailored to you or, using less of your data, display non-personalized ads. Please see our Privacy Policy for details</string>
<string name="pref_ads_summary_personalized">Personalized ads</string>
<string name="pref_ads_summary_non_personalized">Non-personalized ads</string>
<string name="pref_ads_over_18_years_old">I am over 18 years old</string>
<string name="pref_ads_option_personalized">Yes, personalized ads</string>
<string name="pref_ads_option_non_personalized">Yes, non-personalized ads</string>
<string name="pref_ads_consent_title">Чи можемо ми використовувати ваші дані для висвітлювання реклами?</string>
<string name="pref_ads_consent_description">Ви можете змінити свій вибір в будь-який час в налаштуваннях додатку. Ми можемо використовувати ваші дані для висвітлювання реклами, адаптованої до вас або, використовуючи менше ваших даних, висвітлювати неперсоналізовану рекламу. Перегляньте нашу Політику конфіденційності для подробиць</string>
<string name="pref_ads_summary_personalized">Персоналізована реклама</string>
<string name="pref_ads_summary_non_personalized">Неперсоналізована реклама</string>
<string name="pref_ads_over_18_years_old">Мені більше 18 років</string>
<string name="pref_ads_option_personalized">Так, персоналізована реклама</string>
<string name="pref_ads_option_non_personalized">Так, неперсоналізована реклама</string>
<string name="pref_settings_advanced_title">Додатково</string>
<string name="pref_settings_appearance_title">Вигляд та поведінка</string>
<string name="pref_settings_notifications_title">Сповіщення</string>

View File

@ -26,15 +26,11 @@
<string name="student_info_title">Student info</string>
<string name="dashboard_title">Dashboard</string>
<string name="notifications_center_title">Notifications center</string>
<!--Subtitles-->
<string name="grade_subtitle">Semester %1$d, %2$d/%3$d</string>
<!--Login-->
<string name="login_header_default">Sign in with the student or parent account</string>
<string name="login_header_symbol">Enter the symbol from the register page for account: &lt;b>%1$s&lt;/b></string>
<string name="login_header_symbol">Enter the symbol from the register page for account: &lt;b&gt;%1$s&lt;/b&gt;</string>
<string name="login_nickname_hint">Username</string>
<string name="login_email_hint">Email</string>
<string name="login_login_pesel_email_hint">Login, PESEL or e-mail</string>
@ -71,15 +67,13 @@
<string name="login_contact_discord">Discord</string>
<string name="login_email_intent_title">Send email</string>
<string name="login_email_subject" translatable="false">Zgłoszenie: Problemy z logowaniem</string>
<string name="login_email_text" translatable="false">Informacje o aplikacji:\n\nUrządzenie: %1$s\nWersja SDK: %2$s\nWersja aplikacji: %3$s\nDodatkowe informacje: %4$s\nOstatni błąd: %5$s\n\nNazwa szkoły i miejscowość: </string>
<string name="login_email_text" translatable="false">Informacje o aplikacji:\n\nUrządzenie: %1$s\nWersja SDK: %2$s\nWersja aplikacji: %3$s\nDodatkowe informacje: %4$s\nIdentyfikator instalacji: %5$s\nOstatni błąd: %6$s\n\nNazwa szkoły i miejscowość: </string>
<string name="login_recover_warning">Make sure you select the correct UONET+ register variation!</string>
<string name="login_recover_button">I forgot my password</string>
<string name="login_recover_title">Recover your account</string>
<string name="login_recover">Recover</string>
<string name="login_signed_in">Student is already signed in</string>
<string name="login_host_standard">Standard</string>
<!--Main-->
<string name="main_account_picker">Account manager</string>
<string name="main_log_in">Log in</string>
@ -88,8 +82,6 @@
<string name="main_support_title">Application support</string>
<string name="main_support_description">Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time</string>
<string name="main_support_positive">Enable ads</string>
<!--Grade-->
<string name="grade_header">Grade</string>
<string name="grade_semester">Semester %d</string>
@ -152,8 +144,6 @@
<item quantity="one">You received %1$d final grade</item>
<item quantity="other">You received %1$d final grades</item>
</plurals>
<!--Timetable-->
<string name="timetable_lesson">Lesson</string>
<string name="timetable_room">Room</string>
@ -189,8 +179,6 @@
<item quantity="one">%d change</item>
<item quantity="other">%d changes</item>
</plurals>
<!--Completed lessons-->
<string name="completed_lessons_title">Completed lessons</string>
<string name="completed_lessons_button">Show completed lessons</string>
@ -198,8 +186,6 @@
<string name="completed_lessons_topic">Topic</string>
<string name="completed_lessons_absence">Absence</string>
<string name="completed_lessons_resources">Resources</string>
<!--Additional lessons-->
<string name="additional_lessons_title">Additional lessons</string>
<string name="additional_lessons_button">Show additional lessons</string>
@ -215,8 +201,6 @@
<string name="additional_lessons_start">Start time</string>
<string name="additional_lessons_end">End time</string>
<string name="additional_lessons_end_time_error">End time must be greater than start time</string>
<!--Attendance-->
<string name="attendance_summary_button">Attendance summary</string>
<string name="attendance_absence_school">Absent for school reasons</string>
@ -249,12 +233,8 @@
<item quantity="one">%d attendance</item>
<item quantity="other">%d attendance</item>
</plurals>
<!--Attendance summary-->
<string name="attendance_summary_total">Total</string>
<!--Exam-->
<string name="exam_no_items">No exams this week</string>
<string name="exam_type">Type</string>
@ -271,8 +251,6 @@
<item quantity="one">%d exam</item>
<item quantity="other">%d exams</item>
</plurals>
<!--Message-->
<string name="message_inbox">Inbox</string>
<string name="message_sent">Sent</string>
@ -301,9 +279,11 @@
<string name="message_not_exists">Message does not exist</string>
<string name="message_required_recipients">You need to choose at least 1 recipient</string>
<string name="message_content_min_length">The message content must be at least 3 characters</string>
<string name="message_chip_all_mailboxes">All mailboxes</string>
<string name="message_chip_only_unread">Only unread</string>
<string name="message_chip_only_with_attachments">Only with attachments</string>
<string name="message_read">Read: %s</string>
<string name="message_read_by">Read by: %1$d of %2$d people</string>
<plurals name="message_number_item">
<item quantity="one">%1$d message</item>
<item quantity="other">%1$d messages</item>
@ -323,8 +303,7 @@
<item quantity="other">%1$d selected</item>
</plurals>
<string name="message_messages_deleted">Messages deleted</string>
<string name="message_mailbox_chooser_title">Choose mailbox</string>
<!--Note-->
<string name="note_no_items">No info about notes</string>
<string name="note_points">Points</string>
@ -340,7 +319,6 @@
<item quantity="one">You received %1$d note</item>
<item quantity="other">You received %1$d notes</item>
</plurals>
<!--Praise-->
<plurals name="praise_number_item">
<item quantity="one">%d praise</item>
@ -354,7 +332,6 @@
<item quantity="one">You received %1$d praise</item>
<item quantity="other">You received %1$d praises</item>
</plurals>
<!--Neutral notes-->
<plurals name="neutral_note_number_item">
<item quantity="one">%d neutral note</item>
@ -368,8 +345,6 @@
<item quantity="one">You received %1$d neutral note</item>
<item quantity="other">You received %1$d neutral notes</item>
</plurals>
<!--Homework-->
<string name="homework_no_items">No info about homework</string>
<string name="homework_mark_as_done">Mark as done</string>
@ -390,8 +365,6 @@
<item quantity="one">%d homework</item>
<item quantity="other">%d homework</item>
</plurals>
<!--Lucky number-->
<string name="lucky_number_title">Lucky number</string>
<string name="lucky_number_header">Today\'s lucky number is</string>
@ -399,13 +372,9 @@
<string name="lucky_number_notify_new_item_title">Lucky number for today</string>
<string name="lucky_number_notify_new_item">Today\'s lucky number is: %s</string>
<string name="lucky_number_history_button">Show history</string>
<!--Lucky number history-->
<string name="lucky_number_history_title">Lucky number history</string>
<string name="lucky_number_history_empty">No info about lucky numbers</string>
<!--Mobile devices-->
<string name="mobile_devices_title">Mobile devices</string>
<string name="mobile_devices_no_items">No devices</string>
@ -415,12 +384,8 @@
<string name="mobile_device_token">Token</string>
<string name="mobile_device_symbol">Symbol</string>
<string name="mobile_device_pin">PIN</string>
<!--School and teachers-->
<string name="schoolandteachers_title">School and teachers</string>
<!--School-->
<string name="school_title">School</string>
<string name="school_no_info">No info about school</string>
@ -431,14 +396,10 @@
<string name="school_pedagogue">Name of pedagogue</string>
<string name="school_address_button">Show on map</string>
<string name="school_telephone_button">Call</string>
<!--Teacher-->
<string name="teachers_title">Teachers</string>
<string name="teacher_no_items">No info about teachers</string>
<string name="teacher_no_subject">No subject</string>
<!--Conference-->
<string name="conferences_title">Conferences</string>
<string name="conference_no_items">No info about conferences</string>
@ -456,7 +417,6 @@
</plurals>
<string name="conferences_present">Present at conference</string>
<string name="conference_agenda">Agenda</string>
<!--Director information-->
<string name="school_announcement_title">School announcements</string>
<string name="school_announcement_no_items">No school announcements</string>
@ -472,8 +432,6 @@
<item quantity="one">You have %1$d new school announcement</item>
<item quantity="other">You have %1$d new school announcements</item>
</plurals>
<!--Account-->
<string name="account_add_new">Add account</string>
<string name="account_logout">Logout</string>
@ -488,8 +446,6 @@
<string name="account_contact">Contact</string>
<string name="account_address">Residence details</string>
<string name="account_personal_data">Personal information</string>
<!--About-->
<string name="about_version">App version</string>
<string name="about_contributor">Contributors</string>
@ -512,18 +468,12 @@
<string name="about_homepage_summary">Visit the website and help develop the application</string>
<string name="about_licenses">Licenses</string>
<string name="about_licenses_summary">Licenses of libraries used in the application</string>
<string name="about_feedback_template" translatable="false">Informacje o aplikacji:\n\nUrządzenie: %1$s\nWersja SDK: %2$s\nWersja aplikacji: %3$s\n\nTreść zgłoszenia:</string>
<string name="about_feedback_template" translatable="false">Informacje o aplikacji:\n\nUrządzenie: %1$s\nWersja SDK: %2$s\nWersja aplikacji: %3$s\nIdentyfikator instalacji: %4$s\nTreść zgłoszenia:</string>
<!--Licenses-->
<string name="license_dialog_title">License</string>
<!--Contributor-->
<string name="contributor_avatar_description">Avatar</string>
<string name="contributor_see_more">See more on GitHub</string>
<!--Student info-->
<string name="student_info_empty">No info about student or student family</string>
<string name="student_info_first_name">Name</string>
@ -546,19 +496,13 @@
<string name="student_info_female">Female</string>
<string name="student_info_last_name">Last name</string>
<string name="student_info_guardian">Guardian</string>
<!--Account edit-->
<string name="account_edit_nick_hint">Nick</string>
<string name="account_edit_header">Add nick</string>
<string name="account_edit_avatar_title">Choose avatar color</string>
<!--Log viewer-->
<string name="logviewer_share">Share logs</string>
<string name="logviewer_refresh">Refresh</string>
<!--Dashboard-->
<string name="dashboard_timetable_title">Lessons</string>
<string name="dashboard_timetable_title_tomorrow">(Tomorrow)</string>
@ -577,7 +521,6 @@
<string name="dashboard_timetable_third_time">until %1$s</string>
<string name="dashboard_timetable_no_lessons">No upcoming lessons</string>
<string name="dashboard_timetable_error">An error occurred while loading the lessons</string>
<string name="dashboard_homework_title">Homework</string>
<string name="dashboard_homework_no_homework">No homework to do</string>
<string name="dashboard_homework_error">An error occurred while loading the homework</string>
@ -586,11 +529,9 @@
<item quantity="other">%1$d more homework</item>
</plurals>
<string name="dashboard_homework_time">due %1$s</string>
<string name="dashboard_grade_title">Last grades</string>
<string name="dashboard_grade_no_grade">No new grades</string>
<string name="dashboard_grade_error">An error occurred while loading the grades</string>
<string name="dashboard_announcements_title">School announcements</string>
<string name="dashboard_announcements_no_announcements">No current announcements</string>
<string name="dashboard_announcements_error">An error occurred while loading the announcements</string>
@ -598,7 +539,6 @@
<item quantity="one">%1$d more announcement</item>
<item quantity="other">%1$d more announcements</item>
</plurals>
<string name="dashboard_exams_title">Exams</string>
<string name="dashboard_exams_no_exams">No upcoming exams</string>
<string name="dashboard_exams_error">An error occurred while loading the exams</string>
@ -606,7 +546,6 @@
<item quantity="one">%1$d more exam</item>
<item quantity="other">%1$d more exams</item>
</plurals>
<string name="dashboard_conferences_title">Conferences</string>
<string name="dashboard_conferences_no_conferences">No upcoming conferences</string>
<string name="dashboard_conferences_error">An error occurred while loading the conferences</string>
@ -614,16 +553,11 @@
<item quantity="one">%1$d more conference</item>
<item quantity="other">%1$d more conferences</item>
</plurals>
<string name="dashboard_horizontal_group_error">An error occurred while loading data</string>
<string name="dashboard_horizontal_group_no_data">None</string>
<!--Error dialog-->
<string name="dialog_error_check_update">Check for updates</string>
<string name="dialog_error_check_update_message">Before reporting a bug, check first if an update with the bug fix is available</string>
<!--Generic-->
<string name="all_content">Content</string>
<string name="all_retry">Retry</string>
@ -651,16 +585,12 @@
<string name="all_undo">Undo</string>
<string name="all_change">Change</string>
<string name="all_add_to_calendar">Add to calendar</string>
<!--Timetable Widget-->
<string name="widget_timetable_no_items">No lessons</string>
<string name="widget_timetable_theme_title">Choose theme</string>
<string name="widget_timetable_theme_light">Light</string>
<string name="widget_timetable_theme_dark">Dark</string>
<string name="widget_timetable_theme_system">System Theme</string>
<!--Preferences-->
<string name="pref_view_header">App</string>
<string name="pref_view_list">Default view</string>
@ -676,7 +606,6 @@
<string name="pref_view_grade_color_scheme">Grades color scheme</string>
<string name="pref_view_grade_sorting_mode">Subjects sorting</string>
<string name="pref_view_app_language">Language</string>
<string name="pref_notify_header">Notifications</string>
<string name="pref_notify_header_other">Other</string>
<string name="pref_notify_switch">Show notifications</string>
@ -696,7 +625,6 @@
<string name="pref_notification_exact_alarm_popup_title">Upcoming lesson notifications</string>
<string name="pref_notification_exact_alarm_popup_descriptions">You must allow the Wulkanowy app to set alarms and reminders in your system settings to use this feature.</string>
<string name="pref_notification_go_to_settings">Go to settings</string>
<string name="pref_services_header">Synchronization</string>
<string name="pref_services_switch">Automatic update</string>
<string name="pref_services_suspended">Suspended on holidays</string>
@ -707,12 +635,10 @@
<string name="pref_services_message_sync_failed">Sync failed</string>
<string name="pref_services_sync_in_progress">Sync in progress</string>
<string name="pref_services_last_full_sync_date">Last full sync: %s</string>
<string name="pref_other_grade_modifier_plus">Value of the plus</string>
<string name="pref_other_grade_modifier_minus">Value of the minus</string>
<string name="pref_other_fill_message_content">Reply with message history</string>
<string name="pref_other_optional_arithmetic_average">Show arithmetic average when no weights provided</string>
<string name="pref_ads_support_category_name">Support</string>
<string name="pref_ads_privacy_policy">Privacy Policy</string>
<string name="pref_ads_agreements">Agreements</string>
@ -732,13 +658,11 @@
<string name="pref_ads_over_18_years_old">I am over 18 years old</string>
<string name="pref_ads_option_personalized">Yes, personalized ads</string>
<string name="pref_ads_option_non_personalized">Yes, non-personalized ads</string>
<string name="pref_settings_advanced_title">Advanced</string>
<string name="pref_settings_appearance_title">Appearance &amp; Behavior</string>
<string name="pref_settings_notifications_title">Notifications</string>
<string name="pref_settings_sync_title">Synchronization</string>
<string name="pref_settings_ads_title">Advertisements</string>
<string name="pref_grades_appearance_header">Grades</string>
<string name="pref_dashboard_appearance_header">Dashboard</string>
<string name="pref_dashboard_appearance_tiles_title">Tiles visibility</string>
@ -747,7 +671,6 @@
<string name="pref_grades_advanced_header">Grades</string>
<string name="pref_counted_average_advanced_header">Calculated average</string>
<string name="pref_messages_advanced_header">Messages</string>
<string name="pref_appearance_category">Appearance &amp; Behavior</string>
<string name="pref_appearance_category_summary">Languages, themes, subjects sorting</string>
<string name="pref_notifications_category_summary">App notifications, fix problems</string>
@ -758,8 +681,6 @@
<string name="pref_advanced_category">Advanced</string>
<string name="pref_about_category_summary">App version, contributors, social portals</string>
<string name="pref_ads_category_summary">Displaying advertisements, project support</string>
<!--Notification Channels-->
<string name="channel_new_grades">New grades</string>
<string name="channel_new_homework">New homework</string>
@ -774,8 +695,6 @@
<string name="channel_debug">Debug</string>
<string name="channel_change_timetable">Timetable change</string>
<string name="channel_new_attendance">New attendance</string>
<!--Colors-->
<string name="all_black">Black</string>
<string name="all_red">Red</string>
@ -783,15 +702,11 @@
<string name="all_green">Green</string>
<string name="all_purple">Purple</string>
<string name="all_empty_color">No color</string>
<!--Update helper-->
<string name="update_download_started">Download of updates has started…</string>
<string name="update_download_success">An update has just been downloaded.</string>
<string name="update_download_success_button">Restart</string>
<string name="update_failed">Update failed! Wulkanowy may not function properly. Consider updating</string>
<!--Errors-->
<string name="error_no_internet">No internet connection</string>
<string name="error_invalid_device_datetime">An error occurred. Check your device clock</string>

View File

@ -31,13 +31,22 @@ class AdsPresenter @Inject constructor(
view?.showLoadingSupportAd(true)
presenterScope.launch {
runCatching { adsHelper.getSupportAd() }
.onFailure(errorHandler::dispatch)
.onSuccess { it?.let { view?.showAd(it) } }
.onFailure {
errorHandler.dispatch(it)
view?.run {
showLoadingSupportAd(false)
showWatchAdOncePerVisit(true)
}
view?.run {
showLoadingSupportAd(false)
showWatchAdOncePerVisit(false)
}
}
.onSuccess {
it?.let { view?.showAd(it) }
view?.run {
showLoadingSupportAd(false)
showWatchAdOncePerVisit(true)
}
}
}
}

View File

@ -1,8 +1,12 @@
package io.github.wulkanowy.utils
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.core.content.getSystemService
import com.google.ads.mediation.admob.AdMobAdapter
import com.google.android.gms.ads.*
import com.google.android.gms.ads.rewardedinterstitial.RewardedInterstitialAd
@ -10,6 +14,7 @@ import com.google.android.gms.ads.rewardedinterstitial.RewardedInterstitialAdLoa
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.BuildConfig
import io.github.wulkanowy.data.repositories.PreferencesRepository
import java.net.UnknownHostException
import javax.inject.Inject
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
@ -28,6 +33,10 @@ class AdsHelper @Inject constructor(
}
suspend fun getSupportAd(): RewardedInterstitialAd? {
if (!context.isInternetConnected()) {
throw UnknownHostException()
}
val extra = Bundle().apply { putString("npa", "1") }
val adRequest = AdRequest.Builder()
.apply {
@ -84,4 +93,18 @@ class AdsHelper @Inject constructor(
}
}
@Suppress("DEPRECATION")
private fun Context.isInternetConnected(): Boolean {
val connectivityManager = getSystemService<ConnectivityManager>() ?: return false
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val currentNetwork = connectivityManager.activeNetwork
val networkCapabilities = connectivityManager.getNetworkCapabilities(currentNetwork)
networkCapabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) == true
} else {
connectivityManager.activeNetworkInfo?.isConnected == true
}
}
data class AdBanner(val view: View)

View File

@ -4,25 +4,37 @@ import android.app.Activity
import android.content.Context
import android.os.Bundle
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.crashlytics.FirebaseCrashlytics
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.repositories.PreferencesRepository
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AnalyticsHelper @Inject constructor(
@ApplicationContext private val context: Context
@ApplicationContext private val context: Context,
preferencesRepository: PreferencesRepository,
appInfo: AppInfo,
) {
private val analytics by lazy { FirebaseAnalytics.getInstance(context) }
private val crashlytics by lazy { FirebaseCrashlytics.getInstance() }
init {
if (!appInfo.isDebug) {
crashlytics.setUserId(preferencesRepository.installationId)
}
}
fun logEvent(name: String, vararg params: Pair<String, Any?>) {
Bundle().apply {
params.forEach {
if (it.second == null) return@forEach
when (it.second) {
is String, is String? -> putString(it.first, it.second.toString())
is Int, is Int? -> putInt(it.first, it.second as Int)
is Boolean, is Boolean? -> putBoolean(it.first, it.second as Boolean)
params.forEach { (key, value) ->
if (value == null) return@forEach
when (value) {
is String -> putString(key, value)
is Int -> putInt(key, value)
is Boolean -> putBoolean(key, value)
}
}
analytics.logEvent(name, this)

View File

@ -23,9 +23,6 @@ class CrashLogExceptionTree : FormatterPriorityTree(Log.ERROR, ExceptionFilter)
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
if (skipLog(priority, tag, message, t)) return
crashlytics.setCustomKey("priority", priority)
crashlytics.setCustomKey("tag", tag.orEmpty())
crashlytics.setCustomKey("message", message)
if (t != null) {
crashlytics.recordException(t)
} else {

View File

@ -27,7 +27,9 @@ fun getMailboxEntity() = Mailbox(
globalKey = "v4",
fullName = "",
userName = "",
userLoginId = 0,
email = "test",
symbol = "powiatwulkanowy",
schoolId = "123456",
studentName = "",
schoolNameShort = "",
type = MailboxType.UNKNOWN,

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.data.repositories
import android.content.Context
import io.github.wulkanowy.data.dataOrNull
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.db.dao.MailboxDao
import io.github.wulkanowy.data.db.dao.MessageAttachmentDao
import io.github.wulkanowy.data.db.dao.MessagesDao
import io.github.wulkanowy.data.db.entities.Message
@ -10,6 +11,7 @@ import io.github.wulkanowy.data.db.entities.MessageWithAttachment
import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.errorOrNull
import io.github.wulkanowy.data.toFirstResult
import io.github.wulkanowy.domain.messages.GetMailboxByStudentUseCase
import io.github.wulkanowy.getMailboxEntity
import io.github.wulkanowy.getStudentEntity
import io.github.wulkanowy.sdk.Sdk
@ -55,6 +57,12 @@ class MessageRepositoryTest {
@MockK
private lateinit var sharedPrefProvider: SharedPrefProvider
@MockK
private lateinit var mailboxDao: MailboxDao
@MockK
private lateinit var getMailboxByStudentUseCase: GetMailboxByStudentUseCase
private val student = getStudentEntity()
private val mailbox = getMailboxEntity()
@ -74,26 +82,33 @@ class MessageRepositoryTest {
refreshHelper = refreshHelper,
sharedPrefProvider = sharedPrefProvider,
json = Json,
mailboxDao = mailboxDao,
getMailboxByStudentUseCase = getMailboxByStudentUseCase,
)
}
@Test
fun `get messages when fetched completely new message without notify`() = runBlocking {
every { messageDb.loadAll(any(), any()) } returns flowOf(emptyList())
coEvery { mailboxDao.loadAll(any()) } returns listOf(mailbox)
every { messageDb.loadAll(mailbox.globalKey, any()) } returns flowOf(emptyList())
coEvery { sdk.getMessages(Folder.RECEIVED, any()) } returns listOf(
getMessageDto()
getMessageDto().copy(
unreadBy = 5,
readBy = 10,
)
)
coEvery { messageDb.deleteAll(any()) } just Runs
coEvery { messageDb.insertAll(any()) } returns listOf()
repository.getMessages(
val res = repository.getMessages(
student = student,
mailbox = mailbox,
folder = MessageFolder.RECEIVED,
forceRefresh = true,
notify = false,
).toFirstResult().dataOrNull.orEmpty()
).toFirstResult()
assertEquals(null, res.errorOrNull)
coVerify(exactly = 1) { messageDb.deleteAll(withArg { checkEquals(emptyList<Message>()) }) }
coVerify {
messageDb.insertAll(withArg {
@ -187,13 +202,16 @@ class MessageRepositoryTest {
) = Message(
messageGlobalKey = "v4",
mailboxKey = "",
email = "",
correspondents = "",
messageId = messageId,
subject = "",
date = Instant.EPOCH,
folderId = 1,
unread = unread,
hasAttachments = false
readBy = 1,
unreadBy = 1,
hasAttachments = false,
).apply {
this.content = content
}
@ -209,6 +227,8 @@ class MessageRepositoryTest {
dateZoned = Instant.EPOCH.atZone(ZoneOffset.UTC),
folderId = 1,
unread = true,
readBy = 1,
unreadBy = 1,
hasAttachments = false,
)
}

View File

@ -1,65 +1,52 @@
package io.github.wulkanowy.data.repositories
package io.github.wulkanowy.domain
import io.github.wulkanowy.data.db.dao.MailboxDao
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.MailboxType
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.domain.messages.GetMailboxByStudentUseCase
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.mockk.MockKAnnotations
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.impl.annotations.MockK
import io.mockk.impl.annotations.SpyK
import io.mockk.just
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import java.time.Instant
import kotlin.test.assertEquals
import kotlin.test.assertNull
@OptIn(ExperimentalCoroutinesApi::class)
class MailboxRepositoryTest {
@SpyK
private var sdk = Sdk()
class GetMailboxByStudentUseCaseTest {
@MockK
private lateinit var mailboxDao: MailboxDao
@MockK
private lateinit var refreshHelper: AutoRefreshHelper
private lateinit var systemUnderTest: MailboxRepository
private lateinit var systemUnderTest: GetMailboxByStudentUseCase
@Before
fun setUp() {
MockKAnnotations.init(this)
coEvery { refreshHelper.shouldBeRefreshed(any()) } returns false
coEvery { refreshHelper.updateLastRefreshTimestamp(any()) } just Runs
coEvery { mailboxDao.deleteAll(any()) } just Runs
coEvery { mailboxDao.insertAll(any()) } returns emptyList()
coEvery { mailboxDao.loadAll(any()) } returns emptyList()
coEvery { sdk.getMailboxes() } returns emptyList()
systemUnderTest = MailboxRepository(
mailboxDao = mailboxDao,
sdk = sdk,
refreshHelper = refreshHelper,
)
systemUnderTest = GetMailboxByStudentUseCase(mailboxDao = mailboxDao)
}
@Test(expected = IllegalArgumentException::class)
@Test
fun `get mailbox that doesn't exist`() = runTest {
val student = getStudentEntity(
userName = "Stanisław Kowalski",
studentName = "Jan Kowalski",
)
coEvery { sdk.getMailboxes() } returns emptyList()
coEvery { mailboxDao.loadAll(any()) } returns emptyList()
systemUnderTest.getMailbox(student)
assertNull(systemUnderTest(student))
}
@Test
@ -73,7 +60,7 @@ class MailboxRepositoryTest {
expectedMailbox,
)
val selectedMailbox = systemUnderTest.getMailbox(student)
val selectedMailbox = systemUnderTest(student)
assertEquals(expectedMailbox, selectedMailbox)
}
@ -88,7 +75,7 @@ class MailboxRepositoryTest {
expectedMailbox,
)
assertEquals(expectedMailbox, systemUnderTest.getMailbox(student))
assertEquals(expectedMailbox, systemUnderTest(student))
}
@Test
@ -102,10 +89,10 @@ class MailboxRepositoryTest {
expectedMailbox,
)
assertEquals(expectedMailbox, systemUnderTest.getMailbox(student))
assertEquals(expectedMailbox, systemUnderTest(student))
}
@Test(expected = IllegalArgumentException::class)
@Test
fun `get mailbox for not-unique non-authorized student`() = runTest {
val student = getStudentEntity(
userName = "Stanisław Kowalski",
@ -116,7 +103,7 @@ class MailboxRepositoryTest {
getMailboxEntity("Jan Kurowski"),
)
systemUnderTest.getMailbox(student)
assertNull(systemUnderTest(student))
}
@Test
@ -130,7 +117,7 @@ class MailboxRepositoryTest {
expectedMailbox,
)
assertEquals(expectedMailbox, systemUnderTest.getMailbox(student))
assertEquals(expectedMailbox, systemUnderTest(student))
}
@Test
@ -144,7 +131,7 @@ class MailboxRepositoryTest {
expectedMailbox,
)
assertEquals(expectedMailbox, systemUnderTest.getMailbox(student))
assertEquals(expectedMailbox, systemUnderTest(student))
}
@Test
@ -158,7 +145,7 @@ class MailboxRepositoryTest {
expectedMailbox,
)
assertEquals(expectedMailbox, systemUnderTest.getMailbox(student))
assertEquals(expectedMailbox, systemUnderTest(student))
}
private fun getMailboxEntity(
@ -167,7 +154,9 @@ class MailboxRepositoryTest {
globalKey = "",
fullName = "",
userName = "",
userLoginId = 123,
email = "",
schoolId = "",
symbol = "",
studentName = studentName,
schoolNameShort = "",
type = MailboxType.STUDENT,

View File

@ -1,8 +1,8 @@
buildscript {
ext {
kotlin_version = '1.7.10'
about_libraries = '10.4.0'
hilt_version = "2.43.2"
kotlin_version = '1.7.21'
about_libraries = '10.5.1'
hilt_version = "2.44.1"
}
repositories {
mavenCentral()
@ -13,14 +13,14 @@ buildscript {
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath 'com.android.tools.build:gradle:7.2.2'
classpath 'com.android.tools.build:gradle:7.3.1'
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
classpath 'com.google.gms:google-services:4.3.13'
classpath 'com.huawei.agconnect:agcp:1.7.1.300'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.1'
classpath 'com.google.gms:google-services:4.3.14'
classpath 'com.huawei.agconnect:agcp:1.7.3.301'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.2'
classpath "com.github.triplet.gradle:play-publisher:3.6.0"
classpath "ru.cian:huawei-publish-gradle-plugin:1.3.4"
classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.4.0.2513"
classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.5.0.2730"
classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0"
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libraries"
}