Merge branch 'release/1.9.0'

This commit is contained in:
Mikołaj Pich 2023-01-01 21:57:47 +01:00
commit 9a8fb593c0
105 changed files with 2478 additions and 578 deletions

View File

@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2022 Wulkanowy
Copyright 2023 Wulkanowy
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -16,15 +16,15 @@ apply from: 'hooks.gradle'
android {
namespace 'io.github.wulkanowy'
compileSdkVersion 32
compileSdkVersion 33
defaultConfig {
applicationId "io.github.wulkanowy"
testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21
targetSdkVersion 32
versionCode 118
versionName "1.8.3"
targetSdkVersion 33
versionCode 119
versionName "1.9.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "app_name", "Wulkanowy"
@ -162,7 +162,7 @@ play {
track = 'production'
releaseStatus = com.github.triplet.gradle.androidpublisher.ReleaseStatus.IN_PROGRESS
userFraction = 0.10d
updatePriority = 5
updatePriority = 1
enabled.set(false)
}
@ -181,23 +181,23 @@ ext {
android_hilt = "1.0.0"
room = "2.4.3"
chucker = "3.5.2"
mockk = "1.13.2"
mockk = "1.13.3"
coroutines = "1.6.4"
}
dependencies {
implementation "io.github.wulkanowy:sdk:1.8.3"
implementation "io.github.wulkanowy:sdk:1.9.0"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.8'
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-ktx:1.9.0"
implementation 'androidx.core:core-splashscreen:1.0.0'
implementation "androidx.activity:activity-ktx:1.5.1"
implementation "androidx.activity:activity-ktx:1.6.1"
implementation "androidx.appcompat:appcompat:1.5.1"
implementation "androidx.fragment:fragment-ktx:1.5.4"
implementation "androidx.fragment:fragment-ktx:1.5.5"
implementation "androidx.annotation:annotation:1.5.0"
implementation "androidx.preference:preference-ktx:1.2.0"
@ -237,20 +237,21 @@ dependencies {
implementation 'com.github.bastienpaulfr:Treessence:1.0.5'
implementation "com.mikepenz:aboutlibraries-core:$about_libraries"
implementation "io.coil-kt:coil:2.2.2"
implementation "io.github.wulkanowy:AppKillerManager:3.0.0"
implementation "io.github.wulkanowy:AppKillerManager:3.0.1"
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
implementation 'com.fredporciuncula:flow-preferences:1.8.0'
implementation 'org.apache.commons:commons-text:1.10.0'
playImplementation platform('com.google.firebase:firebase-bom:31.0.3')
playImplementation platform('com.google.firebase:firebase-bom:31.1.1')
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.3.0'
playImplementation 'com.google.android.gms:play-services-ads:21.4.0'
hmsImplementation 'com.huawei.hms:hianalytics:6.8.0.300'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.7.3.300'
hmsImplementation 'com.huawei.hms:hianalytics:6.9.0.301'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.7.3.302'
releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker"
@ -263,7 +264,7 @@ dependencies {
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines"
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
testImplementation 'org.robolectric:robolectric:4.9'
testImplementation 'org.robolectric:robolectric:4.9.1'
testImplementation "androidx.test:runner:1.5.1"
testImplementation "androidx.test.ext:junit:1.1.4"
testImplementation "androidx.test:core:1.5.0"
@ -271,9 +272,9 @@ dependencies {
testImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
kaptTest "com.google.dagger:hilt-android-compiler:$hilt_version"
androidTestImplementation "androidx.test:core:1.4.0"
androidTestImplementation "androidx.test:runner:1.4.0"
androidTestImplementation "androidx.test.ext:junit:1.1.3"
androidTestImplementation "androidx.test:core:1.5.0"
androidTestImplementation "androidx.test:runner:1.5.1"
androidTestImplementation "androidx.test.ext:junit:1.1.4"
androidTestImplementation "io.mockk:mockk-android:$mockk"
androidTestImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
}

View File

@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/colorPrimary" />
<foreground android:drawable="@drawable/ic_launcher_foreground_dev" />
</adaptive-icon>
<monochrome android:drawable="@drawable/ic_launcher_foreground_dev_mono" />
</adaptive-icon>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/colorPrimary" />
<foreground android:drawable="@drawable/ic_launcher_foreground_dev" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -8,7 +8,8 @@
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<queries>
<intent>
@ -36,13 +37,14 @@
<application
android:name=".WulkanowyApp"
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="false"
android:theme="@style/WulkanowyTheme"
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
tools:ignore="DataExtractionRules,UnusedAttribute">
<activity
android:name=".ui.modules.splash.SplashActivity"
android:exported="true"

View File

@ -0,0 +1,87 @@
package io.github.wulkanowy.data.mappers
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.pojos.*
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.mapper.mapSemesters
import java.time.Instant
import io.github.wulkanowy.sdk.scrapper.register.RegisterStudent as SdkRegisterStudent
import io.github.wulkanowy.sdk.scrapper.register.RegisterUser as SdkRegisterUser
fun SdkRegisterUser.mapToPojo(password: String) = RegisterUser(
email = email,
login = login,
password = password,
baseUrl = baseUrl,
loginType = loginType,
symbols = symbols.map { registerSymbol ->
RegisterSymbol(
symbol = registerSymbol.symbol,
error = registerSymbol.error,
userName = registerSymbol.userName,
schools = registerSymbol.schools.map {
RegisterUnit(
userLoginId = it.userLoginId,
schoolId = it.schoolId,
schoolName = it.schoolName,
schoolShortName = it.schoolShortName,
parentIds = it.parentIds,
studentIds = it.studentIds,
employeeIds = it.employeeIds,
error = it.error,
students = it.subjects
.filterIsInstance<SdkRegisterStudent>()
.map { registerSubject ->
RegisterStudent(
studentId = registerSubject.studentId,
studentName = registerSubject.studentName,
studentSecondName = registerSubject.studentSecondName,
studentSurname = registerSubject.studentSurname,
className = registerSubject.className,
classId = registerSubject.classId,
isParent = registerSubject.isParent,
semesters = registerSubject.semesters
.mapSemesters()
.mapToEntities(registerSubject.studentId),
)
},
)
}
)
}
)
fun RegisterStudent.mapToStudentWithSemesters(
user: RegisterUser,
symbol: RegisterSymbol,
unit: RegisterUnit,
colors: List<Long>,
): StudentWithSemesters = StudentWithSemesters(
semesters = semesters,
student = Student(
email = user.login, // for compatibility
userName = symbol.userName,
userLoginId = unit.userLoginId,
isParent = isParent,
className = className,
classId = classId,
studentId = studentId,
symbol = symbol.symbol,
loginType = user.loginType.name,
schoolName = unit.schoolName,
schoolShortName = unit.schoolShortName,
schoolSymbol = unit.schoolId,
studentName = "$studentName $studentSurname",
loginMode = Sdk.Mode.SCRAPPER.name,
scrapperBaseUrl = user.baseUrl,
mobileBaseUrl = "",
certificateKey = "",
privateKey = "",
password = user.password,
isCurrent = false,
registrationDate = Instant.now(),
).apply {
avatarColor = colors.random()
},
)

View File

@ -0,0 +1,43 @@
package io.github.wulkanowy.data.pojos
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.sdk.scrapper.Scrapper
data class RegisterUser(
val email: String,
val password: String,
val login: String, // may be the same as email
val baseUrl: String,
val loginType: Scrapper.LoginType,
val symbols: List<RegisterSymbol>,
) : java.io.Serializable
data class RegisterSymbol(
val symbol: String,
val error: Throwable?,
val userName: String,
val schools: List<RegisterUnit>,
) : java.io.Serializable
data class RegisterUnit(
val userLoginId: Int,
val schoolId: String,
val schoolName: String,
val schoolShortName: String,
val parentIds: List<Int>,
val studentIds: List<Int>,
val employeeIds: List<Int>,
val error: Throwable?,
val students: List<RegisterStudent>,
) : java.io.Serializable
data class RegisterStudent(
val studentId: Int,
val studentName: String,
val studentSecondName: String,
val studentSurname: String,
val className: String,
val classId: Int,
val isParent: Boolean,
val semesters: List<Semester>,
) : java.io.Serializable

View File

@ -178,7 +178,7 @@ class MessageRepository @Inject constructor(
).first()
}
suspend fun deleteMessage(student: Student, mailbox: Mailbox, message: Message) {
suspend fun deleteMessage(student: Student, mailbox: Mailbox?, message: Message) {
deleteMessages(student, mailbox, listOf(message))
}

View File

@ -11,6 +11,8 @@ import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.mappers.mapToPojo
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.DispatchersProvider
@ -52,6 +54,14 @@ class StudentRepository @Inject constructor(
sdk.getStudentsFromScrapper(email, password, scrapperBaseUrl, symbol)
.mapToEntities(password, appInfo.defaultColorsForAvatar)
suspend fun getUserSubjectsFromScrapper(
email: String,
password: String,
scrapperBaseUrl: String,
symbol: String
): RegisterUser = sdk.getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, symbol)
.mapToPojo(password)
suspend fun getStudentsHybrid(
email: String,
password: String,

View File

@ -37,7 +37,7 @@ class ErrorDialog : DialogFragment() {
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val error = requireArguments().getSerializable(ARGUMENT_KEY) as Throwable
val error = requireArguments().serializable<Throwable>(ARGUMENT_KEY)
val binding = DialogErrorBinding.inflate(layoutInflater)
binding.bindErrorDetails(error)

View File

@ -1,6 +1,9 @@
package io.github.wulkanowy.ui.base
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.PackageManager.GET_ACTIVITIES
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
@ -41,9 +44,8 @@ class ThemeManager @Inject constructor(private val preferencesRepository: Prefer
)
}
private fun isThemeApplicable(activity: AppCompatActivity) =
activity.packageManager
.getPackageInfo(activity.packageName, GET_ACTIVITIES)
private fun isThemeApplicable(activity: AppCompatActivity): Boolean =
getPackageInfo(activity)
.activities
.singleOrNull { it.name == activity::class.java.canonicalName }
?.theme
@ -52,4 +54,14 @@ class ThemeManager @Inject constructor(private val preferencesRepository: Prefer
|| it == R.style.WulkanowyTheme_Login || it == R.style.WulkanowyTheme_Login_Black
|| it == R.style.WulkanowyTheme_MessageSend || it == R.style.WulkanowyTheme_MessageSend_Black
}
@Suppress("DEPRECATION")
private fun getPackageInfo(activity: AppCompatActivity): PackageInfo {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
activity.packageManager.getPackageInfo(
activity.packageName,
PackageManager.PackageInfoFlags.of(GET_ACTIVITIES.toLong())
)
} else activity.packageManager.getPackageInfo(activity.packageName, GET_ACTIVITIES)
}
}

View File

@ -34,6 +34,7 @@ class AccountFragment : BaseFragment<FragmentAccountBinding>(R.layout.fragment_a
override val titleStringId = R.string.account_title
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)

View File

@ -6,6 +6,7 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import androidx.core.view.get
import androidx.core.view.isVisible
import dagger.hilt.android.AndroidEntryPoint
@ -21,6 +22,7 @@ import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoFragment
import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView
import io.github.wulkanowy.utils.createNameInitialsDrawable
import io.github.wulkanowy.utils.nickOrName
import io.github.wulkanowy.utils.serializable
import javax.inject.Inject
@AndroidEntryPoint
@ -37,12 +39,12 @@ class AccountDetailsFragment :
private const val ARGUMENT_KEY = "Data"
fun newInstance(student: Student) =
AccountDetailsFragment().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, student) }
}
fun newInstance(student: Student) = AccountDetailsFragment().apply {
arguments = bundleOf(ARGUMENT_KEY to student)
}
}
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
@ -51,7 +53,7 @@ class AccountDetailsFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentAccountDetailsBinding.bind(view)
presenter.onAttachView(this, requireArguments()[ARGUMENT_KEY] as Student)
presenter.onAttachView(this, requireArguments().serializable(ARGUMENT_KEY))
}
override fun initView() {

View File

@ -4,11 +4,13 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.GridLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.databinding.DialogAccountEditBinding
import io.github.wulkanowy.ui.base.BaseDialogFragment
import io.github.wulkanowy.utils.serializable
import javax.inject.Inject
@AndroidEntryPoint
@ -24,12 +26,9 @@ class AccountEditDialog : BaseDialogFragment<DialogAccountEditBinding>(), Accoun
private const val ARGUMENT_KEY = "student_with_semesters"
fun newInstance(student: Student) =
AccountEditDialog().apply {
arguments = Bundle().apply {
putSerializable(ARGUMENT_KEY, student)
}
}
fun newInstance(student: Student) = AccountEditDialog().apply {
arguments = bundleOf(ARGUMENT_KEY to student)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
@ -45,7 +44,7 @@ class AccountEditDialog : BaseDialogFragment<DialogAccountEditBinding>(), Accoun
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
presenter.onAttachView(this, requireArguments()[ARGUMENT_KEY] as Student)
presenter.onAttachView(this, requireArguments().serializable(ARGUMENT_KEY))
}
override fun initView() {

View File

@ -4,6 +4,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
@ -13,6 +14,7 @@ import io.github.wulkanowy.ui.modules.account.AccountAdapter
import io.github.wulkanowy.ui.modules.account.AccountFragment
import io.github.wulkanowy.ui.modules.account.AccountItem
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.utils.serializable
import javax.inject.Inject
@AndroidEntryPoint
@ -30,9 +32,7 @@ class AccountQuickDialog : BaseDialogFragment<DialogAccountQuickBinding>(), Acco
fun newInstance(studentsWithSemesters: List<StudentWithSemesters>) =
AccountQuickDialog().apply {
arguments = Bundle().apply {
putSerializable(STUDENTS_ARGUMENT_KEY, studentsWithSemesters.toTypedArray())
}
arguments = bundleOf(STUDENTS_ARGUMENT_KEY to studentsWithSemesters.toTypedArray())
}
}
@ -49,8 +49,8 @@ class AccountQuickDialog : BaseDialogFragment<DialogAccountQuickBinding>(), Acco
@Suppress("UNCHECKED_CAST")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val studentsWithSemesters =
(requireArguments()[STUDENTS_ARGUMENT_KEY] as Array<StudentWithSemesters>).toList()
val studentsWithSemesters = requireArguments()
.serializable<Array<StudentWithSemesters>>(STUDENTS_ARGUMENT_KEY).toList()
presenter.onAttachView(this, studentsWithSemesters)
}

View File

@ -4,11 +4,13 @@ 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.DialogFragment
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.databinding.DialogAttendanceBinding
import io.github.wulkanowy.utils.descriptionRes
import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.serializable
import io.github.wulkanowy.utils.toFormattedString
class AttendanceDialog : DialogFragment() {
@ -22,16 +24,14 @@ class AttendanceDialog : DialogFragment() {
private const val ARGUMENT_KEY = "Item"
fun newInstance(exam: Attendance) = AttendanceDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) }
arguments = bundleOf(ARGUMENT_KEY to exam)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
arguments?.run {
attendance = getSerializable(ARGUMENT_KEY) as Attendance
}
attendance = requireArguments().serializable(ARGUMENT_KEY)
}
override fun onCreateView(

View File

@ -84,6 +84,7 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
}
}
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)

View File

@ -4,11 +4,13 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.data.db.entities.Conference
import io.github.wulkanowy.databinding.DialogConferenceBinding
import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.serializable
import io.github.wulkanowy.utils.toFormattedString
class ConferenceDialog : DialogFragment() {
@ -22,16 +24,14 @@ class ConferenceDialog : DialogFragment() {
private const val ARGUMENT_KEY = "item"
fun newInstance(conference: Conference) = ConferenceDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, conference) }
arguments = bundleOf(ARGUMENT_KEY to conference)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
arguments?.let {
conference = it.getSerializable(ARGUMENT_KEY) as Conference
}
conference = requireArguments().serializable(ARGUMENT_KEY)
}
override fun onCreateView(
@ -57,4 +57,4 @@ class ConferenceDialog : DialogFragment() {
conferenceDialogAgendaTitle.isVisible = conference.agenda.isNotBlank()
}
}
}
}

View File

@ -61,6 +61,7 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
fun newInstance() = DashboardFragment()
}
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)

View File

@ -1,9 +1,7 @@
package io.github.wulkanowy.ui.modules.debug.logviewer
import android.content.Intent
import android.content.Intent.EXTRA_EMAIL
import android.content.Intent.EXTRA_STREAM
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
import android.content.Intent.*
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
@ -36,6 +34,7 @@ class LogViewerFragment : BaseFragment<FragmentLogviewerBinding>(R.layout.fragme
fun newInstance() = LogViewerFragment()
}
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)

View File

@ -4,12 +4,14 @@ 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.DialogFragment
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.databinding.DialogExamBinding
import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.openCalendarEventAdd
import io.github.wulkanowy.utils.serializable
import io.github.wulkanowy.utils.toFormattedString
import java.time.LocalTime
@ -24,16 +26,14 @@ class ExamDialog : DialogFragment() {
private const val ARGUMENT_KEY = "Item"
fun newInstance(exam: Exam) = ExamDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) }
arguments = bundleOf(ARGUMENT_KEY to exam)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
arguments?.run {
exam = getSerializable(ARGUMENT_KEY) as Exam
}
exam = requireArguments().serializable(ARGUMENT_KEY)
}
override fun onCreateView(

View File

@ -51,6 +51,7 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
override val currentPageIndex get() = binding.gradeViewPager.currentItem
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)

View File

@ -5,6 +5,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Grade
@ -27,22 +28,19 @@ class GradeDetailsDialog : DialogFragment() {
private const val COLOR_THEME_KEY = "Theme"
fun newInstance(grade: Grade, colorTheme: GradeColorTheme) =
GradeDetailsDialog().apply {
arguments = Bundle().apply {
putSerializable(ARGUMENT_KEY, grade)
putSerializable(COLOR_THEME_KEY, colorTheme)
}
}
fun newInstance(grade: Grade, colorTheme: GradeColorTheme) = GradeDetailsDialog().apply {
arguments = bundleOf(
ARGUMENT_KEY to grade,
COLOR_THEME_KEY to colorTheme
)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
arguments?.run {
grade = getSerializable(ARGUMENT_KEY) as Grade
gradeColorTheme = getSerializable(COLOR_THEME_KEY) as GradeColorTheme
}
grade = requireArguments().serializable(ARGUMENT_KEY)
gradeColorTheme = requireArguments().serializable(COLOR_THEME_KEY)
}
override fun onCreateView(

View File

@ -5,9 +5,7 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.View.GONE
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.view.View.*
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
@ -42,6 +40,7 @@ class GradeDetailsFragment :
override val isViewEmpty
get() = gradeDetailsAdapter.itemCount == 0
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)

View File

@ -15,6 +15,7 @@ import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.grade.GradeView
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.serializable
import io.github.wulkanowy.utils.setOnItemSelectedListener
import javax.inject.Inject
@ -48,8 +49,8 @@ class GradeStatisticsFragment :
messageContainer = binding.gradeStatisticsRecycler
presenter.onAttachView(
view = this,
type = savedInstanceState?.getSerializable(SAVED_CHART_TYPE) as? GradeStatisticsItem.DataType,
subjectName = savedInstanceState?.getSerializable(SAVED_SUBJECT_NAME) as? String,
type = savedInstanceState?.serializable(SAVED_CHART_TYPE),
subjectName = savedInstanceState?.serializable(SAVED_SUBJECT_NAME),
)
}

View File

@ -7,6 +7,7 @@ import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
@ -14,6 +15,7 @@ import io.github.wulkanowy.data.db.entities.Homework
import io.github.wulkanowy.databinding.DialogHomeworkBinding
import io.github.wulkanowy.ui.base.BaseDialogFragment
import io.github.wulkanowy.utils.openInternetBrowser
import io.github.wulkanowy.utils.serializable
import javax.inject.Inject
@AndroidEntryPoint
@ -35,16 +37,14 @@ class HomeworkDetailsDialog : BaseDialogFragment<DialogHomeworkBinding>(), Homew
private const val ARGUMENT_KEY = "Item"
fun newInstance(homework: Homework) = HomeworkDetailsDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, homework) }
arguments = bundleOf(ARGUMENT_KEY to homework)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
arguments?.run {
homework = getSerializable(ARGUMENT_KEY) as Homework
}
homework = requireArguments().serializable(ARGUMENT_KEY)
}
override fun onCreateView(

View File

@ -2,13 +2,17 @@ package io.github.wulkanowy.ui.modules.login
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build.VERSION_CODES.TIRAMISU
import android.os.Bundle
import android.view.MenuItem
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE
import androidx.fragment.app.commit
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.databinding.ActivityLoginBinding
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.modules.login.advanced.LoginAdvancedFragment
@ -16,6 +20,9 @@ import io.github.wulkanowy.ui.modules.login.form.LoginFormFragment
import io.github.wulkanowy.ui.modules.login.recover.LoginRecoverFragment
import io.github.wulkanowy.ui.modules.login.studentselect.LoginStudentSelectFragment
import io.github.wulkanowy.ui.modules.login.symbol.LoginSymbolFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.notifications.NotificationsFragment
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.UpdateHelper
import javax.inject.Inject
@ -28,6 +35,9 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
@Inject
lateinit var updateHelper: UpdateHelper
@Inject
lateinit var appInfo: AppInfo
companion object {
fun getStartIntent(context: Context) = Intent(context, LoginActivity::class.java)
}
@ -55,7 +65,7 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) onBackPressed()
if (item.itemId == android.R.id.home) onBackPressedDispatcher.onBackPressed()
return true
}
@ -67,8 +77,24 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
openFragment(LoginSymbolFragment.newInstance(loginData))
}
fun navigateToStudentSelect(studentsWithSemesters: List<StudentWithSemesters>) {
openFragment(LoginStudentSelectFragment.newInstance(studentsWithSemesters))
fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser) {
openFragment(LoginStudentSelectFragment.newInstance(loginData, registerUser))
}
fun navigateToNotifications() {
val isNotificationsPermissionRequired = appInfo.systemVersion >= TIRAMISU
val isPermissionGranted = ContextCompat.checkSelfPermission(
this, "android.permission.POST_NOTIFICATIONS"
) == PackageManager.PERMISSION_GRANTED
if (isNotificationsPermissionRequired && !isPermissionGranted) {
openFragment(NotificationsFragment.newInstance(), clearBackStack = true)
} else navigateToFinish()
}
fun navigateToFinish() {
startActivity(MainActivity.getStartIntent(this))
finish()
}
fun onAdvancedLoginClick() {
@ -80,6 +106,8 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
}
private fun openFragment(fragment: Fragment, clearBackStack: Boolean = false) {
supportFragmentManager.popBackStack(fragment::class.java.name, POP_BACK_STACK_INCLUSIVE)
supportFragmentManager.commit {
replace(R.id.loginContainer, fragment)
setReorderingAllowed(true)

View File

@ -6,4 +6,5 @@ data class LoginData(
val login: String,
val password: String,
val baseUrl: String,
val symbol: String?,
) : Serializable

View File

@ -8,7 +8,7 @@ import android.widget.ArrayAdapter
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.pojos.RegisterUser
import io.github.wulkanowy.databinding.FragmentLoginAdvancedBinding
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.ui.base.BaseFragment
@ -327,8 +327,8 @@ class LoginAdvancedFragment :
(activity as? LoginActivity)?.navigateToSymbolFragment(loginData)
}
override fun navigateToStudentSelect(studentsWithSemesters: List<StudentWithSemesters>) {
(activity as? LoginActivity)?.navigateToStudentSelect(studentsWithSemesters)
override fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser) {
(activity as? LoginActivity)?.navigateToStudentSelect(loginData, registerUser)
}
override fun onResume() {

View File

@ -4,9 +4,15 @@ import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.pojos.RegisterStudent
import io.github.wulkanowy.data.pojos.RegisterSymbol
import io.github.wulkanowy.data.pojos.RegisterUnit
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.scrapper.Scrapper
import io.github.wulkanowy.sdk.scrapper.getNormalizedSymbol
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
@ -142,19 +148,23 @@ class LoginAdvancedPresenter @Inject constructor(
is Resource.Success -> {
analytics.logEvent(
"registration_form",
"success" to true,
"students" to it.data.size,
"error" to "No error"
)
val loginData = LoginData(
login = view?.formUsernameValue.orEmpty().trim(),
password = view?.formPassValue.orEmpty().trim(),
baseUrl = view?.formHostValue.orEmpty().trim()
)
when (it.data.size) {
0 -> view?.navigateToSymbol(loginData)
else -> view?.navigateToStudentSelect(it.data)
}
"success" to true,
"students" to it.data.size,
"error" to "No error"
)
val loginData = LoginData(
login = view?.formUsernameValue.orEmpty().trim(),
password = view?.formPassValue.orEmpty().trim(),
baseUrl = view?.formHostValue.orEmpty().trim(),
symbol = view?.formSymbolValue.orEmpty().trim().getNormalizedSymbol(),
)
when (it.data.size) {
0 -> view?.navigateToSymbol(loginData)
else -> view?.navigateToStudentSelect(
loginData = loginData,
registerUser = it.data.toRegisterUser(loginData),
)
}
}
is Resource.Error -> {
analytics.logEvent(
@ -173,6 +183,58 @@ class LoginAdvancedPresenter @Inject constructor(
}.launch("login")
}
private fun List<StudentWithSemesters>.toRegisterUser(loginData: LoginData) = RegisterUser(
email = loginData.login,
password = loginData.password,
login = loginData.login,
baseUrl = loginData.baseUrl,
loginType = firstOrNull()?.student?.loginType?.let(
Scrapper.LoginType::valueOf
) ?: Scrapper.LoginType.AUTO,
symbols = this
.groupBy { students -> students.student.symbol }
.map { (symbol, students) ->
RegisterSymbol(
symbol = symbol,
error = null,
userName = "",
schools = students
.groupBy { student ->
Triple(
first = student.student.schoolSymbol,
second = student.student.userLoginId,
third = student.student.schoolShortName
)
}
.map { (groupKey, students) ->
val (schoolId, loginId, schoolName) = groupKey
RegisterUnit(
students = students.map {
RegisterStudent(
studentId = it.student.studentId,
studentName = it.student.studentName,
studentSecondName = it.student.studentName,
studentSurname = it.student.studentName,
className = it.student.className,
classId = it.student.classId,
isParent = it.student.isParent,
semesters = it.semesters,
)
},
userLoginId = loginId,
schoolId = schoolId,
schoolName = schoolName,
schoolShortName = schoolName,
parentIds = listOf(),
studentIds = listOf(),
employeeIds = listOf(),
error = null
)
}
)
},
)
private suspend fun getStudentsAppropriatesToLoginType(): List<StudentWithSemesters> {
val email = view?.formUsernameValue.orEmpty()
val password = view?.formPassValue.orEmpty()

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.ui.modules.login.advanced
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.login.LoginData
@ -72,7 +73,7 @@ interface LoginAdvancedView : BaseView {
fun navigateToSymbol(loginData: LoginData)
fun navigateToStudentSelect(studentsWithSemesters: List<StudentWithSemesters>)
fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser)
fun setErrorPinRequired()

View File

@ -9,7 +9,7 @@ import androidx.core.view.isVisible
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.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.FragmentLoginFormBinding
import io.github.wulkanowy.ui.base.BaseFragment
@ -226,8 +226,8 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
(activity as? LoginActivity)?.navigateToSymbolFragment(loginData)
}
override fun navigateToStudentSelect(studentsWithSemesters: List<StudentWithSemesters>) {
(activity as? LoginActivity)?.navigateToStudentSelect(studentsWithSemesters)
override fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser) {
(activity as? LoginActivity)?.navigateToStudentSelect(loginData, registerUser)
}
override fun openAdvancedLogin() {

View File

@ -93,7 +93,7 @@ class LoginFormPresenter @Inject constructor(
if (!validateCredentials(email, password, host)) return
resourceFlow {
studentRepository.getStudentsScrapper(
studentRepository.getUserSubjectsFromScrapper(
email = email,
password = password,
scrapperBaseUrl = host,
@ -109,14 +109,14 @@ class LoginFormPresenter @Inject constructor(
}
}
.onResourceSuccess {
when (it.size) {
0 -> view?.navigateToSymbol(LoginData(email, password, host))
else -> view?.navigateToStudentSelect(it)
val loginData = LoginData(email, password, host, symbol)
when (it.symbols.size) {
0 -> view?.navigateToSymbol(loginData)
else -> view?.navigateToStudentSelect(loginData, it)
}
analytics.logEvent(
"registration_form",
"success" to true,
"students" to it.size,
"scrapperBaseUrl" to host,
"error" to "No error"
)
@ -134,7 +134,6 @@ class LoginFormPresenter @Inject constructor(
analytics.logEvent(
"registration_form",
"success" to false,
"students" to -1,
"scrapperBaseUrl" to host,
"error" to it.message.ifNullOrBlank { "No message" }
)

View File

@ -1,6 +1,6 @@
package io.github.wulkanowy.ui.modules.login.form
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.login.LoginData
@ -60,7 +60,7 @@ interface LoginFormView : BaseView {
fun navigateToSymbol(loginData: LoginData)
fun navigateToStudentSelect(studentsWithSemesters: List<StudentWithSemesters>)
fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser)
fun openPrivacyPolicyPage()

View File

@ -98,7 +98,7 @@ class LoginRecoverFragment :
loginRecoverButton.setOnClickListener { presenter.onRecoverClick() }
loginRecoverErrorRetry.setOnClickListener { presenter.onRecoverClick() }
loginRecoverErrorDetails.setOnClickListener { presenter.onDetailsClick() }
loginRecoverLogin.setOnClickListener { (activity as LoginActivity).onBackPressed() }
loginRecoverLogin.setOnClickListener { (activity as LoginActivity).onBackPressedDispatcher.onBackPressed() }
}
with(bindingLocal.loginRecoverHost) {

View File

@ -2,65 +2,182 @@ package io.github.wulkanowy.ui.modules.login.studentselect
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
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.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.databinding.ItemLoginStudentSelectBinding
import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.*
import javax.inject.Inject
@SuppressLint("SetTextI18n")
class LoginStudentSelectAdapter @Inject constructor() :
RecyclerView.Adapter<LoginStudentSelectAdapter.ItemViewHolder>() {
ListAdapter<LoginStudentSelectItem, RecyclerView.ViewHolder>(Differ) {
private val checkedList = mutableMapOf<Int, Boolean>()
override fun getItemViewType(position: Int): Int = getItem(position).type.ordinal
var items = emptyList<Pair<StudentWithSemesters, Boolean>>()
set(value) {
field = value
checkedList.clear()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (LoginStudentSelectItemType.values()[viewType]) {
LoginStudentSelectItemType.EMPTY_SYMBOLS_HEADER -> EmptySymbolsHeaderViewHolder(
ItemLoginStudentSelectEmptySymbolHeaderBinding.inflate(inflater, parent, false),
)
LoginStudentSelectItemType.SYMBOL_HEADER -> SymbolsHeaderViewHolder(
ItemLoginStudentSelectHeaderSymbolBinding.inflate(inflater, parent, false)
)
LoginStudentSelectItemType.SCHOOL_HEADER -> SchoolHeaderViewHolder(
ItemLoginStudentSelectHeaderSchoolBinding.inflate(inflater, parent, false)
)
LoginStudentSelectItemType.STUDENT -> StudentViewHolder(
ItemLoginStudentSelectStudentBinding.inflate(inflater, parent, false)
)
LoginStudentSelectItemType.HELP -> HelpViewHolder(
ItemLoginStudentSelectHelpBinding.inflate(inflater, parent, false)
)
}
}
var onClickListener: (StudentWithSemesters, alreadySaved: Boolean) -> Unit = { _, _ -> }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is EmptySymbolsHeaderViewHolder -> holder.bind(getItem(position) as LoginStudentSelectItem.EmptySymbolsHeader)
is SymbolsHeaderViewHolder -> holder.bind(getItem(position) as LoginStudentSelectItem.SymbolHeader)
is SchoolHeaderViewHolder -> holder.bind(getItem(position) as LoginStudentSelectItem.SchoolHeader)
is StudentViewHolder -> holder.bind(getItem(position) as LoginStudentSelectItem.Student)
is HelpViewHolder -> holder.bind(getItem(position) as LoginStudentSelectItem.Help)
}
}
override fun getItemCount() = items.size
private class EmptySymbolsHeaderViewHolder(
private val binding: ItemLoginStudentSelectEmptySymbolHeaderBinding,
) : RecyclerView.ViewHolder(binding.root) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder(
ItemLoginStudentSelectBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val (studentAndSemesters, alreadySaved) = items[position]
val student = studentAndSemesters.student
val semesters = studentAndSemesters.semesters
val diary = semesters.maxByOrNull { it.semesterId }
with(holder.binding) {
loginItemName.text = "${student.studentName} ${diary?.diaryName.orEmpty()}"
loginItemSchool.text = student.schoolName
loginItemName.isEnabled = !alreadySaved
loginItemSchool.isEnabled = !alreadySaved
loginItemSignedIn.visibility = if (alreadySaved) View.VISIBLE else View.GONE
with(loginItemCheck) {
isEnabled = !alreadySaved
keyListener = null
isChecked = checkedList[position] ?: false
fun bind(item: LoginStudentSelectItem.EmptySymbolsHeader) {
with(binding) {
loginStudentSelectEmptySymbolChevron.rotation = if (item.isExpanded) 270f else 90f
root.setOnClickListener { item.onClick() }
}
}
}
root.setOnClickListener {
onClickListener(studentAndSemesters, alreadySaved)
private class SymbolsHeaderViewHolder(
private val binding: ItemLoginStudentSelectHeaderSymbolBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: LoginStudentSelectItem.SymbolHeader) {
with(binding) {
loginStudentSelectHeaderSymbolValue.text = buildString {
append(root.context.getString(R.string.mobile_device_symbol))
append(": ")
append(item.humanReadableName ?: item.symbol.symbol)
if (!item.humanReadableName.isNullOrBlank()) {
append(" (${item.symbol.symbol})")
}
}
loginStudentSelectHeaderSymbolUsername.text = item.symbol.userName
loginStudentSelectHeaderSymbolUsername.isVisible = item.symbol.userName.isNotBlank()
loginStudentSelectHeaderSymbolError.text = item.symbol.error?.message
loginStudentSelectHeaderSymbolError.isVisible = item.symbol.error != null
loginStudentSelectHeaderSymbolError.maxLines = when {
item.isErrorExpanded -> Int.MAX_VALUE
else -> 2
}
if (item.symbol.error != null) {
root.setOnClickListener { item.onClick(item.symbol) }
} else root.setOnClickListener(null)
}
}
}
private class SchoolHeaderViewHolder(
private val binding: ItemLoginStudentSelectHeaderSchoolBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: LoginStudentSelectItem.SchoolHeader) {
with(binding) {
loginStudentSelectHeaderSchoolName.text = buildString {
append(item.unit.schoolName.trim())
append(" (")
append(item.unit.schoolShortName)
append(")")
}
loginStudentSelectHeaderSchoolDetails.isVisible = item.unit.students.isEmpty()
loginStudentSelectHeaderSchoolError.text = item.unit.error?.message
loginStudentSelectHeaderSchoolError.isVisible = item.unit.error != null
loginStudentSelectHeaderSchoolError.maxLines = when {
item.isErrorExpanded -> Int.MAX_VALUE
else -> 2
}
if (item.unit.error != null) {
root.setOnClickListener { item.onClick(item.unit) }
} else root.setOnClickListener(null)
}
}
}
private class StudentViewHolder(
private val binding: ItemLoginStudentSelectStudentBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: LoginStudentSelectItem.Student) {
val student = item.student
val semesters = student.semesters
val diary = semesters.maxByOrNull { it.semesterId }
with(binding) {
loginItemName.text = "${student.studentName} ${student.studentSurname}"
loginItemName.isEnabled = item.isEnabled
loginItemSignedIn.text = if (!item.isEnabled) {
root.context.getString(R.string.login_signed_in)
} else diary?.diaryName
with(loginItemCheck) {
if (isEnabled) {
isChecked = !isChecked
checkedList[position] = isChecked
}
keyListener = null
isEnabled = item.isEnabled
isChecked = item.isSelected || !item.isEnabled
}
root.isEnabled = item.isEnabled
root.setOnClickListener {
item.onClick(item)
}
}
}
}
class ItemViewHolder(val binding: ItemLoginStudentSelectBinding) :
RecyclerView.ViewHolder(binding.root)
private class HelpViewHolder(
private val binding: ItemLoginStudentSelectHelpBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: LoginStudentSelectItem.Help) {
with(binding) {
loginStudentSelectHelpSymbol.isVisible = item.isSymbolButtonVisible
loginStudentSelectHelpSymbol.setOnClickListener { item.onEnterSymbolClick() }
loginStudentSelectHelpMail.setOnClickListener { item.onContactUsClick() }
loginStudentSelectHelpDiscord.setOnClickListener { item.onDiscordClick() }
}
}
}
private object Differ : ItemCallback<LoginStudentSelectItem>() {
override fun areItemsTheSame(
oldItem: LoginStudentSelectItem, newItem: LoginStudentSelectItem
): Boolean = when {
oldItem is LoginStudentSelectItem.EmptySymbolsHeader && newItem is LoginStudentSelectItem.EmptySymbolsHeader -> true
oldItem is LoginStudentSelectItem.SymbolHeader && newItem is LoginStudentSelectItem.SymbolHeader -> {
oldItem.symbol == newItem.symbol
}
oldItem is LoginStudentSelectItem.Student && newItem is LoginStudentSelectItem.Student -> {
oldItem.student == newItem.student
}
else -> oldItem == newItem
}
override fun areContentsTheSame(
oldItem: LoginStudentSelectItem, newItem: LoginStudentSelectItem
): Boolean = oldItem == newItem
}
}

View File

@ -2,21 +2,20 @@ package io.github.wulkanowy.ui.modules.login.studentselect
import android.os.Bundle
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.core.view.isVisible
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.pojos.RegisterUser
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
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.openEmailClient
import io.github.wulkanowy.utils.openInternetBrowser
import io.github.wulkanowy.utils.serializable
import javax.inject.Inject
@AndroidEntryPoint
@ -36,12 +35,23 @@ class LoginStudentSelectFragment :
@Inject
lateinit var preferencesRepository: PreferencesRepository
companion object {
const val ARG_STUDENTS = "STUDENTS"
private lateinit var symbolsNames: Array<String>
private lateinit var symbolsValues: Array<String>
fun newInstance(studentsWithSemesters: List<StudentWithSemesters>) =
override val symbols: Map<String, String> by lazy {
symbolsValues.zip(symbolsNames).toMap()
}
companion object {
private const val ARG_LOGIN = "LOGIN"
private const val ARG_STUDENTS = "STUDENTS"
fun newInstance(loginData: LoginData, registerUser: RegisterUser) =
LoginStudentSelectFragment().apply {
arguments = bundleOf(ARG_STUDENTS to studentsWithSemesters)
arguments = bundleOf(
ARG_LOGIN to loginData,
ARG_STUDENTS to registerUser,
)
}
}
@ -49,62 +59,50 @@ class LoginStudentSelectFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentLoginStudentSelectBinding.bind(view)
symbolsNames = resources.getStringArray(R.array.symbols)
symbolsValues = resources.getStringArray(R.array.symbols_values)
presenter.onAttachView(
view = this,
students = requireArguments().getSerializable(ARG_STUDENTS) as List<StudentWithSemesters>,
loginData = requireArguments().serializable(ARG_LOGIN),
registerUser = requireArguments().serializable(ARG_STUDENTS),
)
}
override fun initView() {
(requireActivity() as LoginActivity).showActionBar(true)
loginAdapter.onClickListener = presenter::onItemSelected
with(binding) {
loginStudentSelectSignIn.setOnClickListener { presenter.onSignIn() }
loginStudentSelectContactDiscord.setOnClickListener { presenter.onDiscordClick() }
loginStudentSelectContactEmail.setOnClickListener { presenter.onEmailClick() }
with(loginStudentSelectRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = loginAdapter
}
loginStudentSelectRecycler.adapter = loginAdapter
}
}
override fun updateData(data: List<Pair<StudentWithSemesters, Boolean>>) {
with(loginAdapter) {
items = data
notifyDataSetChanged()
}
override fun updateData(data: List<LoginStudentSelectItem>) {
loginAdapter.submitList(data)
}
override fun openMainView() {
startActivity(MainActivity.getStartIntent(requireContext()))
requireActivity().finish()
override fun navigateToSymbol(loginData: LoginData) {
(requireActivity() as LoginActivity).navigateToSymbolFragment(loginData)
}
override fun navigateToNext() {
(requireActivity() as LoginActivity).navigateToNotifications()
}
override fun showProgress(show: Boolean) {
binding.loginStudentSelectProgress.visibility = if (show) VISIBLE else GONE
binding.loginStudentSelectProgress.isVisible = show
}
override fun showContent(show: Boolean) {
binding.loginStudentSelectContent.visibility = if (show) VISIBLE else GONE
binding.loginStudentSelectContent.isVisible = show
}
override fun enableSignIn(enable: Boolean) {
binding.loginStudentSelectSignIn.isEnabled = enable
}
override fun showContact(show: Boolean) {
binding.loginStudentSelectContact.visibility = if (show) VISIBLE else GONE
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
override fun openDiscordInvite() {
context?.openInternetBrowser("https://discord.gg/vccAQBr", ::showMessage)
}
@ -125,4 +123,9 @@ class LoginStudentSelectFragment :
)
)
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
}

View File

@ -0,0 +1,50 @@
package io.github.wulkanowy.ui.modules.login.studentselect
import io.github.wulkanowy.data.pojos.RegisterStudent
import io.github.wulkanowy.data.pojos.RegisterSymbol
import io.github.wulkanowy.data.pojos.RegisterUnit
sealed class LoginStudentSelectItem(val type: LoginStudentSelectItemType) {
data class EmptySymbolsHeader(
val isExpanded: Boolean,
val onClick: () -> Unit,
) : LoginStudentSelectItem(LoginStudentSelectItemType.EMPTY_SYMBOLS_HEADER)
data class SymbolHeader(
val symbol: RegisterSymbol,
val humanReadableName: String?,
val isErrorExpanded: Boolean,
val onClick: (RegisterSymbol) -> Unit,
) : LoginStudentSelectItem(LoginStudentSelectItemType.SYMBOL_HEADER)
data class SchoolHeader(
val unit: RegisterUnit,
val isErrorExpanded: Boolean,
val onClick: (RegisterUnit) -> Unit,
) : LoginStudentSelectItem(LoginStudentSelectItemType.SCHOOL_HEADER)
data class Student(
val symbol: RegisterSymbol,
val unit: RegisterUnit,
val student: RegisterStudent,
val isEnabled: Boolean,
val isSelected: Boolean,
val onClick: (Student) -> Unit,
) : LoginStudentSelectItem(LoginStudentSelectItemType.STUDENT)
data class Help(
val onEnterSymbolClick: () -> Unit,
val onContactUsClick: () -> Unit,
val onDiscordClick: () -> Unit,
val isSymbolButtonVisible: Boolean,
) : LoginStudentSelectItem(LoginStudentSelectItemType.HELP)
}
enum class LoginStudentSelectItemType {
EMPTY_SYMBOLS_HEADER,
SYMBOL_HEADER,
SCHOOL_HEADER,
STUDENT,
HELP,
}

View File

@ -1,15 +1,23 @@
package io.github.wulkanowy.ui.modules.login.studentselect
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.dataOrNull
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.mappers.mapToStudentWithSemesters
import io.github.wulkanowy.data.pojos.RegisterStudent
import io.github.wulkanowy.data.pojos.RegisterSymbol
import io.github.wulkanowy.data.pojos.RegisterUnit
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.sdk.scrapper.login.AccountPermissionException
import io.github.wulkanowy.services.sync.SyncManager
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.ifNullOrBlank
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
@ -19,18 +27,30 @@ class LoginStudentSelectPresenter @Inject constructor(
studentRepository: StudentRepository,
private val loginErrorHandler: LoginErrorHandler,
private val syncManager: SyncManager,
private val analytics: AnalyticsHelper
private val analytics: AnalyticsHelper,
private val appInfo: AppInfo,
) : BasePresenter<LoginStudentSelectView>(loginErrorHandler, studentRepository) {
private var lastError: Throwable? = null
private val selectedStudents = mutableListOf<StudentWithSemesters>()
private lateinit var registerUser: RegisterUser
private lateinit var loginData: LoginData
fun onAttachView(view: LoginStudentSelectView, students: List<StudentWithSemesters>) {
private lateinit var students: List<StudentWithSemesters>
private var isEmptySymbolsExpanded = false
private var expandedSymbolError: RegisterSymbol? = null
private var expandedSchoolError: RegisterUnit? = null
private val selectedStudents = mutableListOf<LoginStudentSelectItem.Student>()
fun onAttachView(
view: LoginStudentSelectView,
loginData: LoginData,
registerUser: RegisterUser,
) {
super.onAttachView(view)
with(view) {
initView()
showContact(false)
enableSignIn(false)
loginErrorHandler.onStudentDuplicate = {
showMessage(it)
@ -38,50 +58,171 @@ class LoginStudentSelectPresenter @Inject constructor(
}
}
if (students.size == 1) registerStudents(students)
loadData(students)
this.loginData = loginData
this.registerUser = registerUser
loadData()
}
private fun loadData() {
resetSelectedState()
resourceFlow { studentRepository.getSavedStudents(false) }.onEach {
students = it.dataOrNull.orEmpty()
when (it) {
is Resource.Loading -> Timber.d("Login student select students load started")
is Resource.Success -> refreshItems()
is Resource.Error -> {
errorHandler.dispatch(it.error)
lastError = it.error
refreshItems()
}
}
}.launch()
}
private fun createItems(): List<LoginStudentSelectItem> = buildList {
val notEmptySymbols = registerUser.symbols.filter { it.schools.isNotEmpty() }
val emptySymbols = registerUser.symbols.filter { it.schools.isEmpty() }
if (emptySymbols.isNotEmpty() && notEmptySymbols.isNotEmpty() && emptySymbols.any { it.symbol == loginData.symbol }) {
add(createEmptySymbolItem(emptySymbols.first { it.symbol == loginData.symbol }))
}
addAll(createNotEmptySymbolItems(notEmptySymbols, students))
addAll(createEmptySymbolItems(emptySymbols, notEmptySymbols.isNotEmpty()))
val helpItem = LoginStudentSelectItem.Help(
onEnterSymbolClick = ::onEnterSymbol,
onContactUsClick = ::onEmailClick,
onDiscordClick = ::onDiscordClick,
isSymbolButtonVisible = "login" !in loginData.baseUrl,
)
add(helpItem)
}
private fun createNotEmptySymbolItems(
notEmptySymbols: List<RegisterSymbol>,
students: List<StudentWithSemesters>,
) = buildList {
notEmptySymbols.forEach { registerSymbol ->
val symbolHeader = LoginStudentSelectItem.SymbolHeader(
symbol = registerSymbol,
humanReadableName = view?.symbols?.get(registerSymbol.symbol),
isErrorExpanded = expandedSymbolError == registerSymbol,
onClick = ::onSymbolItemClick,
)
add(symbolHeader)
registerSymbol.schools.forEach { registerUnit ->
val schoolHeader = LoginStudentSelectItem.SchoolHeader(
unit = registerUnit,
isErrorExpanded = expandedSchoolError == registerUnit,
onClick = ::onUnitItemClick,
)
add(schoolHeader)
registerUnit.students.forEach {
add(createStudentItem(it, registerSymbol, registerUnit, students))
}
}
}
}
private fun createStudentItem(
student: RegisterStudent,
symbol: RegisterSymbol,
school: RegisterUnit,
students: List<StudentWithSemesters>,
) = LoginStudentSelectItem.Student(
symbol = symbol,
unit = school,
student = student,
onClick = ::onItemSelected,
isEnabled = students.none {
it.student.email == registerUser.login
&& it.student.symbol == symbol.symbol
&& it.student.studentId == student.studentId
&& it.student.schoolSymbol == school.schoolId
&& it.student.classId == student.classId
},
isSelected = selectedStudents
.filter { it.symbol.symbol == symbol.symbol }
.filter { it.unit.schoolId == school.schoolId }
.filter { it.student.studentId == student.studentId }
.filter { it.student.classId == student.classId }
.size == 1,
)
private fun createEmptySymbolItems(
emptySymbols: List<RegisterSymbol>,
isNotEmptySymbolsExist: Boolean,
) = buildList {
val filteredEmptySymbols = emptySymbols.filter {
it.error !is AccountPermissionException
}.ifEmpty { emptySymbols.takeIf { !isNotEmptySymbolsExist }.orEmpty() }
if (filteredEmptySymbols.isNotEmpty() && isNotEmptySymbolsExist) {
val emptyHeader = LoginStudentSelectItem.EmptySymbolsHeader(
isExpanded = isEmptySymbolsExpanded,
onClick = ::onEmptySymbolsToggle,
)
add(emptyHeader)
if (isEmptySymbolsExpanded) {
filteredEmptySymbols.forEach {
add(createEmptySymbolItem(it))
}
}
}
if (filteredEmptySymbols.isNotEmpty() && !isNotEmptySymbolsExist) {
filteredEmptySymbols.forEach {
add(createEmptySymbolItem(it))
}
}
}
private fun createEmptySymbolItem(registerSymbol: RegisterSymbol) =
LoginStudentSelectItem.SymbolHeader(
symbol = registerSymbol,
humanReadableName = view?.symbols?.get(registerSymbol.symbol),
isErrorExpanded = expandedSymbolError == registerSymbol,
onClick = ::onSymbolItemClick,
)
fun onSignIn() {
registerStudents(selectedStudents)
}
fun onItemSelected(studentWithSemester: StudentWithSemesters, alreadySaved: Boolean) {
if (alreadySaved) return
private fun onEmptySymbolsToggle() {
isEmptySymbolsExpanded = !isEmptySymbolsExpanded
refreshItems()
}
private fun onItemSelected(item: LoginStudentSelectItem.Student) {
if (!item.isEnabled) return
selectedStudents
.removeAll { it == studentWithSemester }
.let { if (!it) selectedStudents.add(studentWithSemester) }
.removeAll {
it.student.studentId == item.student.studentId &&
it.student.classId == item.student.classId &&
it.unit.schoolId == item.unit.schoolId &&
it.symbol.symbol == item.symbol.symbol
}
.let { if (!it) selectedStudents.add(item) }
view?.enableSignIn(selectedStudents.isNotEmpty())
refreshItems()
}
private fun compareStudents(a: Student, b: Student): Boolean {
return a.email == b.email
&& a.symbol == b.symbol
&& a.studentId == b.studentId
&& a.schoolSymbol == b.schoolSymbol
&& a.classId == b.classId
private fun onSymbolItemClick(symbol: RegisterSymbol) {
expandedSymbolError = if (symbol != expandedSymbolError) symbol else null
refreshItems()
}
private fun loadData(studentsWithSemesters: List<StudentWithSemesters>) {
resetSelectedState()
resourceFlow { studentRepository.getSavedStudents(false) }.onEach {
when (it) {
is Resource.Loading -> Timber.d("Login student select students load started")
is Resource.Success -> view?.updateData(studentsWithSemesters.map { studentWithSemesters ->
studentWithSemesters to it.data.any { item ->
compareStudents(studentWithSemesters.student, item.student)
}
})
is Resource.Error -> {
errorHandler.dispatch(it.error)
lastError = it.error
view?.updateData(studentsWithSemesters.map { student -> student to false })
}
}
}.launch()
private fun onUnitItemClick(unit: RegisterUnit) {
expandedSchoolError = if (unit != expandedSchoolError) unit else null
refreshItems()
}
private fun resetSelectedState() {
@ -89,7 +230,20 @@ class LoginStudentSelectPresenter @Inject constructor(
view?.enableSignIn(false)
}
private fun registerStudents(studentsWithSemesters: List<StudentWithSemesters>) {
private fun refreshItems() {
view?.updateData(createItems())
}
private fun registerStudents(students: List<LoginStudentSelectItem>) {
val studentsWithSemesters = students
.filterIsInstance<LoginStudentSelectItem.Student>().map { item ->
item.student.mapToStudentWithSemesters(
user = registerUser,
symbol = item.symbol,
unit = item.unit,
colors = appInfo.defaultColorsForAvatar,
)
}
resourceFlow { studentRepository.saveStudents(studentsWithSemesters) }
.logResourceStatus("registration")
.onEach {
@ -100,14 +254,13 @@ class LoginStudentSelectPresenter @Inject constructor(
}
is Resource.Success -> {
syncManager.startOneTimeSyncWorker(quiet = true)
view?.openMainView()
view?.navigateToNext()
logRegisterEvent(studentsWithSemesters)
}
is Resource.Error -> {
view?.apply {
showProgress(false)
showContent(true)
showContact(true)
}
lastError = it.error
loginErrorHandler.dispatch(it.error)
@ -117,12 +270,22 @@ class LoginStudentSelectPresenter @Inject constructor(
}.launch("register")
}
fun onDiscordClick() {
private fun onEnterSymbol() {
view?.navigateToSymbol(loginData)
}
private fun onDiscordClick() {
view?.openDiscordInvite()
}
fun onEmailClick() {
view?.openEmail(lastError?.message.ifNullOrBlank { "empty" })
private fun onEmailClick() {
view?.openEmail(lastError?.message.ifNullOrBlank {
registerUser.symbols.flatMap { symbol ->
symbol.schools.map { it.error?.message } + symbol.error?.message
}.filterNotNull().distinct().joinToString("; ") {
it.take(46) + "..."
}.ifEmpty { "blank" }
})
}
private fun logRegisterEvent(

View File

@ -1,15 +1,19 @@
package io.github.wulkanowy.ui.modules.login.studentselect
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.login.LoginData
interface LoginStudentSelectView : BaseView {
val symbols: Map<String, String>
fun initView()
fun updateData(data: List<Pair<StudentWithSemesters, Boolean>>)
fun updateData(data: List<LoginStudentSelectItem>)
fun openMainView()
fun navigateToSymbol(loginData: LoginData)
fun navigateToNext()
fun showProgress(show: Boolean)
@ -17,8 +21,6 @@ interface LoginStudentSelectView : BaseView {
fun enableSignIn(enable: Boolean)
fun showContact(show: Boolean)
fun openDiscordInvite()
fun openEmail(lastError: String)

View File

@ -12,17 +12,13 @@ import androidx.core.text.parseAsHtml
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.pojos.RegisterUser
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
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.hideSoftInput
import io.github.wulkanowy.utils.openEmailClient
import io.github.wulkanowy.utils.openInternetBrowser
import io.github.wulkanowy.utils.showSoftInput
import io.github.wulkanowy.utils.*
import javax.inject.Inject
@AndroidEntryPoint
@ -46,6 +42,8 @@ class LoginSymbolFragment :
}
}
override val symbolValue: String? get() = binding.loginSymbolName.text?.toString()
override val symbolNameError: CharSequence?
get() = binding.loginSymbolNameLayout.error
@ -54,7 +52,7 @@ class LoginSymbolFragment :
binding = FragmentLoginSymbolBinding.bind(view)
presenter.onAttachView(
view = this,
loginData = requireArguments().getSerializable(SAVED_LOGIN_DATA) as LoginData,
loginData = requireArguments().serializable(SAVED_LOGIN_DATA),
)
}
@ -62,7 +60,7 @@ class LoginSymbolFragment :
(requireActivity() as LoginActivity).showActionBar(true)
with(binding) {
loginSymbolSignIn.setOnClickListener { presenter.attemptLogin(loginSymbolName.text.toString()) }
loginSymbolSignIn.setOnClickListener { presenter.attemptLogin() }
loginSymbolFaq.setOnClickListener { presenter.onFaqClick() }
loginSymbolContactEmail.setOnClickListener { presenter.onEmailClick() }
@ -96,9 +94,13 @@ class LoginSymbolFragment :
}
override fun setErrorSymbolRequire() {
binding.loginSymbolNameLayout.apply {
setErrorSymbol(getString(R.string.error_field_required))
}
override fun setErrorSymbol(message: String) {
with(binding.loginSymbolNameLayout) {
requestFocus()
error = getString(R.string.error_field_required)
error = message
}
}
@ -129,8 +131,8 @@ class LoginSymbolFragment :
binding.loginSymbolContainer.visibility = if (show) VISIBLE else GONE
}
override fun navigateToStudentSelect(studentsWithSemesters: List<StudentWithSemesters>) {
(activity as? LoginActivity)?.navigateToStudentSelect(studentsWithSemesters)
override fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser) {
(activity as? LoginActivity)?.navigateToStudentSelect(loginData, registerUser)
}
override fun onSaveInstanceState(outState: Bundle) {

View File

@ -1,9 +1,12 @@
package io.github.wulkanowy.ui.modules.login.symbol
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.dataOrNull
import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.sdk.scrapper.getNormalizedSymbol
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
@ -23,9 +26,14 @@ class LoginSymbolPresenter @Inject constructor(
lateinit var loginData: LoginData
private var registerUser: RegisterUser? = null
fun onAttachView(view: LoginSymbolView, loginData: LoginData) {
super.onAttachView(view)
this.loginData = loginData
loginErrorHandler.onBadCredentials = {
view.setErrorSymbol(it.orEmpty())
}
with(view) {
initView()
showContact(false)
@ -39,20 +47,24 @@ class LoginSymbolPresenter @Inject constructor(
view?.apply { if (symbolNameError != null) clearSymbolError() }
}
fun attemptLogin(symbol: String) {
if (symbol.isBlank()) {
fun attemptLogin() {
if (view?.symbolValue.isNullOrBlank()) {
view?.setErrorSymbolRequire()
return
}
loginData = loginData.copy(
symbol = view?.symbolValue?.getNormalizedSymbol(),
)
resourceFlow {
studentRepository.getStudentsScrapper(
studentRepository.getUserSubjectsFromScrapper(
email = loginData.login,
password = loginData.password,
scrapperBaseUrl = loginData.baseUrl,
symbol = symbol,
symbol = view?.symbolValue.orEmpty(),
)
}.onEach {
registerUser = it.dataOrNull
when (it) {
is Resource.Loading -> view?.run {
Timber.i("Login with symbol started")
@ -61,7 +73,7 @@ class LoginSymbolPresenter @Inject constructor(
showContent(false)
}
is Resource.Success -> {
when (it.data.size) {
when (it.data.symbols.size) {
0 -> {
Timber.i("Login with symbol result: Empty student list")
view?.run {
@ -71,15 +83,14 @@ class LoginSymbolPresenter @Inject constructor(
}
else -> {
Timber.i("Login with symbol result: Success")
view?.navigateToStudentSelect(requireNotNull(it.data))
view?.navigateToStudentSelect(loginData, requireNotNull(it.data))
}
}
analytics.logEvent(
"registration_symbol",
"success" to true,
"students" to it.data.size,
"scrapperBaseUrl" to loginData.baseUrl,
"symbol" to symbol,
"symbol" to view?.symbolValue,
"error" to "No error"
)
}
@ -90,7 +101,7 @@ class LoginSymbolPresenter @Inject constructor(
"success" to false,
"students" to -1,
"scrapperBaseUrl" to loginData.baseUrl,
"symbol" to symbol,
"symbol" to view?.symbolValue,
"error" to it.error.message.ifNullOrBlank { "No message" }
)
loginErrorHandler.dispatch(it.error)
@ -111,6 +122,12 @@ class LoginSymbolPresenter @Inject constructor(
}
fun onEmailClick() {
view?.openEmail(loginData.baseUrl, lastError?.message.ifNullOrBlank { "empty" })
view?.openEmail(loginData.baseUrl, lastError?.message.ifNullOrBlank {
registerUser?.symbols?.flatMap { symbol ->
symbol.schools.map { it.error?.message } + symbol.error?.message
}?.filterNotNull()?.distinct()?.joinToString(";") {
it.take(46) + "..."
} ?: "blank"
})
}
}

View File

@ -1,10 +1,13 @@
package io.github.wulkanowy.ui.modules.login.symbol
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.login.LoginData
interface LoginSymbolView : BaseView {
val symbolValue: String?
val symbolNameError: CharSequence?
fun initView()
@ -15,6 +18,8 @@ interface LoginSymbolView : BaseView {
fun setErrorSymbolRequire()
fun setErrorSymbol(message: String)
fun clearSymbolError()
fun clearAndFocusSymbol()
@ -27,7 +32,7 @@ interface LoginSymbolView : BaseView {
fun showContent(show: Boolean)
fun navigateToStudentSelect(studentsWithSemesters: List<StudentWithSemesters>)
fun navigateToStudentSelect(loginData: LoginData, registerUser: RegisterUser)
fun showContact(show: Boolean)

View File

@ -6,6 +6,8 @@ import android.os.Build.VERSION_CODES.P
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.activity.OnBackPressedCallback
import androidx.activity.addCallback
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment
@ -50,6 +52,8 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
@Inject
lateinit var appInfo: AppInfo
private var onBackCallback: OnBackPressedCallback? = null
private var accountMenu: MenuItem? = null
private val overlayProvider by lazy { ElevationOverlayProvider(this) }
@ -88,6 +92,9 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
this.savedInstanceState = savedInstanceState
messageContainer = binding.mainMessageContainer
updateHelper.messageContainer = binding.mainFragmentContainer
onBackCallback = onBackPressedDispatcher.addCallback(this, enabled = false) {
presenter.onBackPressed()
}
val destination = intent.getStringExtra(EXTRA_START_DESTINATION)
?.takeIf { savedInstanceState == null }
@ -266,6 +273,7 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
analytics.popCurrentScreen(navController.currentFrag!!::class.simpleName)
navController.pushFragment(fragment)
onBackCallback?.isEnabled = !isRootView
}
override fun popView(depth: Int) {
@ -273,10 +281,7 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
analytics.popCurrentScreen(navController.currentFrag!!::class.simpleName)
navController.safelyPopFragments(depth)
}
override fun onBackPressed() {
presenter.onBackPressed { super.onBackPressed() }
onBackCallback?.isEnabled = !isRootView
}
override fun showStudentAvatar(student: Student) {

View File

@ -139,12 +139,9 @@ class MainPresenter @Inject constructor(
return true
}
fun onBackPressed(default: () -> Unit) {
fun onBackPressed() {
Timber.i("Back pressed in main view")
view?.run {
if (isRootView) default()
else popView()
}
view?.popView()
}
fun onTabSelected(index: Int, wasSelected: Boolean): Boolean {

View File

@ -10,6 +10,7 @@ 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 io.github.wulkanowy.utils.parcelableArray
import javax.inject.Inject
@AndroidEntryPoint
@ -52,8 +53,7 @@ class MailboxChooserDialog : BaseDialogFragment<DialogMailboxChooserBinding>(),
presenter.onAttachView(
view = this,
requireMailbox = requireArguments().getBoolean(REQUIRED_KEY, false),
mailboxes = requireArguments().getParcelableArray(MAILBOX_KEY).orEmpty()
.toList() as List<Mailbox>,
mailboxes = requireArguments().parcelableArray<Mailbox>(MAILBOX_KEY).orEmpty().toList(),
)
}

View File

@ -13,6 +13,7 @@ import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.core.content.getSystemService
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
@ -23,6 +24,7 @@ import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.message.send.SendMessageActivity
import io.github.wulkanowy.utils.serializable
import io.github.wulkanowy.utils.shareText
import javax.inject.Inject
@ -66,13 +68,12 @@ class MessagePreviewFragment :
companion object {
const val MESSAGE_ID_KEY = "message_id"
fun newInstance(message: Message): MessagePreviewFragment {
return MessagePreviewFragment().apply {
arguments = Bundle().apply { putSerializable(MESSAGE_ID_KEY, message) }
}
fun newInstance(message: Message) = MessagePreviewFragment().apply {
arguments = bundleOf(MESSAGE_ID_KEY to message)
}
}
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
@ -83,8 +84,8 @@ class MessagePreviewFragment :
binding = FragmentMessagePreviewBinding.bind(view)
messageContainer = binding.messagePreviewContainer
presenter.onAttachView(
this,
(savedInstanceState ?: arguments)?.getSerializable(MESSAGE_ID_KEY) as? Message
view = this,
message = (savedInstanceState ?: arguments)?.serializable(MESSAGE_ID_KEY),
)
}

View File

@ -186,7 +186,7 @@ class MessagePreviewPresenter @Inject constructor(
runCatching {
val student = studentRepository.getCurrentStudent(decryptPass = true)
val mailbox = messageRepository.getMailboxByStudent(student)
messageRepository.deleteMessage(student, mailbox!!, message!!)
messageRepository.deleteMessage(student, mailbox, message!!)
}
.onFailure {
retryCallback = { onMessageDelete() }

View File

@ -28,6 +28,7 @@ import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialo
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.nullableSerializable
import io.github.wulkanowy.utils.showSoftInput
import javax.inject.Inject
@ -108,12 +109,12 @@ class SendMessageActivity : BaseActivity<SendMessagePresenter, ActivitySendMessa
presenter.onAttachView(
view = this,
reason = intent.getSerializableExtra(EXTRA_REASON) as? String,
message = intent.getSerializableExtra(EXTRA_MESSAGE) as? Message,
reply = intent.getSerializableExtra(EXTRA_REPLY) as? Boolean
reason = intent.nullableSerializable(EXTRA_REASON),
message = intent.nullableSerializable(EXTRA_MESSAGE),
reply = intent.nullableSerializable(EXTRA_REPLY)
)
supportFragmentManager.setFragmentResultListener(LISTENER_KEY, this) { _, bundle ->
presenter.onMailboxSelected(bundle.getSerializable(MAILBOX_KEY) as? Mailbox)
presenter.onMailboxSelected(bundle.nullableSerializable(MAILBOX_KEY))
}
}

View File

@ -9,6 +9,7 @@ import android.view.View.*
import android.widget.CompoundButton
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.core.os.bundleOf
import androidx.core.view.updatePadding
import androidx.fragment.app.setFragmentResultListener
import androidx.recyclerview.widget.LinearLayoutManager
@ -27,6 +28,7 @@ import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.hideSoftInput
import io.github.wulkanowy.utils.nullableSerializable
import javax.inject.Inject
@AndroidEntryPoint
@ -43,12 +45,8 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
const val MESSAGE_TAB_FOLDER_ID = "message_tab_folder_id"
fun newInstance(folder: MessageFolder): MessageTabFragment {
return MessageTabFragment().apply {
arguments = Bundle().apply {
putString(MESSAGE_TAB_FOLDER_ID, folder.name)
}
}
fun newInstance(folder: MessageFolder) = MessageTabFragment().apply {
arguments = bundleOf(MESSAGE_TAB_FOLDER_ID to folder.name)
}
}
@ -86,6 +84,7 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
}
}
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
@ -130,11 +129,12 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
setFragmentResultListener(requireArguments().getString(MESSAGE_TAB_FOLDER_ID)!!) { _, bundle ->
presenter.onMailboxSelected(
mailbox = bundle.getSerializable(MailboxChooserDialog.MAILBOX_KEY) as? Mailbox,
mailbox = bundle.nullableSerializable(MailboxChooserDialog.MAILBOX_KEY),
)
}
}
@Suppress("DEPRECATION")
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.action_menu_message_tab, menu)

View File

@ -6,6 +6,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Note
@ -13,6 +14,7 @@ import io.github.wulkanowy.databinding.DialogNoteBinding
import io.github.wulkanowy.sdk.scrapper.notes.NoteCategory
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.serializable
import io.github.wulkanowy.utils.toFormattedString
class NoteDialog : DialogFragment() {
@ -25,17 +27,15 @@ class NoteDialog : DialogFragment() {
private const val ARGUMENT_KEY = "Item"
fun newInstance(exam: Note) = NoteDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) }
fun newInstance(note: Note) = NoteDialog().apply {
arguments = bundleOf(ARGUMENT_KEY to note)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
arguments?.run {
note = getSerializable(ARGUMENT_KEY) as Note
}
note = requireArguments().serializable(ARGUMENT_KEY)
}
override fun onCreateView(

View File

@ -0,0 +1,64 @@
package io.github.wulkanowy.ui.modules.notifications
import android.os.Bundle
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
import androidx.appcompat.app.AlertDialog
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.FragmentNotificationsBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.utils.openNotificationSettings
@AndroidEntryPoint
class NotificationsFragment :
BaseFragment<FragmentNotificationsBinding>(R.layout.fragment_notifications) {
private val permission = "android.permission.POST_NOTIFICATIONS"
private val requestPermissionLauncher = registerForActivityResult(RequestPermission()) {
if (it) {
navigateToFinish()
} else showSettingsDialog()
}
companion object {
fun newInstance() = NotificationsFragment()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentNotificationsBinding.bind(view)
initView()
}
private fun initView() {
with(binding) {
notificationsSkip.setOnClickListener { navigateToFinish() }
notificationsEnable.setOnClickListener { requestPermission() }
}
}
private fun showSettingsDialog() {
AlertDialog.Builder(requireContext())
.setTitle(R.string.notifications_header_title)
.setMessage(R.string.notifications_header_description)
.setNegativeButton(R.string.notifications_skip) { dialog, _ ->
dialog.dismiss()
navigateToFinish()
}
.setPositiveButton(R.string.pref_notification_go_to_settings) { _, _ ->
requireActivity().openNotificationSettings()
}
.show()
}
private fun requestPermission() {
requestPermissionLauncher.launch(permission)
}
private fun navigateToFinish() {
(requireActivity() as LoginActivity).navigateToFinish()
}
}

View File

@ -2,10 +2,10 @@ package io.github.wulkanowy.ui.modules.schoolannouncement
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.text.parseAsHtml
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.databinding.ItemSchoolAnnouncementBinding
import io.github.wulkanowy.utils.parseUonetHtml
import io.github.wulkanowy.utils.toFormattedString
import javax.inject.Inject
@ -28,7 +28,7 @@ class SchoolAnnouncementAdapter @Inject constructor() :
with(holder.binding) {
schoolAnnouncementItemDate.text = item.date.toFormattedString()
schoolAnnouncementItemType.text = item.subject
schoolAnnouncementItemContent.text = item.content.parseAsHtml()
schoolAnnouncementItemContent.text = item.content.parseUonetHtml()
root.setOnClickListener { onItemClickListener(item) }
}

View File

@ -4,11 +4,13 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.text.parseAsHtml
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.databinding.DialogSchoolAnnouncementBinding
import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.parseUonetHtml
import io.github.wulkanowy.utils.serializable
import io.github.wulkanowy.utils.toFormattedString
class SchoolAnnouncementDialog : DialogFragment() {
@ -21,17 +23,15 @@ class SchoolAnnouncementDialog : DialogFragment() {
private const val ARGUMENT_KEY = "item"
fun newInstance(exam: SchoolAnnouncement) = SchoolAnnouncementDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) }
fun newInstance(announcement: SchoolAnnouncement) = SchoolAnnouncementDialog().apply {
arguments = bundleOf(ARGUMENT_KEY to announcement)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
arguments?.run {
announcement = getSerializable(ARGUMENT_KEY) as SchoolAnnouncement
}
announcement = requireArguments().serializable(ARGUMENT_KEY)
}
override fun onCreateView(
@ -46,7 +46,7 @@ class SchoolAnnouncementDialog : DialogFragment() {
with(binding) {
announcementDialogSubjectValue.text = announcement.subject
announcementDialogDateValue.text = announcement.date.toFormattedString()
announcementDialogDescriptionValue.text = announcement.content.parseAsHtml()
announcementDialogDescriptionValue.text = announcement.content.parseUonetHtml()
announcementDialogClose.setOnClickListener { dismiss() }
}

View File

@ -1,18 +1,16 @@
package io.github.wulkanowy.ui.modules.settings.notifications
import android.annotation.SuppressLint
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
import android.os.Build
import android.content.pm.PackageManager
import android.os.Bundle
import android.provider.Settings
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
@ -26,7 +24,7 @@ import io.github.wulkanowy.ui.base.ErrorDialog
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.openInternetBrowser
import timber.log.Timber
import io.github.wulkanowy.utils.openNotificationSettings
import javax.inject.Inject
@AndroidEntryPoint
@ -42,7 +40,14 @@ class NotificationsFragment : PreferenceFragmentCompat(),
override val titleStringId get() = R.string.pref_settings_notifications_title
private val notificationsPermission = "android.permission.POST_NOTIFICATIONS"
override val isNotificationPermissionGranted: Boolean
get() = ContextCompat.checkSelfPermission(
requireContext(), notificationsPermission
) == PackageManager.PERMISSION_GRANTED
override val isNotificationPiggybackPermissionGranted: Boolean
get() {
val packageNameList =
NotificationManagerCompat.getEnabledListenerPackages(requireContext())
@ -51,6 +56,13 @@ class NotificationsFragment : PreferenceFragmentCompat(),
return appPackageName in packageNameList
}
private val requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
if (it) {
presenter.onNotificationsPermissionResult()
} else openNotificationsPermissionDialog()
}
private val notificationSettingsPiggybackContract =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
presenter.onNotificationPiggybackPermissionResult()
@ -156,25 +168,29 @@ class NotificationsFragment : PreferenceFragmentCompat(),
.show()
}
@SuppressLint("InlinedApi")
override fun openSystemSettings() {
val intent = if (appInfo.systemVersion >= Build.VERSION_CODES.O) {
Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
putExtra("android.provider.extra.APP_PACKAGE", requireActivity().packageName)
}
} else {
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", requireActivity().packageName, null)
}
}
try {
requireActivity().startActivity(intent)
} catch (e: Exception) {
Timber.e(e)
}
requireActivity().openNotificationSettings()
}
override fun openNotificationPermissionDialog() {
override fun requestNotificationPermissions() {
requestPermissionLauncher.launch(notificationsPermission)
}
override fun openNotificationsPermissionDialog() {
AlertDialog.Builder(requireContext())
.setTitle(R.string.notifications_header_title)
.setMessage(R.string.notifications_header_description)
.setPositiveButton(R.string.pref_notification_go_to_settings) { _, _ ->
requireActivity().openNotificationSettings()
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
setNotificationPreferencesChecked(false)
}
.setOnDismissListener { setNotificationPreferencesChecked(false) }
.show()
}
override fun openNotificationPiggyBackPermissionDialog() {
AlertDialog.Builder(requireContext())
.setTitle(getString(R.string.pref_notification_piggyback_popup_title))
.setMessage(getString(R.string.pref_notification_piggyback_popup_description))
@ -202,6 +218,11 @@ class NotificationsFragment : PreferenceFragmentCompat(),
.show()
}
override fun setNotificationPreferencesChecked(isChecked: Boolean) {
findPreference<SwitchPreferenceCompat>(getString(R.string.pref_key_notifications_enable))?.isChecked =
isChecked
}
override fun setNotificationPiggybackPreferenceChecked(isChecked: Boolean) {
findPreference<SwitchPreferenceCompat>(getString(R.string.pref_key_notifications_piggyback))?.isChecked =
isChecked

View File

@ -26,12 +26,13 @@ class NotificationsPresenter @Inject constructor(
with(view) {
enableNotification(
preferencesRepository.notificationsEnableKey,
preferencesRepository.isServiceEnabled
notificationKey = preferencesRepository.notificationsEnableKey,
enable = preferencesRepository.isServiceEnabled
)
initView(appInfo.isDebug)
}
checkNotificationsPermissionState()
checkNotificationPiggybackState()
Timber.i("Settings notifications view was initialized")
@ -49,12 +50,17 @@ class NotificationsPresenter @Inject constructor(
view?.openNotificationExactAlarmSettings()
}
}
notificationsEnableKey -> {
if (isNotificationsEnable && view?.isNotificationPermissionGranted == false) {
view?.requestNotificationPermissions()
}
}
isDebugNotificationEnableKey -> {
chuckerCollector.showNotification = isDebugNotificationEnable
}
isNotificationPiggybackEnabledKey -> {
if (isNotificationPiggybackEnabled && view?.isNotificationPermissionGranted == false) {
view?.openNotificationPermissionDialog()
if (isNotificationPiggybackEnabled && view?.isNotificationPiggybackPermissionGranted == false) {
view?.openNotificationPiggyBackPermissionDialog()
}
}
}
@ -70,9 +76,15 @@ class NotificationsPresenter @Inject constructor(
view?.openSystemSettings()
}
fun onNotificationsPermissionResult() {
view?.run {
setNotificationPreferencesChecked(isNotificationPermissionGranted)
}
}
fun onNotificationPiggybackPermissionResult() {
view?.run {
setNotificationPiggybackPreferenceChecked(isNotificationPermissionGranted)
setNotificationPiggybackPreferenceChecked(isNotificationPiggybackPermissionGranted)
}
}
@ -80,10 +92,18 @@ class NotificationsPresenter @Inject constructor(
view?.setUpcomingLessonsNotificationPreferenceChecked(timetableNotificationHelper.canScheduleExactAlarms())
}
private fun checkNotificationsPermissionState() {
if (preferencesRepository.isNotificationsEnable) {
view?.run {
setNotificationPreferencesChecked(isNotificationPermissionGranted)
}
}
}
private fun checkNotificationPiggybackState() {
if (preferencesRepository.isNotificationPiggybackEnabled) {
view?.run {
setNotificationPiggybackPreferenceChecked(isNotificationPermissionGranted)
setNotificationPiggybackPreferenceChecked(isNotificationPiggybackPermissionGranted)
}
}
}

View File

@ -6,6 +6,8 @@ interface NotificationsView : BaseView {
val isNotificationPermissionGranted: Boolean
val isNotificationPiggybackPermissionGranted: Boolean
fun initView(showDebugNotificationSwitch: Boolean)
fun showFixSyncDialog()
@ -14,10 +16,16 @@ interface NotificationsView : BaseView {
fun enableNotification(notificationKey: String, enable: Boolean)
fun openNotificationPermissionDialog()
fun requestNotificationPermissions()
fun openNotificationsPermissionDialog()
fun openNotificationPiggyBackPermissionDialog()
fun openNotificationExactAlarmSettings()
fun setNotificationPreferencesChecked(isChecked: Boolean)
fun setNotificationPiggybackPreferenceChecked(isChecked: Boolean)
fun setUpcomingLessonsNotificationPreferenceChecked(isChecked: Boolean)

View File

@ -8,6 +8,7 @@ import android.view.MenuInflater
import android.view.View
import android.widget.Toast
import androidx.core.content.getSystemService
import androidx.core.os.bundleOf
import androidx.core.view.get
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
@ -24,6 +25,8 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.capitalise
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.nullableSerializable
import io.github.wulkanowy.utils.serializable
import javax.inject.Inject
@AndroidEntryPoint
@ -38,7 +41,9 @@ class StudentInfoFragment :
lateinit var studentInfoAdapter: StudentInfoAdapter
override val titleStringId: Int
get() = when (requireArguments().getSerializable(INFO_TYPE_ARGUMENT_KEY) as? StudentInfoView.Type) {
get() = when (
requireArguments().nullableSerializable<StudentInfoView.Type>(INFO_TYPE_ARGUMENT_KEY)
) {
StudentInfoView.Type.PERSONAL -> R.string.account_personal_data
StudentInfoView.Type.CONTACT -> R.string.account_contact
StudentInfoView.Type.ADDRESS -> R.string.account_address
@ -58,13 +63,14 @@ class StudentInfoFragment :
fun newInstance(type: StudentInfoView.Type, studentWithSemesters: StudentWithSemesters) =
StudentInfoFragment().apply {
arguments = Bundle().apply {
putSerializable(INFO_TYPE_ARGUMENT_KEY, type)
putSerializable(STUDENT_ARGUMENT_KEY, studentWithSemesters)
}
arguments = bundleOf(
INFO_TYPE_ARGUMENT_KEY to type,
STUDENT_ARGUMENT_KEY to studentWithSemesters
)
}
}
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
@ -74,9 +80,9 @@ class StudentInfoFragment :
super.onViewCreated(view, savedInstanceState)
binding = FragmentStudentInfoBinding.bind(view)
presenter.onAttachView(
this,
requireArguments().getSerializable(INFO_TYPE_ARGUMENT_KEY) as StudentInfoView.Type,
requireArguments().getSerializable(STUDENT_ARGUMENT_KEY) as StudentWithSemesters
view = this,
type = requireArguments().serializable(INFO_TYPE_ARGUMENT_KEY),
studentWithSemesters = requireArguments().serializable(STUDENT_ARGUMENT_KEY),
)
}
@ -153,7 +159,6 @@ class StudentInfoFragment :
)
}
@OptIn(ExperimentalStdlibApi::class)
override fun showFamilyTypeData(studentInfo: StudentInfo) {
val items = buildList {
add(studentInfo.firstGuardian?.let {

View File

@ -8,14 +8,12 @@ import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.databinding.DialogTimetableBinding
import io.github.wulkanowy.utils.capitalise
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.toFormattedString
import io.github.wulkanowy.utils.*
import java.time.Instant
class TimetableDialog : DialogFragment() {
@ -28,17 +26,15 @@ class TimetableDialog : DialogFragment() {
private const val ARGUMENT_KEY = "Item"
fun newInstance(exam: Timetable) = TimetableDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) }
fun newInstance(lesson: Timetable) = TimetableDialog().apply {
arguments = bundleOf(ARGUMENT_KEY to lesson)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
arguments?.run {
lesson = getSerializable(ARGUMENT_KEY) as Timetable
}
lesson = requireArguments().serializable(ARGUMENT_KEY)
}
override fun onCreateView(

View File

@ -7,6 +7,7 @@ import android.view.MenuItem
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import androidx.core.os.bundleOf
import androidx.core.text.parseAsHtml
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
@ -39,9 +40,7 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
private const val ARGUMENT_DATE_KEY = "ARGUMENT_DATE"
fun newInstance(date: LocalDate? = null) = TimetableFragment().apply {
arguments = Bundle().apply {
date?.let { putLong(ARGUMENT_DATE_KEY, it.toEpochDay()) }
}
arguments = date?.let { bundleOf(ARGUMENT_DATE_KEY to it.toEpochDay()) }
}
}
@ -51,6 +50,7 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
override val currentStackSize get() = (activity as? MainActivity)?.currentStackSize
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)

View File

@ -4,10 +4,12 @@ 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.DialogFragment
import io.github.wulkanowy.data.db.entities.CompletedLesson
import io.github.wulkanowy.databinding.DialogLessonCompletedBinding
import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.serializable
class CompletedLessonDialog : DialogFragment() {
@ -19,17 +21,15 @@ class CompletedLessonDialog : DialogFragment() {
private const val ARGUMENT_KEY = "Item"
fun newInstance(exam: CompletedLesson) = CompletedLessonDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) }
fun newInstance(lesson: CompletedLesson) = CompletedLessonDialog().apply {
arguments = bundleOf(ARGUMENT_KEY to lesson)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
arguments?.run {
completedLesson = getSerializable(ARGUMENT_KEY) as CompletedLesson
}
completedLesson = requireArguments().serializable(ARGUMENT_KEY)
}
override fun onCreateView(

View File

@ -0,0 +1,32 @@
package io.github.wulkanowy.utils
import android.content.Intent
import android.os.Build
import android.os.Bundle
import java.io.Serializable
inline fun <reified T : Serializable> Bundle.serializable(key: String): T = when {
Build.VERSION.SDK_INT >= 33 -> getSerializable(key, T::class.java)!!
else -> @Suppress("DEPRECATION") getSerializable(key) as T
}
inline fun <reified T : Serializable> Bundle.nullableSerializable(key: String): T? = when {
Build.VERSION.SDK_INT >= 33 -> getSerializable(key, T::class.java)
else -> @Suppress("DEPRECATION") getSerializable(key) as T?
}
@Suppress("DEPRECATION", "UNCHECKED_CAST")
inline fun <reified T : Serializable> Bundle.parcelableArray(key: String): Array<T>? = when {
Build.VERSION.SDK_INT >= 33 -> getParcelableArray(key, T::class.java)
else -> getParcelableArray(key) as Array<T>?
}
inline fun <reified T : Serializable> Intent.serializable(key: String): T = when {
Build.VERSION.SDK_INT >= 33 -> getSerializableExtra(key, T::class.java)!!
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as T
}
inline fun <reified T : Serializable> Intent.nullableSerializable(key: String): T? = when {
Build.VERSION.SDK_INT >= 33 -> getSerializableExtra(key, T::class.java)
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as T?
}

View File

@ -1,11 +1,15 @@
package io.github.wulkanowy.utils
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.CalendarContract
import android.provider.Settings
import io.github.wulkanowy.BuildConfig
import timber.log.Timber
import java.time.LocalDateTime
import java.time.ZoneId
@ -86,6 +90,23 @@ fun Context.openDialer(phone: String) {
}
}
fun Activity.openNotificationSettings() {
val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
putExtra("android.provider.extra.APP_PACKAGE", packageName)
}
} else {
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", packageName, null)
}
}
try {
startActivity(intent)
} catch (e: Exception) {
Timber.e(e)
}
}
fun Context.shareText(text: String, subject: String?) {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND

View File

@ -1,7 +1,15 @@
package io.github.wulkanowy.utils
import androidx.core.text.parseAsHtml
import org.apache.commons.text.StringEscapeUtils
inline fun String?.ifNullOrBlank(defaultValue: () -> String) =
if (isNullOrBlank()) defaultValue() else this
fun String.capitalise() =
replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
fun String.parseUonetHtml() = this
.let(StringEscapeUtils::unescapeHtml4)
.replace("\n", "<br/>")
.parseAsHtml()

View File

@ -1,8 +1,10 @@
Wersja 1.8.3
Wersja 1.9.0
- naprawiliśmy logowanie dla użytkowników systemu Resman Rzeszów
- dodaliśmy wsparcie dla nowej platformy z Tomaszowa Mazowieckiego
- poprawiliśmy dopasowywanie skrzynek pocztowych do uczniów
- naprawiliśmy literówkę w tytule wiadomości z szablonem usprawiedliwienia
- dodaliśmy obsługę Androida 13 (w tym ikona aplikacji obsługująca Material You)
- przerobiliśmy ekran wyboru ucznia przy pierwszym logowaniu
- naprawiliśmy usuwanie wiadomości w niektórych przypadkach
- naprawiliśmy błąd występujący przy resecie hasła
- naprawiliśmy literówkę w tytule domyślnej treści wiadomości usprawiedliwiania
- naprawiliśmy nazwę aplikacji przy ustawionym w telefonie jezyku francuskim
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases

View File

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="1926"
android:viewportHeight="1926">
<path
android:fillColor="#fff"
android:pathData="M920,1357.7v-184.5h42.7q19.3,0.3 34.3,6.6 15.2,6.2 25.6,17.5 10.6,11.2 16,26.9 5.6,15.6 5.7, 34.6v13.5q-0.1,19 -5.7,34.7 -5.4,15.6 -16,26.9 -10.4,11.2 -25.6,17.5 -15,6.2 -34.3,6.3zM943.8,1192.5v146.1h18.9q15, -0.1 25.8,-5.2 11,-5.2 18,-14 7.3,-9 10.7,-21 3.5,-12 3.6,-26v-13.9q0,-14 -3.6,-26t-10.7,-20.7q-7,-9 -18,-14 -10.9,-5 -25.8,-5.3zM1179.5,1272.5h-77v65.4h89.8v19.9L1079,1357.8v-184.6h112.2v20h-88.7v59.4h77zM1288.9, 1320l43.6,-146.8h25l-58.2,184.6h-20.4l-58.3,-184.6h25z"
android:strokeWidth="1"
android:strokeColor="#000" />
<clip-path android:pathData="M 0 0 L 1926 0 L 1926 1111 L 850 1111 L 850 1926 L 0 1926 z" />
<path
android:fillColor="#fff"
android:pathData="M1082,1260v4l-6,73c0,5 -5,9 -10,9H879c-4,0 -8,-2 -10,-5l-31,-58 -29,-71v-8l48,-94c2,-3 2,-5 1, -8l-24,-65c-1,-2 0,-5 1,-7l39,-84c4,-8 17,-8 20,1l12,38 38,101c1,2 3,5 6,6l80,27c3,1 6,3 7,6l45,135zM852,717c0,-23 15,-43 35,-52l-2,-8c0,-11 13,-20 29,-20h1a38,38 0,0 1,36 -18l3,-2c11,-16 37,-27 68,-27 12,0 23,2 32,4 3,-3 8,-6 14,-6s12,3 14,8c8,-9 22,-16 38,-16 25,0 45,16 45,36l-1,7 3,4c16,6 27,17 27,30 0,14 -15,27 -34,32 -2,0 -3,1 -3,3 0,11 -11,21 -26,22v1c0,22 -41,40 -92,40 -11,0 -21,-1 -31,-3v1c0,9 -12,16 -26,16h-2l2,7c0,14 -18,25 -40,25h-7l-4, 2c-2,7 -8,12 -15,12 -9,0 -16,-7 -16,-16 0,-5 1,-8 4,-11l1,-4c-2,-3 -2,-5 -2,-8 0,-2 -2,-3 -3,-3 -27,-6 -48,-29 -48,-56z" />
<path
android:fillColor="#fff"
android:pathData="M1336,1346h-125c-3,0 -6,-1 -8,-4l-84,-118 -1,-2 -42,-122 -5,-5 -102,-52 -5,-6 -15,-75 -2,-3 -34,-51c-2,-3 -2,-6 -1,-9l21,-45c2,-4 5,-6 9,-6l39,-2c3,0 5,-1 6,-3l29,-22c5,-4 14,-2 17,4l98,201 1,2 13,81 2,5 197,216c6,6 1,16 -8,16zM801,1302c1,3 1,6 -1,8l-19,31c-2,3 -5,5 -9,5H591c-8,0 -13,-8 -9,-15l116,-171 2,-3 53, -228c1,-3 3,-5 5,-6l72,-40c3,-1 5,-4 5,-7l10,-42c2,-9 15,-11 20,-3l3,6c2,2 2,5 1,8l-66,190v5l16,91v5l-40,88v6l22,72z" />
</vector>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="1926"
android:viewportHeight="1926">
<path
android:fillColor="#fff"
android:pathData="M1082,1260v4l-6,73c0,5 -5,9 -10,9H879c-4,0 -8,-2 -10,-5l-31,-58 -29,-71v-8l48,-94c2,-3 2,-5 1, -8l-24,-65c-1,-2 0,-5 1,-7l39,-84c4,-8 17,-8 20,1l12,38 38,101c1,2 3,5 6,6l80,27c3,1 6,3 7,6l45,135zM852,717c0,-23 15,-43 35,-52l-2,-8c0,-11 13,-20 29,-20h1a38,38 0,0 1,36 -18l3,-2c11,-16 37,-27 68,-27 12,0 23,2 32,4 3,-3 8,-6 14,-6s12,3 14,8c8,-9 22,-16 38,-16 25,0 45,16 45,36l-1,7 3,4c16,6 27,17 27,30 0,14 -15,27 -34,32 -2,0 -3,1 -3,3 0,11 -11,21 -26,22v1c0,22 -41,40 -92,40 -11,0 -21,-1 -31,-3v1c0,9 -12,16 -26,16h-2l2,7c0,14 -18,25 -40,25h-7l-4, 2c-2,7 -8,12 -15,12 -9,0 -16,-7 -16,-16 0,-5 1,-8 4,-11l1,-4c-2,-3 -2,-5 -2,-8 0,-2 -2,-3 -3,-3 -27,-6 -48,-29 -48,-56z" />
<path
android:fillColor="#fff"
android:pathData="M1336,1346h-125c-3,0 -6,-1 -8,-4l-84,-118 -1,-2 -42,-122 -5,-5 -102,-52 -5,-6 -15,-75 -2,-3 -34,-51c-2,-3 -2,-6 -1,-9l21,-45c2,-4 5,-6 9,-6l39,-2c3,0 5,-1 6,-3l29,-22c5,-4 14,-2 17,4l98,201 1,2 13,81 2,5 197,216c6,6 1,16 -8,16zM801,1302c1,3 1,6 -1,8l-19,31c-2,3 -5,5 -9,5H591c-8,0 -13,-8 -9,-15l116,-171 2,-3 53, -228c1,-3 3,-5 5,-6l72,-40c3,-1 5,-4 5,-7l10,-42c2,-9 15,-11 20,-3l3,6c2,2 2,5 1,8l-66,190v5l16,91v5l-40,88v6l22,72z" />
</vector>

View File

@ -40,7 +40,7 @@
android:layout_marginStart="0dp"
android:layout_marginTop="28dp"
android:layout_marginEnd="24dp"
android:text="@string/all_title"
android:text="@string/conference_place"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
@ -71,7 +71,7 @@
android:layout_marginStart="0dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="24dp"
android:text="@string/all_subject"
android:text="@string/conference_topic"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"

View File

@ -3,92 +3,14 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/loginStudentSelectProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="gone" />
android:orientation="vertical"
tools:context=".ui.modules.login.studentselect.LoginStudentSelectFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/loginStudentSelectContent"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/loginStudentSelectContact"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible">
<View
android:id="@+id/loginStudentSelectContactTopDivider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider" />
<TextView
android:id="@+id/loginStudentSelectContactHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="32dp"
android:layout_marginRight="32dp"
android:layout_marginBottom="16dp"
android:gravity="center_horizontal"
android:text="@string/login_contact_header"
android:textSize="14sp"
app:fontFamily="sans-serif-medium" />
<LinearLayout
android:id="@+id/loginStudentSelectContactButtons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/loginStudentSelectContactEmail"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:layout_weight="1"
android:text="@string/login_contact_email"
app:icon="@drawable/ic_more_messages" />
<com.google.android.material.button.MaterialButton
android:id="@+id/loginStudentSelectContactDiscord"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_weight="1"
android:text="@string/login_contact_discord"
app:icon="@drawable/ic_about_discord" />
</LinearLayout>
<View
android:id="@+id/loginStudentSelectContactBottomDivider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="?android:attr/listDivider" />
</LinearLayout>
<TextView
android:id="@+id/loginStudentSelectHeader"
android:layout_width="match_parent"
@ -106,22 +28,24 @@
app:layout_constraintBottom_toTopOf="@id/loginStudentSelectRecycler"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/loginStudentSelectContact"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/loginStudentSelectRecycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="144dp"
android:fadingEdge="vertical"
android:fadingEdgeLength="100dp"
android:requiresFadingEdge="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constrainedHeight="true"
app:layout_constraintBottom_toTopOf="@id/loginStudentSelectSignIn"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_max="432dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/loginStudentSelectHeader"
tools:itemCount="6"
tools:listitem="@layout/item_login_student_select" />
tools:itemCount="33"
tools:listitem="@layout/item_login_student_select_student" />
<com.google.android.material.button.MaterialButton
android:id="@+id/loginStudentSelectSignIn"
@ -136,4 +60,12 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/loginStudentSelectRecycler" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/loginStudentSelectProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="gone" />
</FrameLayout>

View File

@ -0,0 +1,69 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.modules.notifications.NotificationsFragment">
<ImageView
android:id="@+id/notifications_header_icon"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginTop="32dp"
android:src="@drawable/ic_settings_notifications"
app:layout_constraintBottom_toTopOf="@id/notifications_header_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.4"
app:layout_constraintVertical_chainStyle="packed"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/notifications_header_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginVertical="16dp"
android:gravity="center_horizontal"
android:text="@string/notifications_header_title"
android:textColor="?android:textColorPrimary"
android:textSize="24sp"
app:layout_constraintBottom_toTopOf="@id/notifications_header_description"
app:layout_constraintTop_toBottomOf="@id/notifications_header_icon"
app:lineHeight="30sp" />
<TextView
android:id="@+id/notifications_header_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:gravity="center_horizontal"
android:text="@string/notifications_header_description"
android:textColor="?android:textColorSecondary"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@id/notifications_skip"
app:layout_constraintTop_toBottomOf="@id/notifications_header_title"
app:lineHeight="24sp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/notifications_skip"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/notifications_skip"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/notifications_enable"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/notifications_enable"
app:icon="@drawable/ic_settings_notifications"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,38 @@
<?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:layout_marginTop="8dp"
android:background="?selectableItemBackground"
android:paddingHorizontal="16dp">
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/login_student_select_empty_symbol_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="16dp"
android:text="@string/login_other_search_locations"
android:textColor="?android:textColorPrimary"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/login_student_select_empty_symbol_chevron"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginEnd="16dp"
android:rotation="90"
android:src="@drawable/ic_chevron_right"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?android:textColorPrimary"
tools:ignore="ContentDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,38 @@
<?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:paddingHorizontal="24dp"
android:paddingVertical="8dp">
<TextView
android:id="@+id/login_student_select_header_school_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?android:textColorPrimary"
app:layout_constraintTop_toTopOf="parent"
tools:text="Publiczna szkoła Wulkanowego" />
<TextView
android:id="@+id/login_student_select_header_school_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="@string/login_no_active_student"
android:textColor="?colorTimetableChange"
app:layout_constraintTop_toBottomOf="@id/login_student_select_header_school_name" />
<TextView
android:id="@+id/login_student_select_header_school_error"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:ellipsize="end"
android:maxLines="2"
android:textColor="?colorError"
app:layout_constraintTop_toBottomOf="@id/login_student_select_header_school_details"
tools:text="@tools:sample/lorem/random" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,38 @@
<?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:paddingHorizontal="16dp"
android:paddingVertical="8dp">
<TextView
android:id="@+id/login_student_select_header_symbol_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?android:textColorPrimary"
app:layout_constraintTop_toTopOf="parent"
tools:text="powiatjaroslawski" />
<TextView
android:id="@+id/login_student_select_header_symbol_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textColor="?android:textColorSecondary"
app:layout_constraintTop_toBottomOf="@id/login_student_select_header_symbol_value"
tools:text="Jan Kowalski" />
<TextView
android:id="@+id/login_student_select_header_symbol_error"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:ellipsize="end"
android:maxLines="2"
android:textColor="?colorError"
app:layout_constraintTop_toBottomOf="@id/login_student_select_header_symbol_username"
tools:text="@tools:sample/lorem/random" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,61 @@
<?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:paddingHorizontal="16dp"
android:paddingVertical="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/login_student_select_help_symbol"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/login_symbol_enter"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<TextView
android:id="@+id/login_student_select_help_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/login_contact_header"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/login_student_select_help_mail"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/login_student_select_help_symbol" />
<com.google.android.material.button.MaterialButton
android:id="@+id/login_student_select_help_mail"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:contentDescription="@string/login_contact_email"
app:icon="@drawable/ic_more_messages"
app:iconGravity="textStart"
app:iconPadding="0dp"
app:layout_constraintEnd_toEndOf="@id/login_student_select_help_title"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/login_student_select_help_title"
app:layout_constraintTop_toBottomOf="@id/login_student_select_help_symbol" />
<com.google.android.material.button.MaterialButton
android:id="@+id/login_student_select_help_discord"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:contentDescription="@string/login_contact_discord"
app:icon="@drawable/ic_about_discord"
app:iconGravity="textStart"
app:iconPadding="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/login_student_select_help_mail"
app:layout_constraintTop_toTopOf="@id/login_student_select_help_mail" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -4,7 +4,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:minHeight="72dp"
android:minHeight="56dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
tools:context=".ui.modules.login.studentselect.LoginStudentSelectAdapter">
@ -14,9 +14,10 @@
android:layout_width="32dp"
android:layout_height="24dp"
android:layout_centerVertical="true"
android:layout_marginStart="12dp"
android:layout_marginStart="32dp"
android:layout_marginEnd="28dp"
android:background="@android:color/transparent"
android:clickable="false"
tools:text=" " />
<TextView
@ -32,34 +33,18 @@
tools:text="@tools:sample/full_names" />
<TextView
android:id="@+id/loginItemSchool"
android:id="@+id/loginItemSignedIn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/loginItemName"
android:layout_marginEnd="16dp"
android:layout_toEndOf="@id/loginItemCheck"
android:ellipsize="end"
android:gravity="bottom"
android:maxLines="2"
android:textColor="?android:textColorSecondary"
android:textSize="14sp"
tools:text="@tools:sample/lorem/random" />
<TextView
android:id="@+id/loginItemSignedIn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="20dp"
android:layout_below="@id/loginItemSchool"
android:layout_marginEnd="16dp"
android:layout_toEndOf="@id/loginItemCheck"
android:ellipsize="end"
android:enabled="false"
android:gravity="bottom"
android:maxLines="1"
android:minHeight="20dp"
android:text="@string/login_signed_in"
android:textColor="?android:textColorSecondary"
android:textSize="14sp"
android:visibility="gone"
tools:visibility="visible" />
android:textSize="14sp" />
</RelativeLayout>

View File

@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/colorPrimary" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
<monochrome android:drawable="@drawable/ic_launcher_foreground_mono" />
</adaptive-icon>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/colorPrimary" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -55,7 +55,7 @@
<string name="login_invalid_symbol">Neplatný symbol</string>
<string name="login_incorrect_symbol">Žák nebyl nalezen. Zkontrolujte správnost symbolu a vybrané varianty deníku UONET+</string>
<string name="login_duplicate_student">Vybraný žák je už přihlášen</string>
<string name="login_symbol_helper">Symbol najdete na stránce deníku v &#160;<b>Uczeń</b>&#160;<b>Dostęp Mobilny</b>&#160;<b>Zarejestruj urządzenie mobilne</b>.\n\nUjistěte se, že jste na předchozí obrazovce nastavili správnou variantu deníku do pole <b>Variace deníku UONET+</b></string>
<string name="login_symbol_helper">Symbol najdete na stránce deníku v &#160;<b>Uczeń</b>&#160;<b>Dostęp Mobilny</b>&#160;<b>Wygeneruj kod dostępu</b>.\n\nUjistěte se, že jste nastavili správnou variantu deníku v poli <b>Variace deníku UONET+</b> na první přihlašovací obrazovce</string>
<string name="login_select_student">Vyberte žáky, kteří se mají do aplikace přihlásit</string>
<string name="login_advanced">Jiné možnosti</string>
<string name="login_advanced_warning_mobile_api">V tomto režimu nefungují následující: šťastné číslo, statistiky třídy, shrnutí frekvencí, ospravedlnění nepřítomnosti, dokončené lekce, informace o škole a prohlížení seznamu registrovaných zařízení</string>
@ -72,6 +72,14 @@
<string name="login_recover">Obnovit</string>
<string name="login_signed_in">Žák je už přihlášen</string>
<string name="login_host_standard">Standardní</string>
<string name="login_other_search_locations">Jiná místa vyhledávání</string>
<string name="login_no_active_student">Nebyli nalezeni žádní aktivní žáci</string>
<string name="login_symbol_enter">Zadejte jiný symbol</string>
<!--Notifications-->
<string name="notifications_header_title">Povolit oznámení</string>
<string name="notifications_header_description">Povolit upozornění, abyste nezmeškali zprávu od učitele nebo o nové známce</string>
<string name="notifications_skip">Přeskočit</string>
<string name="notifications_enable">Zapnout</string>
<!--Main-->
<string name="main_account_picker">Manažer účtů</string>
<string name="main_log_in">Přihlásit se</string>
@ -485,6 +493,8 @@
</plurals>
<string name="conferences_present">Přítomnost na setkání</string>
<string name="conference_agenda">Agenda</string>
<string name="conference_place">Místo</string>
<string name="conference_topic">Téma</string>
<!--Director information-->
<string name="school_announcement_title">Školní oznámení</string>
<string name="school_announcement_no_items">Žádná školní oznámení</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,728 @@
<?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>Wygeneruj kod dostępu</b>.\n\nMake sure that you have set the appropriate register variant in the <b>UONET+ register variant</b> field on the first login 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>
<string name="login_other_search_locations">Other search locations</string>
<string name="login_no_active_student">No active students found</string>
<string name="login_symbol_enter">Enter a different symbol</string>
<!--Notifications-->
<string name="notifications_header_title">Enable notifications</string>
<string name="notifications_header_description">Enable notifications so you don\'t miss message from teacher or new grade</string>
<string name="notifications_skip">Skip</string>
<string name="notifications_enable">Enable</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>
<string name="conference_place">Place</string>
<string name="conference_topic">Topic</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

@ -55,7 +55,7 @@
<string name="login_invalid_symbol">Ungültige symbol</string>
<string name="login_incorrect_symbol">Schüler nicht gefunden. Überprüfen Sie das Symbol und die gewählte Variation des UONET+ Registers</string>
<string name="login_duplicate_student">Ausgewählter Student ist bereits angemeldet.</string>
<string name="login_symbol_helper">Das Symbol kann auf der Registerseite in&#160;<b>Uczeń</b>&#160;<b>Dostęp Mobilny</b>&#160;<b>Zarejestruj urządzenie mobilne</b>gefunden werden.\n\nStellen Sie sicher, dass Sie die entsprechende Registervariante im Feld <b>UONET+ Registervariante</b> auf dem vorherigen Bildschirm festgelegt haben</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>Wygeneruj kod dostępu</b>.\n\nMake sure that you have set the appropriate register variant in the <b>UONET+ register variant</b> field on the first login screen</string>
<string name="login_select_student">Wählen Sie die Studenten aus, die sich bei der Anwendung anmelden sollen</string>
<string name="login_advanced">Andere Optionen</string>
<string name="login_advanced_warning_mobile_api">In diesem Modus funktioniert eine Glücknummer, eine Klassenstatistik, eine Zusammenfassung der Anwesenheit, eine Entschuldigung für die Abwesenheit, abgeschlossene Lektionen, Schulinformationen und eine Vorschau der Liste der registrierten Geräte nicht</string>
@ -72,6 +72,14 @@
<string name="login_recover">Wiederherstellen</string>
<string name="login_signed_in">Student ist bereits angemeldet</string>
<string name="login_host_standard">Standard</string>
<string name="login_other_search_locations">Other search locations</string>
<string name="login_no_active_student">No active students found</string>
<string name="login_symbol_enter">Enter a different symbol</string>
<!--Notifications-->
<string name="notifications_header_title">Enable notifications</string>
<string name="notifications_header_description">Enable notifications so you don\'t miss message from teacher or new grade</string>
<string name="notifications_skip">Skip</string>
<string name="notifications_enable">Enable</string>
<!--Main-->
<string name="main_account_picker">Kundenbetreuer</string>
<string name="main_log_in">Anmelden</string>
@ -275,7 +283,7 @@
<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_all_mailboxes">Alle postfächer</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>
@ -299,7 +307,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>
<string name="message_mailbox_chooser_title">Postfach auswählen</string>
<!--Note-->
<string name="note_no_items">Keine Informationen über Eintragen</string>
<string name="note_points">Punkte</string>
@ -413,6 +421,8 @@
</plurals>
<string name="conferences_present">Teilnahme an einem Meeting</string>
<string name="conference_agenda">Agenda</string>
<string name="conference_place">Place</string>
<string name="conference_topic">Topic</string>
<!--Director information-->
<string name="school_announcement_title">Schulankündigungen</string>
<string name="school_announcement_no_items">Keine schulankündigungen</string>

View File

@ -55,7 +55,7 @@
<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_symbol_helper">The symbol can be found on the register page in&#160;<b>Uczeń</b> →&#160;<b>Dostęp Mobilny</b>&#160;<b>Wygeneruj kod dostępu</b>.\n\nMake sure that you have set the appropriate register variant in the <b>UONET+ register variant</b> field on the first login 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>
@ -72,6 +72,14 @@
<string name="login_recover">Recover</string>
<string name="login_signed_in">Student is already signed in</string>
<string name="login_host_standard">Standard</string>
<string name="login_other_search_locations">Other search locations</string>
<string name="login_no_active_student">No active students found</string>
<string name="login_symbol_enter">Enter a different symbol</string>
<!--Notifications-->
<string name="notifications_header_title">Enable notifications</string>
<string name="notifications_header_description">Enable notifications so you don\'t miss message from teacher or new grade</string>
<string name="notifications_skip">Skip</string>
<string name="notifications_enable">Enable</string>
<!--Main-->
<string name="main_account_picker">Account manager</string>
<string name="main_log_in">Log in</string>
@ -413,6 +421,8 @@
</plurals>
<string name="conferences_present">Present at conference</string>
<string name="conference_agenda">Agenda</string>
<string name="conference_place">Place</string>
<string name="conference_topic">Topic</string>
<!--Director information-->
<string name="school_announcement_title">School announcements</string>
<string name="school_announcement_no_items">No school announcements</string>

View File

@ -55,7 +55,7 @@
<string name="login_invalid_symbol">Nieprawidłowy symbol</string>
<string name="login_incorrect_symbol">Nie znaleziono ucznia. Sprawdź poprawność symbolu i wybranej odmiany dziennika UONET+</string>
<string name="login_duplicate_student">Wybrany uczeń jest już zalogowany</string>
<string name="login_symbol_helper">Symbol znajdziesz na stronie dziennika w&#160;<b>Uczeń</b> &#160;<b>Dostęp Mobilny</b>&#160;<b>Zarejestruj urządzenie mobilne</b>.\n\nUpewnij się, że w polu <b>Dziennik UONET+</b> na poprzednim ekranie została ustawiona odpowiednia odmiana dziennika</string>
<string name="login_symbol_helper">Symbol można znaleźć na stronie dziennika w&#160;<b>Uczeń</b>&#160;<b>Dostęp Mobilny</b>&#160;<b>Wygeneruj kod dostępu</b>.\n\nUpewnij się, że ustawiłeś odpowiednią odmianę dziennika w polu <b>Odmiana dziennika UONET+</b> na pierwszym ekranie logowania</string>
<string name="login_select_student">Wybierz uczniów do zalogowania w aplikacji</string>
<string name="login_advanced">Inne opcje</string>
<string name="login_advanced_warning_mobile_api">W tym trybie nie działa szczęśliwy numerek, uczeń na tle klasy, podsumowanie frekwencji, usprawiedliwianie nieobecności, lekcje zrealizowane, informacje o szkole i podgląd listy zarejestrowanych urządzeń</string>
@ -72,6 +72,14 @@
<string name="login_recover">Przywróć</string>
<string name="login_signed_in">Uczeń jest już zalogowany</string>
<string name="login_host_standard">Standardowa</string>
<string name="login_other_search_locations">Inne lokalizacje wyszukiwania</string>
<string name="login_no_active_student">Nie znaleziono aktywnych uczniów</string>
<string name="login_symbol_enter">Wprowadź inny symbol</string>
<!--Notifications-->
<string name="notifications_header_title">Włącz powiadomienia</string>
<string name="notifications_header_description">Włącz powiadomienia, aby nie przegapić wiadomości od nauczyciela lub nowej oceny</string>
<string name="notifications_skip">Pomiń</string>
<string name="notifications_enable">Włącz</string>
<!--Main-->
<string name="main_account_picker">Menadżer kont</string>
<string name="main_log_in">Zaloguj się</string>
@ -485,6 +493,8 @@
</plurals>
<string name="conferences_present">Obecność na zebraniu</string>
<string name="conference_agenda">Agenda</string>
<string name="conference_place">Miejsce</string>
<string name="conference_topic">Temat</string>
<!--Director information-->
<string name="school_announcement_title">Ogłoszenia szkolne</string>
<string name="school_announcement_no_items">Brak ogłoszeń szkolnych</string>

View File

@ -55,7 +55,7 @@
<string name="login_invalid_symbol">Неверный symbol</string>
<string name="login_incorrect_symbol">Ученик не найден. Проверьте symbol и выбранный тип дненика UONET+</string>
<string name="login_duplicate_student">Данный ученик уже авторизован</string>
<string name="login_symbol_helper">Symbol можно найти на странице регистрации в &#160;<b>Uczeń</b> →&#160;<b>Dostęp Mobilny</b>&#160;<b>Zarejestruj urządzenie mobilne</b>.\n\nУбедитесь, что вы выбрали соответствующий тип дневника в поле <b>Тип дневника UONET+</b> на предыдущем экране</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>Wygeneruj kod dostępu</b>.\n\nMake sure that you have set the appropriate register variant in the <b>UONET+ register variant</b> field on the first login screen</string>
<string name="login_select_student">Выберите учеников для авторизации в приложении</string>
<string name="login_advanced">Другие варианты</string>
<string name="login_advanced_warning_mobile_api">В этом режиме не работают: счастливый номер, статистика класса по оценкам, статистика посещаемости и уроков, информация о школе и список зарегистрированных устройств</string>
@ -72,6 +72,14 @@
<string name="login_recover">Восстановить</string>
<string name="login_signed_in">Ученик уже авторизован</string>
<string name="login_host_standard">Стандартный</string>
<string name="login_other_search_locations">Other search locations</string>
<string name="login_no_active_student">No active students found</string>
<string name="login_symbol_enter">Enter a different symbol</string>
<!--Notifications-->
<string name="notifications_header_title">Enable notifications</string>
<string name="notifications_header_description">Enable notifications so you don\'t miss message from teacher or new grade</string>
<string name="notifications_skip">Skip</string>
<string name="notifications_enable">Enable</string>
<!--Main-->
<string name="main_account_picker">Менеджер аккаунтов</string>
<string name="main_log_in">Войти</string>
@ -485,6 +493,8 @@
</plurals>
<string name="conferences_present">Присутствует на встрече</string>
<string name="conference_agenda">Повестка дня</string>
<string name="conference_place">Place</string>
<string name="conference_topic">Topic</string>
<!--Director information-->
<string name="school_announcement_title">Объявления школы</string>
<string name="school_announcement_no_items">Нет школьных объявлений</string>

View File

@ -35,7 +35,7 @@
<string name="login_email_hint">Email</string>
<string name="login_login_pesel_email_hint">Prihlásenie, číslo PESEL alebo e-mail</string>
<string name="login_password_hint">Heslo</string>
<string name="login_host_hint">Variácie denníka UONET+</string>
<string name="login_host_hint">Variácia denníka UONET+</string>
<string name="login_type_api">Mobile API</string>
<string name="login_type_scrapper">Scraper</string>
<string name="login_type_hybrid">Hybridné</string>
@ -55,7 +55,7 @@
<string name="login_invalid_symbol">Neplatný symbol</string>
<string name="login_incorrect_symbol">Žiak nebol nájdený. Skontrolujte správnosť symbolu a vybrané varianty denníka UONET+</string>
<string name="login_duplicate_student">Vybraný žiak už je prihlásený</string>
<string name="login_symbol_helper">Symbol nájdete na stránke denníka v &#160;<b>Uczeń</b>&#160;<b>Dostęp Mobilny</b>&#160;<b>Zarejestruj urządzenie mobilne</b>.\n\nUistite sa, že ste na predchádzajúcu obrazovke nastaviť správny variant denníka do poľa <b>Variácie denníka UONET+</b></string>
<string name="login_symbol_helper">Symbol nájdete na stránke denníka v &#160;<b>Uczeń</b>&#160;<b>Dostęp Mobilny</b>&#160;<b>Wygeneruj kod dostępu</b>.\n\nUistite sa, že ste nastavili správny variant denníka v poli <b>Variácia denníka UONET+</b> na prvej prihlasovacej obrazovke</string>
<string name="login_select_student">Vyberte žiakov, ktorí sa majú do aplikácie prihlásiť</string>
<string name="login_advanced">Iné možnosti</string>
<string name="login_advanced_warning_mobile_api">V tomto režime nefungujú nasledovné: šťastné číslo, štatistiky triedy, zhrnutie frekvencií, ospravedlnenie neprítomnosti, dokončené lekcie, informácie o škole a prezeranie zoznamu registrovaných zariadení</string>
@ -72,6 +72,14 @@
<string name="login_recover">Obnoviť</string>
<string name="login_signed_in">Žiak je už prihlásený</string>
<string name="login_host_standard">Štandardná</string>
<string name="login_other_search_locations">Iné miesta vyhľadávania</string>
<string name="login_no_active_student">Neboli nájdení žiadni aktívni žiaci</string>
<string name="login_symbol_enter">Zadajte iný symbol</string>
<!--Notifications-->
<string name="notifications_header_title">Povoliť oznámenia</string>
<string name="notifications_header_description">Povoliť oznámenia, aby ste nezmeškali správu od učiteľa alebo o novej známke</string>
<string name="notifications_skip">Preskočiť</string>
<string name="notifications_enable">Zapnúť</string>
<!--Main-->
<string name="main_account_picker">Manažér účtov</string>
<string name="main_log_in">Prihlásiť sa</string>
@ -485,6 +493,8 @@
</plurals>
<string name="conferences_present">Prítomnosť na stretnutí</string>
<string name="conference_agenda">Agenda</string>
<string name="conference_place">Miesto</string>
<string name="conference_topic">Téma</string>
<!--Director information-->
<string name="school_announcement_title">Školské oznámenia</string>
<string name="school_announcement_no_items">Žiadne školské oznámenia</string>

View File

@ -55,7 +55,7 @@
<string name="login_invalid_symbol">Неправильний symbol</string>
<string name="login_incorrect_symbol">Студента не знайдено. Перевірте symbol та обранний тип щоденника UONET+</string>
<string name="login_duplicate_student">Цього учня вже авторизовано</string>
<string name="login_symbol_helper">Symbol можна знайти на сторінці реєстрації в &#160;<b>Uczeń</b>&#160;<b>Dostęp Mobilny</b>&#160;<b>Zarejestruj urządzenie mobilne</b>.\n\nПереконайтесь, що ви встановили відповідний тип щоденника в полі <b>Тип щоденника UONET+</b> на попередньому екрані</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>Wygeneruj kod dostępu</b>.\n\nMake sure that you have set the appropriate register variant in the <b>UONET+ register variant</b> field on the first login screen</string>
<string name="login_select_student">Виберіть учнів для авторизації в додатку</string>
<string name="login_advanced">Інші варіанти</string>
<string name="login_advanced_warning_mobile_api">У цьому режимі не працюють: щасливий номер, статистика класу по оцінкам, статистика відвідуваності та уроків, інформація про школу та список зареєстрованих пристроїв</string>
@ -72,6 +72,14 @@
<string name="login_recover">Відновити</string>
<string name="login_signed_in">Учня вже авторизовано</string>
<string name="login_host_standard">Стандартний</string>
<string name="login_other_search_locations">Other search locations</string>
<string name="login_no_active_student">No active students found</string>
<string name="login_symbol_enter">Enter a different symbol</string>
<!--Notifications-->
<string name="notifications_header_title">Enable notifications</string>
<string name="notifications_header_description">Enable notifications so you don\'t miss message from teacher or new grade</string>
<string name="notifications_skip">Skip</string>
<string name="notifications_enable">Enable</string>
<!--Main-->
<string name="main_account_picker">Змінити облікові записи</string>
<string name="main_log_in">Увійти</string>
@ -485,6 +493,8 @@
</plurals>
<string name="conferences_present">Присутність на зустрічі</string>
<string name="conference_agenda">Порядок денний</string>
<string name="conference_place">Place</string>
<string name="conference_topic">Topic</string>
<!--Director information-->
<string name="school_announcement_title">Оголошення школи</string>
<string name="school_announcement_no_items">Немає шкільних оголошень</string>

View File

@ -42,7 +42,7 @@
<item>https://vulcan.net.pl/?login</item>
<item>https://vulcan.net.pl/?login</item>
<item>https://vulcan.net.pl/?login</item>
<item>http://fakelog.cf/?email</item>
<item>https://fakelog.cf/?email</item>
</string-array>
<string-array name="hosts_symbols">
<item>Default</item>

View File

@ -55,7 +55,7 @@
<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_symbol_helper">The symbol can be found on the register page in&#160;<b>Uczeń</b> →&#160;<b>Dostęp Mobilny</b>&#160;<b>Wygeneruj kod dostępu</b>.\n\nMake sure that you have set the appropriate register variant in the <b>UONET+ register variant</b> field on the first login 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>
@ -74,6 +74,14 @@
<string name="login_recover">Recover</string>
<string name="login_signed_in">Student is already signed in</string>
<string name="login_host_standard">Standard</string>
<string name="login_other_search_locations">Other search locations</string>
<string name="login_no_active_student">No active students found</string>
<string name="login_symbol_enter">Enter a different symbol</string>
<!--Notifications-->
<string name="notifications_header_title">Enable notifications</string>
<string name="notifications_header_description">Enable notifications so you don\'t miss message from teacher or new grade</string>
<string name="notifications_skip">Skip</string>
<string name="notifications_enable">Enable</string>
<!--Main-->
<string name="main_account_picker">Account manager</string>
<string name="main_log_in">Log in</string>
@ -417,6 +425,8 @@
</plurals>
<string name="conferences_present">Present at conference</string>
<string name="conference_agenda">Agenda</string>
<string name="conference_place">Place</string>
<string name="conference_topic">Topic</string>
<!--Director information-->
<string name="school_announcement_title">School announcements</string>
<string name="school_announcement_no_items">No school announcements</string>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup>
<exclude domain="file" />
<exclude domain="database" />
<exclude domain="sharedpref" />
<exclude domain="external" />
<exclude domain="root" />
</cloud-backup>
<device-transfer>
<exclude domain="file" />
<exclude domain="database" />
<exclude domain="sharedpref" />
<exclude domain="external" />
<exclude domain="root" />
</device-transfer>
</data-extraction-rules>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true"/>
<debug-overrides>
<trust-anchors>
<certificates src="user" />

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