Merge branch 'release/2.1.0'

This commit is contained in:
Mikołaj Pich 2023-08-25 00:01:42 +02:00
commit af346842a3
87 changed files with 4793 additions and 457 deletions

View File

@ -1,8 +1,11 @@
import com.github.triplet.gradle.androidpublisher.ReleaseStatus
import ru.cian.huawei.publish.ReleaseNote
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization' apply plugin: 'kotlinx-serialization'
apply plugin: 'kotlin-parcelize' apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlin-kapt' apply plugin: 'com.google.devtools.ksp'
apply plugin: 'dagger.hilt.android.plugin' apply plugin: 'dagger.hilt.android.plugin'
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics' apply plugin: 'com.google.firebase.crashlytics'
@ -10,21 +13,22 @@ apply plugin: 'com.github.triplet.play'
apply plugin: 'ru.cian.huawei-publish' apply plugin: 'ru.cian.huawei-publish'
apply plugin: 'com.mikepenz.aboutlibraries.plugin' apply plugin: 'com.mikepenz.aboutlibraries.plugin'
apply plugin: 'com.huawei.agconnect' apply plugin: 'com.huawei.agconnect'
apply plugin: 'kotlin-kapt'
apply from: 'jacoco.gradle' apply from: 'jacoco.gradle'
apply from: 'sonarqube.gradle' apply from: 'sonarqube.gradle'
apply from: 'hooks.gradle' apply from: 'hooks.gradle'
android { android {
namespace 'io.github.wulkanowy' namespace 'io.github.wulkanowy'
compileSdkVersion 33 compileSdk 33
defaultConfig { defaultConfig {
applicationId "io.github.wulkanowy" applicationId "io.github.wulkanowy"
testApplicationId "io.github.tests.wulkanowy" testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 33 targetSdkVersion 33
versionCode 130 versionCode 131
versionName "2.0.8" versionName "2.1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "app_name", "Wulkanowy" resValue "string", "app_name", "Wulkanowy"
@ -33,14 +37,6 @@ android {
firebase_enabled: project.hasProperty("enableFirebase"), firebase_enabled: project.hasProperty("enableFirebase"),
admob_project_id: "" admob_project_id: ""
] ]
javaCompileOptions {
annotationProcessorOptions {
arguments += [
"room.schemaLocation": "$projectDir/schemas".toString(),
"room.incremental" : "true"
]
}
}
buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "null" buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "null"
buildConfigField "String", "DASHBOARD_TILE_AD_ID", "null" buildConfigField "String", "DASHBOARD_TILE_AD_ID", "null"
@ -85,7 +81,7 @@ android {
} }
} }
flavorDimensions "platform" flavorDimensions += "platform"
productFlavors { productFlavors {
hms { hms {
@ -124,20 +120,20 @@ android {
} }
} }
testOptions.unitTests { testOptions {
includeAndroidResources = true unitTests.includeAndroidResources = true
// workaround HMS test errors https://github.com/robolectric/robolectric/issues/2750 // workaround HMS test errors https://github.com/robolectric/robolectric/issues/2750
all { jvmArgs '-noverify' } unitTests.all { jvmArgs '-noverify' }
} }
compileOptions { compileOptions {
coreLibraryDesugaringEnabled true coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_11 sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_17
} }
kotlinOptions { kotlinOptions {
jvmTarget = "11" jvmTarget = "17"
freeCompilerArgs += ["-opt-in=kotlin.RequiresOptIn", "-Xjvm-default=all"] freeCompilerArgs += ["-opt-in=kotlin.RequiresOptIn", "-Xjvm-default=all"]
} }
@ -156,17 +152,16 @@ android {
kapt { kapt {
correctErrorTypes true correctErrorTypes true
} }
ksp {
kotlin { arg("room.schemaLocation", "$projectDir/schemas".toString())
jvmToolchain(11)
} }
play { play {
defaultToAppBundles = false defaultToAppBundles = false
track = 'production' track = 'production'
releaseStatus = com.github.triplet.gradle.androidpublisher.ReleaseStatus.IN_PROGRESS releaseStatus = ReleaseStatus.IN_PROGRESS
userFraction = 0.25d userFraction = 0.25d
updatePriority = 1 updatePriority = 3
enabled.set(false) enabled.set(false)
} }
@ -177,7 +172,7 @@ huaweiPublish {
buildFormat = "aab" buildFormat = "aab"
deployType = "publish" deployType = "publish"
releaseNotes = [ releaseNotes = [
new ru.cian.huawei.publish.ReleaseNote( new ReleaseNote(
"pl-PL", "pl-PL",
"$projectDir/src/main/play/release-notes/pl-PL/default.txt" "$projectDir/src/main/play/release-notes/pl-PL/default.txt"
) )
@ -189,14 +184,14 @@ huaweiPublish {
ext { ext {
work_manager = "2.8.1" work_manager = "2.8.1"
android_hilt = "1.0.0" android_hilt = "1.0.0"
room = "2.5.1" room = "2.5.2"
chucker = "3.5.2" chucker = "3.5.2"
mockk = "1.13.5" mockk = "1.13.7"
coroutines = "1.7.1" coroutines = "1.7.3"
} }
dependencies { dependencies {
implementation 'io.github.wulkanowy:sdk:2.0.8' implementation 'io.github.wulkanowy:sdk:2.1.0'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
@ -207,11 +202,11 @@ dependencies {
implementation 'androidx.core:core-splashscreen:1.0.1' implementation 'androidx.core:core-splashscreen:1.0.1'
implementation "androidx.activity:activity-ktx:1.7.2" implementation "androidx.activity:activity-ktx:1.7.2"
implementation "androidx.appcompat:appcompat:1.6.1" implementation "androidx.appcompat:appcompat:1.6.1"
implementation "androidx.fragment:fragment-ktx:1.5.7" implementation "androidx.fragment:fragment-ktx:1.6.1"
implementation "androidx.annotation:annotation:1.6.0" implementation "androidx.annotation:annotation:1.6.0"
implementation "androidx.preference:preference-ktx:1.2.0" implementation "androidx.preference:preference-ktx:1.2.1"
implementation "androidx.recyclerview:recyclerview:1.3.0" implementation "androidx.recyclerview:recyclerview:1.3.1"
implementation "androidx.viewpager2:viewpager2:1.1.0-beta02" implementation "androidx.viewpager2:viewpager2:1.1.0-beta02"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.constraintlayout:constraintlayout:2.1.4" implementation "androidx.constraintlayout:constraintlayout:2.1.4"
@ -228,7 +223,7 @@ dependencies {
implementation "androidx.room:room-runtime:$room" implementation "androidx.room:room-runtime:$room"
implementation "androidx.room:room-ktx:$room" implementation "androidx.room:room-ktx:$room"
kapt "androidx.room:room-compiler:$room" ksp "androidx.room:room-compiler:$room"
implementation "com.google.dagger:hilt-android:$hilt_version" implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version" kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
@ -252,17 +247,17 @@ dependencies {
implementation 'com.fredporciuncula:flow-preferences:1.9.1' implementation 'com.fredporciuncula:flow-preferences:1.9.1'
implementation 'org.apache.commons:commons-text:1.10.0' implementation 'org.apache.commons:commons-text:1.10.0'
playImplementation platform('com.google.firebase:firebase-bom:32.1.0') playImplementation platform('com.google.firebase:firebase-bom:32.2.2')
playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-analytics-ktx'
playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-messaging:'
playImplementation 'com.google.firebase:firebase-crashlytics:' playImplementation 'com.google.firebase:firebase-crashlytics:'
playImplementation 'com.google.firebase:firebase-config-ktx' playImplementation 'com.google.firebase:firebase-config-ktx'
playImplementation 'com.google.android.play:core:1.10.3' playImplementation 'com.google.android.play:core:1.10.3'
playImplementation 'com.google.android.play:core-ktx:1.8.1' playImplementation 'com.google.android.play:core-ktx:1.8.1'
playImplementation 'com.google.android.gms:play-services-ads:22.1.0' playImplementation 'com.google.android.gms:play-services-ads:22.2.0'
hmsImplementation 'com.huawei.hms:hianalytics:6.10.0.301' hmsImplementation 'com.huawei.hms:hianalytics:6.10.0.303'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.0.300' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.1.300'
releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker"

File diff suppressed because it is too large Load Diff

View File

@ -50,5 +50,9 @@
{ {
"displayName": "Tomasz F.", "displayName": "Tomasz F.",
"githubUsername": "Pengwius" "githubUsername": "Pengwius"
},
{
"displayName": "Antoni Paduch",
"githubUsername": "janAte1"
} }
] ]

View File

@ -148,7 +148,7 @@ inline fun <ResultType, RequestType, T> networkBoundResource(
crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit, crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit,
crossinline onFetchFailed: (Throwable) -> Unit = { }, crossinline onFetchFailed: (Throwable) -> Unit = { },
crossinline shouldFetch: (ResultType) -> Boolean = { true }, crossinline shouldFetch: (ResultType) -> Boolean = { true },
crossinline mapResult: (ResultType) -> T crossinline mapResult: (ResultType) -> T,
) = flow { ) = flow {
emit(Resource.Loading()) emit(Resource.Loading())

View File

@ -50,6 +50,7 @@ import javax.inject.Singleton
AutoMigration(from = 51, to = 52), AutoMigration(from = 51, to = 52),
AutoMigration(from = 54, to = 55, spec = Migration55::class), AutoMigration(from = 54, to = 55, spec = Migration55::class),
AutoMigration(from = 55, to = 56), AutoMigration(from = 55, to = 56),
AutoMigration(from = 56, to = 57, spec = Migration57::class),
], ],
version = AppDatabase.VERSION_SCHEMA, version = AppDatabase.VERSION_SCHEMA,
exportSchema = true exportSchema = true
@ -58,7 +59,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
companion object { companion object {
const val VERSION_SCHEMA = 56 const val VERSION_SCHEMA = 57
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(), Migration2(),

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.data.db package io.github.wulkanowy.data.db
import androidx.room.TypeConverter import androidx.room.TypeConverter
import io.github.wulkanowy.data.enums.MessageType
import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.utils.toTimestamp import io.github.wulkanowy.utils.toTimestamp
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
@ -68,4 +69,9 @@ class Converters {
@TypeConverter @TypeConverter
fun stringToDestination(destination: String): Destination = json.decodeFromString(destination) fun stringToDestination(destination: String): Destination = json.decodeFromString(destination)
@TypeConverter
fun messageTypesToString(types: List<MessageType>): String = json.encodeToString(types)
@TypeConverter
fun stringToMessageTypes(text: String): List<MessageType> = json.decodeFromString(text)
} }

View File

@ -22,4 +22,4 @@ abstract class AdminMessageDao : BaseDao<AdminMessage> {
deleteAll(oldMessages) deleteAll(oldMessages)
insertAll(newMessages) insertAll(newMessages)
} }
} }

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import io.github.wulkanowy.data.enums.MessageType
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
@ -33,7 +34,8 @@ data class AdminMessage(
val priority: String, val priority: String,
val type: String, @ColumnInfo(name = "types", defaultValue = "[]")
val types: List<MessageType> = emptyList(),
@ColumnInfo(name = "is_dismissible") @ColumnInfo(name = "is_dismissible")
val isDismissible: Boolean = false val isDismissible: Boolean = false

View File

@ -0,0 +1,10 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.DeleteColumn
import androidx.room.migration.AutoMigrationSpec
@DeleteColumn(
tableName = "AdminMessages",
columnName = "type",
)
class Migration57 : AutoMigrationSpec

View File

@ -0,0 +1,9 @@
package io.github.wulkanowy.data.enums
enum class MessageType {
GENERAL_MESSAGE,
DASHBOARD_MESSAGE,
LOGIN_MESSAGE,
PASS_RESET_MESSAGE,
ERROR_OVERRIDE,
}

View File

@ -0,0 +1,11 @@
package io.github.wulkanowy.data.enums
enum class TimetableGapsMode(val value: String) {
NO_GAPS("no_gaps"),
BETWEEN_LESSONS("between"),
BETWEEN_AND_BEFORE_LESSONS("before_and_between");
companion object {
fun getByValue(value: String) = entries.find { it.value == value } ?: BETWEEN_LESSONS
}
}

View File

@ -1,10 +1,11 @@
package io.github.wulkanowy.data.repositories package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.api.AdminMessageService import io.github.wulkanowy.data.api.AdminMessageService
import io.github.wulkanowy.data.db.dao.AdminMessageDao import io.github.wulkanowy.data.db.dao.AdminMessageDao
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.utils.AppInfo import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -13,34 +14,20 @@ import javax.inject.Singleton
class AdminMessageRepository @Inject constructor( class AdminMessageRepository @Inject constructor(
private val adminMessageService: AdminMessageService, private val adminMessageService: AdminMessageService,
private val adminMessageDao: AdminMessageDao, private val adminMessageDao: AdminMessageDao,
private val appInfo: AppInfo
) { ) {
private val saveFetchResultMutex = Mutex() private val saveFetchResultMutex = Mutex()
suspend fun getAdminMessages(student: Student) = networkBoundResource( fun getAdminMessages(): Flow<Resource<List<AdminMessage>>> =
mutex = saveFetchResultMutex, networkBoundResource(
isResultEmpty = { it == null }, mutex = saveFetchResultMutex,
query = { adminMessageDao.loadAll() }, isResultEmpty = { false },
fetch = { adminMessageService.getAdminMessages() }, query = { adminMessageDao.loadAll() },
shouldFetch = { true }, fetch = { adminMessageService.getAdminMessages() },
saveFetchResult = { oldItems, newItems -> shouldFetch = { true },
adminMessageDao.removeOldAndSaveNew(oldItems, newItems) saveFetchResult = { oldItems, newItems ->
}, adminMessageDao.removeOldAndSaveNew(oldItems, newItems)
showSavedOnLoading = false, },
mapResult = { adminMessages -> showSavedOnLoading = false,
adminMessages.filter { adminMessage -> )
val isCorrectRegister = adminMessage.targetRegisterHost?.let {
student.scrapperBaseUrl.contains(it, true)
} ?: true
val isCorrectFlavor =
adminMessage.targetFlavor?.equals(appInfo.buildFlavor, true) ?: true
val isCorrectMaxVersion =
adminMessage.versionMax?.let { it >= appInfo.versionCode } ?: true
val isCorrectMinVersion =
adminMessage.versionMin?.let { it <= appInfo.versionCode } ?: true
isCorrectRegister && isCorrectFlavor && isCorrectMaxVersion && isCorrectMinVersion
}.maxByOrNull { it.id }
}
)
} }

View File

@ -3,18 +3,26 @@ package io.github.wulkanowy.data.repositories
import android.content.Context import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.* import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.db.dao.MailboxDao import io.github.wulkanowy.data.db.dao.MailboxDao
import io.github.wulkanowy.data.db.dao.MessageAttachmentDao import io.github.wulkanowy.data.db.dao.MessageAttachmentDao
import io.github.wulkanowy.data.db.dao.MessagesDao import io.github.wulkanowy.data.db.dao.MessagesDao
import io.github.wulkanowy.data.db.entities.* import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageWithAttachment
import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageFolder import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED
import io.github.wulkanowy.data.enums.MessageFolder.TRASHED import io.github.wulkanowy.data.enums.MessageFolder.TRASHED
import io.github.wulkanowy.data.mappers.mapFromEntities import io.github.wulkanowy.data.mappers.mapFromEntities
import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.pojos.MessageDraft import io.github.wulkanowy.data.pojos.MessageDraft
import io.github.wulkanowy.data.waitForResult
import io.github.wulkanowy.domain.messages.GetMailboxByStudentUseCase import io.github.wulkanowy.domain.messages.GetMailboxByStudentUseCase
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.pojo.Folder import io.github.wulkanowy.sdk.pojo.Folder
@ -25,7 +33,6 @@ import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import timber.log.Timber import timber.log.Timber
@ -97,7 +104,7 @@ class MessageRepository @Inject constructor(
shouldFetch = { shouldFetch = {
checkNotNull(it) { "This message no longer exist!" } checkNotNull(it) { "This message no longer exist!" }
Timber.d("Message content in db empty: ${it.message.content.isBlank()}") Timber.d("Message content in db empty: ${it.message.content.isBlank()}")
it.message.unread || it.message.content.isBlank() (it.message.unread && markAsRead) || it.message.content.isBlank()
}, },
query = { query = {
messagesDb.loadMessageWithAttachment(message.messageGlobalKey) messagesDb.loadMessageWithAttachment(message.messageGlobalKey)
@ -113,7 +120,10 @@ class MessageRepository @Inject constructor(
messagesDb.updateAll( messagesDb.updateAll(
listOf(old.message.apply { listOf(old.message.apply {
id = message.id id = message.id
unread = !markAsRead unread = when {
markAsRead -> false
else -> unread
}
sender = new.sender sender = new.sender
recipients = new.recipients.singleOrNull() ?: "Wielu adresatów" recipients = new.recipients.singleOrNull() ?: "Wielu adresatów"
content = content.ifBlank { new.content } content = content.ifBlank { new.content }
@ -123,7 +133,7 @@ class MessageRepository @Inject constructor(
items = new.attachments.mapToEntities(message.messageGlobalKey), items = new.attachments.mapToEntities(message.messageGlobalKey),
) )
Timber.d("Message ${message.messageId} with blank content: ${old.message.content.isBlank()}, marked as read") Timber.d("Message ${message.messageId} with blank content: ${old.message.content.isBlank()}, marked as read: $markAsRead")
} }
) )

View File

@ -15,7 +15,6 @@ import io.github.wulkanowy.ui.modules.grade.GradeAverageMode
import io.github.wulkanowy.ui.modules.settings.appearance.menuorder.AppMenuItem import io.github.wulkanowy.ui.modules.settings.appearance.menuorder.AppMenuItem
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.time.Instant import java.time.Instant
@ -201,6 +200,14 @@ class PreferencesRepository @Inject constructor(
R.bool.pref_default_timetable_show_timers R.bool.pref_default_timetable_show_timers
) )
val showTimetableGaps: TimetableGapsMode
get() = TimetableGapsMode.getByValue(
getString(
R.string.pref_key_timetable_show_gaps,
R.string.pref_default_timetable_show_gaps
)
)
val showSubjectsWithoutGrades: Boolean val showSubjectsWithoutGrades: Boolean
get() = getBoolean( get() = getBoolean(
R.string.pref_key_subjects_without_grades, R.string.pref_key_subjects_without_grades,
@ -343,6 +350,12 @@ class PreferencesRepository @Inject constructor(
) )
} }
var isIncognitoMode: Boolean
get() = getBoolean(R.string.pref_key_incognito_moge, R.bool.pref_default_incognito_mode)
set(value) = sharedPref.edit {
putBoolean(context.getString(R.string.pref_key_incognito_moge), value)
}
var installationId: String var installationId: String
get() = sharedPref.getString(PREF_KEY_INSTALLATION_ID, null).orEmpty() get() = sharedPref.getString(PREF_KEY_INSTALLATION_ID, null).orEmpty()
private set(value) = sharedPref.edit { putString(PREF_KEY_INSTALLATION_ID, value) } private set(value) = sharedPref.edit { putString(PREF_KEY_INSTALLATION_ID, value) }

View File

@ -41,7 +41,7 @@ class SemesterRepository @Inject constructor(
val isRefreshOnModeChangeRequired = when { val isRefreshOnModeChangeRequired = when {
Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE -> { Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE -> {
semesters.firstOrNull { it.isCurrent }?.let { semesters.firstOrNull { it.isCurrent() }?.let {
0 == it.diaryId && 0 == it.kindergartenDiaryId 0 == it.diaryId && 0 == it.kindergartenDiaryId
} == true } == true
} }
@ -49,7 +49,7 @@ class SemesterRepository @Inject constructor(
} }
val isRefreshOnNoCurrentAppropriate = val isRefreshOnNoCurrentAppropriate =
refreshOnNoCurrent && !semesters.any { semester -> semester.isCurrent } refreshOnNoCurrent && !semesters.any { semester -> semester.isCurrent() }
return forceRefresh || isNoSemesters || isRefreshOnModeChangeRequired || isRefreshOnNoCurrentAppropriate return forceRefresh || isNoSemesters || isRefreshOnModeChangeRequired || isRefreshOnNoCurrentAppropriate
} }

View File

@ -0,0 +1,64 @@
package io.github.wulkanowy.domain.adminmessage
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageType
import io.github.wulkanowy.data.mapResourceData
import io.github.wulkanowy.data.repositories.AdminMessageRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.utils.AppInfo
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class GetAppropriateAdminMessageUseCase @Inject constructor(
private val adminMessageRepository: AdminMessageRepository,
private val preferencesRepository: PreferencesRepository,
private val appInfo: AppInfo
) {
operator fun invoke(student: Student, type: MessageType): Flow<Resource<AdminMessage?>> {
return invoke(student.scrapperBaseUrl, type)
}
operator fun invoke(scrapperBaseUrl: String, type: MessageType): Flow<Resource<AdminMessage?>> {
return adminMessageRepository.getAdminMessages().mapResourceData { adminMessages ->
adminMessages
.asSequence()
.filter { it.isNotDismissed() }
.filter { it.isVersionMatch() }
.filter { it.isRegisterHostMatch(scrapperBaseUrl) }
.filter { it.isFlavorMatch() }
.filter { it.isTypeMatch(type) }
.maxByOrNull { it.id }
}
}
private fun AdminMessage.isNotDismissed(): Boolean {
return id !in preferencesRepository.dismissedAdminMessageIds
}
private fun AdminMessage.isRegisterHostMatch(scrapperBaseUrl: String): Boolean {
return targetRegisterHost?.let {
scrapperBaseUrl.contains(it, true)
} ?: true
}
private fun AdminMessage.isFlavorMatch(): Boolean {
return targetFlavor?.equals(appInfo.buildFlavor, true) ?: true
}
private fun AdminMessage.isVersionMatch(): Boolean {
val isCorrectMaxVersion = versionMax?.let { it >= appInfo.versionCode } ?: true
val isCorrectMinVersion = versionMin?.let { it <= appInfo.versionCode } ?: true
return isCorrectMaxVersion && isCorrectMinVersion
}
private fun AdminMessage.isTypeMatch(messageType: MessageType): Boolean {
if (messageType in types) return true
if (MessageType.GENERAL_MESSAGE in types) return true
return false
}
}

View File

@ -4,6 +4,7 @@ import android.content.Intent
import android.widget.RemoteViewsService import android.widget.RemoteViewsService
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.TimetableRepository import io.github.wulkanowy.data.repositories.TimetableRepository
@ -26,10 +27,19 @@ class TimetableWidgetService : RemoteViewsService() {
@Inject @Inject
lateinit var sharedPref: SharedPrefProvider lateinit var sharedPref: SharedPrefProvider
@Inject
lateinit var prefRepository: PreferencesRepository
override fun onGetViewFactory(intent: Intent?): RemoteViewsFactory { override fun onGetViewFactory(intent: Intent?): RemoteViewsFactory {
Timber.d("TimetableWidgetFactory created") Timber.d("TimetableWidgetFactory created")
return TimetableWidgetFactory( return TimetableWidgetFactory(
timetableRepo, studentRepo, semesterRepo, sharedPref, applicationContext, intent timetableRepository = timetableRepo,
studentRepository = studentRepo,
semesterRepository = semesterRepo,
sharedPref = sharedPref,
prefRepository = prefRepository,
context = applicationContext,
intent = intent,
) )
} }
} }

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.modules.dashboard
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.ui.modules.dashboard.adapters.DashboardAdapter import io.github.wulkanowy.ui.modules.dashboard.adapters.DashboardAdapter
import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder
import java.util.* import java.util.*
class DashboardItemMoveCallback( class DashboardItemMoveCallback(
@ -55,5 +56,5 @@ class DashboardItemMoveCallback(
} }
private val RecyclerView.ViewHolder.isAdminMessageOrAccountItem: Boolean private val RecyclerView.ViewHolder.isAdminMessageOrAccountItem: Boolean
get() = this is DashboardAdapter.AdminMessageViewHolder || this is DashboardAdapter.AccountViewHolder get() = this is AdminMessageViewHolder || this is DashboardAdapter.AccountViewHolder
} }

View File

@ -5,7 +5,9 @@ import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageFolder import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.enums.MessageType
import io.github.wulkanowy.data.repositories.* import io.github.wulkanowy.data.repositories.*
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AdsHelper import io.github.wulkanowy.utils.AdsHelper
@ -32,7 +34,7 @@ class DashboardPresenter @Inject constructor(
private val conferenceRepository: ConferenceRepository, private val conferenceRepository: ConferenceRepository,
private val preferencesRepository: PreferencesRepository, private val preferencesRepository: PreferencesRepository,
private val schoolAnnouncementRepository: SchoolAnnouncementRepository, private val schoolAnnouncementRepository: SchoolAnnouncementRepository,
private val adminMessageRepository: AdminMessageRepository, private val getAppropriateAdminMessageUseCase: GetAppropriateAdminMessageUseCase,
private val adsHelper: AdsHelper private val adsHelper: AdsHelper
) : BasePresenter<DashboardView>(errorHandler, studentRepository) { ) : BasePresenter<DashboardView>(errorHandler, studentRepository) {
@ -159,19 +161,23 @@ class DashboardPresenter @Inject constructor(
DashboardItem.Type.ACCOUNT -> { DashboardItem.Type.ACCOUNT -> {
updateData(DashboardItem.Account(student), forceRefresh) updateData(DashboardItem.Account(student), forceRefresh)
} }
DashboardItem.Type.HORIZONTAL_GROUP -> { DashboardItem.Type.HORIZONTAL_GROUP -> {
loadHorizontalGroup(student, forceRefresh) loadHorizontalGroup(student, forceRefresh)
} }
DashboardItem.Type.LESSONS -> loadLessons(student, forceRefresh) DashboardItem.Type.LESSONS -> loadLessons(student, forceRefresh)
DashboardItem.Type.GRADES -> loadGrades(student, forceRefresh) DashboardItem.Type.GRADES -> loadGrades(student, forceRefresh)
DashboardItem.Type.HOMEWORK -> loadHomework(student, forceRefresh) DashboardItem.Type.HOMEWORK -> loadHomework(student, forceRefresh)
DashboardItem.Type.ANNOUNCEMENTS -> { DashboardItem.Type.ANNOUNCEMENTS -> {
loadSchoolAnnouncements(student, forceRefresh) loadSchoolAnnouncements(student, forceRefresh)
} }
DashboardItem.Type.EXAMS -> loadExams(student, forceRefresh) DashboardItem.Type.EXAMS -> loadExams(student, forceRefresh)
DashboardItem.Type.CONFERENCES -> { DashboardItem.Type.CONFERENCES -> {
loadConferences(student, forceRefresh) loadConferences(student, forceRefresh)
} }
DashboardItem.Type.ADS -> loadAds(forceRefresh) DashboardItem.Type.ADS -> loadAds(forceRefresh)
DashboardItem.Type.ADMIN_MESSAGE -> loadAdminMessage(student, forceRefresh) DashboardItem.Type.ADMIN_MESSAGE -> loadAdminMessage(student, forceRefresh)
} }
@ -355,6 +361,7 @@ class DashboardPresenter @Inject constructor(
firstLoadedItemList += DashboardItem.Type.GRADES firstLoadedItemList += DashboardItem.Type.GRADES
} }
} }
is Resource.Success -> { is Resource.Success -> {
Timber.i("Loading dashboard grades result: Success") Timber.i("Loading dashboard grades result: Success")
updateData( updateData(
@ -365,6 +372,7 @@ class DashboardPresenter @Inject constructor(
forceRefresh forceRefresh
) )
} }
is Resource.Error -> { is Resource.Error -> {
Timber.i("Loading dashboard grades result: An exception occurred") Timber.i("Loading dashboard grades result: An exception occurred")
errorHandler.dispatch(it.error) errorHandler.dispatch(it.error)
@ -402,12 +410,14 @@ class DashboardPresenter @Inject constructor(
firstLoadedItemList += DashboardItem.Type.LESSONS firstLoadedItemList += DashboardItem.Type.LESSONS
} }
} }
is Resource.Success -> { is Resource.Success -> {
Timber.i("Loading dashboard lessons result: Success") Timber.i("Loading dashboard lessons result: Success")
updateData( updateData(
DashboardItem.Lessons(it.data), forceRefresh DashboardItem.Lessons(it.data), forceRefresh
) )
} }
is Resource.Error -> { is Resource.Error -> {
Timber.i("Loading dashboard lessons result: An exception occurred") Timber.i("Loading dashboard lessons result: An exception occurred")
errorHandler.dispatch(it.error) errorHandler.dispatch(it.error)
@ -457,10 +467,12 @@ class DashboardPresenter @Inject constructor(
firstLoadedItemList += DashboardItem.Type.HOMEWORK firstLoadedItemList += DashboardItem.Type.HOMEWORK
} }
} }
is Resource.Success -> { is Resource.Success -> {
Timber.i("Loading dashboard homework result: Success") Timber.i("Loading dashboard homework result: Success")
updateData(DashboardItem.Homework(it.data), forceRefresh) updateData(DashboardItem.Homework(it.data), forceRefresh)
} }
is Resource.Error -> { is Resource.Error -> {
Timber.i("Loading dashboard homework result: An exception occurred") Timber.i("Loading dashboard homework result: An exception occurred")
errorHandler.dispatch(it.error) errorHandler.dispatch(it.error)
@ -489,10 +501,12 @@ class DashboardPresenter @Inject constructor(
firstLoadedItemList += DashboardItem.Type.ANNOUNCEMENTS firstLoadedItemList += DashboardItem.Type.ANNOUNCEMENTS
} }
} }
is Resource.Success -> { is Resource.Success -> {
Timber.i("Loading dashboard announcements result: Success") Timber.i("Loading dashboard announcements result: Success")
updateData(DashboardItem.Announcements(it.data), forceRefresh) updateData(DashboardItem.Announcements(it.data), forceRefresh)
} }
is Resource.Error -> { is Resource.Error -> {
Timber.i("Loading dashboard announcements result: An exception occurred") Timber.i("Loading dashboard announcements result: An exception occurred")
errorHandler.dispatch(it.error) errorHandler.dispatch(it.error)
@ -530,10 +544,12 @@ class DashboardPresenter @Inject constructor(
firstLoadedItemList += DashboardItem.Type.EXAMS firstLoadedItemList += DashboardItem.Type.EXAMS
} }
} }
is Resource.Success -> { is Resource.Success -> {
Timber.i("Loading dashboard exams result: Success") Timber.i("Loading dashboard exams result: Success")
updateData(DashboardItem.Exams(it.data), forceRefresh) updateData(DashboardItem.Exams(it.data), forceRefresh)
} }
is Resource.Error -> { is Resource.Error -> {
Timber.i("Loading dashboard exams result: An exception occurred") Timber.i("Loading dashboard exams result: An exception occurred")
errorHandler.dispatch(it.error) errorHandler.dispatch(it.error)
@ -569,10 +585,12 @@ class DashboardPresenter @Inject constructor(
firstLoadedItemList += DashboardItem.Type.CONFERENCES firstLoadedItemList += DashboardItem.Type.CONFERENCES
} }
} }
is Resource.Success -> { is Resource.Success -> {
Timber.i("Loading dashboard conferences result: Success") Timber.i("Loading dashboard conferences result: Success")
updateData(DashboardItem.Conferences(it.data), forceRefresh) updateData(DashboardItem.Conferences(it.data), forceRefresh)
} }
is Resource.Error -> { is Resource.Error -> {
Timber.i("Loading dashboard conferences result: An exception occurred") Timber.i("Loading dashboard conferences result: An exception occurred")
errorHandler.dispatch(it.error) errorHandler.dispatch(it.error)
@ -584,12 +602,12 @@ class DashboardPresenter @Inject constructor(
} }
private fun loadAdminMessage(student: Student, forceRefresh: Boolean) { private fun loadAdminMessage(student: Student, forceRefresh: Boolean) {
flatResourceFlow { adminMessageRepository.getAdminMessages(student) } flatResourceFlow {
.filter { getAppropriateAdminMessageUseCase(
val data = it.dataOrNull ?: return@filter true student = student,
val isDismissed = data.id in preferencesRepository.dismissedAdminMessageIds type = MessageType.DASHBOARD_MESSAGE,
!isDismissed )
} }
.onEach { .onEach {
when (it) { when (it) {
is Resource.Loading -> { is Resource.Loading -> {
@ -597,6 +615,7 @@ class DashboardPresenter @Inject constructor(
if (forceRefresh) return@onEach if (forceRefresh) return@onEach
updateData(DashboardItem.AdminMessages(), forceRefresh) updateData(DashboardItem.AdminMessages(), forceRefresh)
} }
is Resource.Success -> { is Resource.Success -> {
Timber.i("Loading dashboard admin message result: Success") Timber.i("Loading dashboard admin message result: Success")
updateData( updateData(
@ -604,6 +623,7 @@ class DashboardPresenter @Inject constructor(
forceRefresh = forceRefresh forceRefresh = forceRefresh
) )
} }
is Resource.Error -> { is Resource.Error -> {
Timber.i("Loading dashboard admin message result: An exception occurred") Timber.i("Loading dashboard admin message result: An exception occurred")
Timber.e(it.error) Timber.e(it.error)

View File

@ -1,8 +1,6 @@
package io.github.wulkanowy.ui.modules.dashboard.adapters package io.github.wulkanowy.ui.modules.dashboard.adapters
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.Typeface import android.graphics.Typeface
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
@ -24,6 +22,7 @@ import io.github.wulkanowy.data.db.entities.TimetableHeader
import io.github.wulkanowy.data.enums.GradeColorTheme import io.github.wulkanowy.data.enums.GradeColorTheme
import io.github.wulkanowy.databinding.* import io.github.wulkanowy.databinding.*
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder
import io.github.wulkanowy.utils.* import io.github.wulkanowy.utils.*
import timber.log.Timber import timber.log.Timber
import java.time.Duration import java.time.Duration
@ -109,7 +108,9 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
ItemDashboardConferencesBinding.inflate(inflater, parent, false) ItemDashboardConferencesBinding.inflate(inflater, parent, false)
) )
DashboardItem.Type.ADMIN_MESSAGE.ordinal -> AdminMessageViewHolder( DashboardItem.Type.ADMIN_MESSAGE.ordinal -> AdminMessageViewHolder(
ItemDashboardAdminMessageBinding.inflate(inflater, parent, false) ItemDashboardAdminMessageBinding.inflate(inflater, parent, false),
onAdminMessageDismissClickListener = onAdminMessageDismissClickListener,
onAdminMessageClickListener = onAdminMessageClickListener,
) )
DashboardItem.Type.ADS.ordinal -> AdsViewHolder( DashboardItem.Type.ADS.ordinal -> AdsViewHolder(
ItemDashboardAdsBinding.inflate(inflater, parent, false) ItemDashboardAdsBinding.inflate(inflater, parent, false)
@ -128,7 +129,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
is AnnouncementsViewHolder -> bindAnnouncementsViewHolder(holder, position) is AnnouncementsViewHolder -> bindAnnouncementsViewHolder(holder, position)
is ExamsViewHolder -> bindExamsViewHolder(holder, position) is ExamsViewHolder -> bindExamsViewHolder(holder, position)
is ConferencesViewHolder -> bindConferencesViewHolder(holder, position) is ConferencesViewHolder -> bindConferencesViewHolder(holder, position)
is AdminMessageViewHolder -> bindAdminMessage(holder, position) is AdminMessageViewHolder -> holder.bind((items[position] as DashboardItem.AdminMessages).adminMessage)
is AdsViewHolder -> bindAdsViewHolder(holder, position) is AdsViewHolder -> bindAdsViewHolder(holder, position)
} }
} }
@ -733,39 +734,6 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
} }
} }
private fun bindAdminMessage(adminMessageViewHolder: AdminMessageViewHolder, position: Int) {
val item = (items[position] as DashboardItem.AdminMessages).adminMessage ?: return
val context = adminMessageViewHolder.binding.root.context
val (backgroundColor, textColor) = when (item.priority) {
"HIGH" -> {
context.getThemeAttrColor(R.attr.colorMessageHigh) to
context.getThemeAttrColor(R.attr.colorOnMessageHigh)
}
"MEDIUM" -> {
context.getThemeAttrColor(R.attr.colorMessageMedium) to Color.BLACK
}
else -> null to context.getThemeAttrColor(R.attr.colorOnSurface)
}
with(adminMessageViewHolder.binding) {
dashboardAdminMessageItemTitle.text = item.title
dashboardAdminMessageItemTitle.setTextColor(textColor)
dashboardAdminMessageItemDescription.text = item.content
dashboardAdminMessageItemDescription.setTextColor(textColor)
dashboardAdminMessageItemIcon.setColorFilter(textColor)
dashboardAdminMessageItemDismiss.isVisible = item.isDismissible
dashboardAdminMessageItemDismiss.setTextColor(textColor)
dashboardAdminMessageItemDismiss.setOnClickListener {
onAdminMessageDismissClickListener(item)
}
root.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) })
item.destinationUrl?.let { url ->
root.setOnClickListener { onAdminMessageClickListener(url) }
}
}
}
private fun bindAdsViewHolder(adsViewHolder: AdsViewHolder, position: Int) { private fun bindAdsViewHolder(adsViewHolder: AdsViewHolder, position: Int) {
val item = (items[position] as DashboardItem.Ads).adBanner ?: return val item = (items[position] as DashboardItem.Ads).adBanner ?: return
val binding = adsViewHolder.binding val binding = adsViewHolder.binding
@ -819,9 +787,6 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
val adapter by lazy { DashboardConferencesAdapter() } val adapter by lazy { DashboardConferencesAdapter() }
} }
class AdminMessageViewHolder(val binding: ItemDashboardAdminMessageBinding) :
RecyclerView.ViewHolder(binding.root)
class AdsViewHolder(val binding: ItemDashboardAdsBinding) : class AdsViewHolder(val binding: ItemDashboardAdsBinding) :
RecyclerView.ViewHolder(binding.root) RecyclerView.ViewHolder(binding.root)

View File

@ -0,0 +1,52 @@
package io.github.wulkanowy.ui.modules.dashboard.viewholders
import android.content.res.ColorStateList
import android.graphics.Color
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.databinding.ItemDashboardAdminMessageBinding
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import io.github.wulkanowy.utils.getThemeAttrColor
class AdminMessageViewHolder(
private val binding: ItemDashboardAdminMessageBinding,
private val onAdminMessageDismissClickListener: (AdminMessage) -> Unit,
private val onAdminMessageClickListener: (String?) -> Unit,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: AdminMessage?) {
item ?: return
val context = binding.root.context
val (backgroundColor, textColor) = when (item.priority) {
"HIGH" -> {
context.getThemeAttrColor(R.attr.colorMessageHigh) to
context.getThemeAttrColor(R.attr.colorOnMessageHigh)
}
"MEDIUM" -> {
context.getThemeAttrColor(R.attr.colorMessageMedium) to Color.BLACK
}
else -> null to context.getThemeAttrColor(R.attr.colorOnSurface)
}
with(binding) {
dashboardAdminMessageItemTitle.text = item.title
dashboardAdminMessageItemTitle.setTextColor(textColor)
dashboardAdminMessageItemDescription.text = item.content
dashboardAdminMessageItemDescription.setTextColor(textColor)
dashboardAdminMessageItemIcon.setColorFilter(textColor)
dashboardAdminMessageItemDismiss.isVisible = item.isDismissible
dashboardAdminMessageItemDismiss.setTextColor(textColor)
dashboardAdminMessageItemDismiss.setOnClickListener {
onAdminMessageDismissClickListener(item)
}
root.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) })
item.destinationUrl?.let { url ->
root.setOnClickListener { onAdminMessageClickListener(url) }
}
}
}
}

View File

@ -9,10 +9,12 @@ import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.FragmentLoginFormBinding import io.github.wulkanowy.databinding.FragmentLoginFormBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder
import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.utils.* import io.github.wulkanowy.utils.*
@ -207,6 +209,19 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
binding.loginFormContainer.visibility = if (show) VISIBLE else GONE binding.loginFormContainer.visibility = if (show) VISIBLE else GONE
} }
override fun showAdminMessage(message: AdminMessage?) {
AdminMessageViewHolder(
binding = binding.loginFormMessage,
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
onAdminMessageClickListener = presenter::onAdminMessageSelected,
).bind(message)
binding.loginFormMessage.root.isVisible = message != null
}
override fun openInternetBrowser(url: String) {
requireContext().openInternetBrowser(url)
}
override fun showDomainSuffixInput(show: Boolean) { override fun showDomainSuffixInput(show: Boolean) {
binding.loginFormDomainSuffixLayout.isVisible = show binding.loginFormDomainSuffixLayout.isVisible = show
} }

View File

@ -1,13 +1,19 @@
package io.github.wulkanowy.ui.modules.login.form package io.github.wulkanowy.ui.modules.login.form
import androidx.core.net.toUri import androidx.core.net.toUri
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.enums.MessageType
import io.github.wulkanowy.data.flatResourceFlow
import io.github.wulkanowy.data.logResourceStatus import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.onResourceData
import io.github.wulkanowy.data.onResourceError import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceLoading import io.github.wulkanowy.data.onResourceLoading
import io.github.wulkanowy.data.onResourceNotLoading import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.onResourceSuccess import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
@ -22,7 +28,9 @@ class LoginFormPresenter @Inject constructor(
studentRepository: StudentRepository, studentRepository: StudentRepository,
private val loginErrorHandler: LoginErrorHandler, private val loginErrorHandler: LoginErrorHandler,
private val appInfo: AppInfo, private val appInfo: AppInfo,
private val analytics: AnalyticsHelper private val analytics: AnalyticsHelper,
private val getAppropriateAdminMessageUseCase: GetAppropriateAdminMessageUseCase,
private val preferencesRepository: PreferencesRepository,
) : BasePresenter<LoginFormView>(loginErrorHandler, studentRepository) { ) : BasePresenter<LoginFormView>(loginErrorHandler, studentRepository) {
private var lastError: Throwable? = null private var lastError: Throwable? = null
@ -41,6 +49,31 @@ class LoginFormPresenter @Inject constructor(
Timber.i("Entered wrong username or password") Timber.i("Entered wrong username or password")
} }
} }
reloadAdminMessage()
}
private fun reloadAdminMessage() {
flatResourceFlow {
getAppropriateAdminMessageUseCase(
scrapperBaseUrl = view?.formHostValue.orEmpty(),
type = MessageType.LOGIN_MESSAGE,
)
}
.logResourceStatus("load login admin message")
.onResourceData { view?.showAdminMessage(it) }
.onResourceError { view?.showAdminMessage(null) }
.launch()
}
fun onAdminMessageSelected(url: String?) {
url?.let { view?.openInternetBrowser(it) }
}
fun onAdminMessageDismissed(adminMessage: AdminMessage) {
preferencesRepository.dismissedAdminMessageIds += adminMessage.id
view?.showAdminMessage(null)
} }
fun onPrivacyLinkClick() { fun onPrivacyLinkClick() {
@ -63,6 +96,7 @@ class LoginFormPresenter @Inject constructor(
} }
updateCustomDomainSuffixVisibility() updateCustomDomainSuffixVisibility()
updateUsernameLabel() updateUsernameLabel()
reloadAdminMessage()
} }
} }
@ -103,7 +137,9 @@ class LoginFormPresenter @Inject constructor(
val email = view?.formUsernameValue.orEmpty().trim() val email = view?.formUsernameValue.orEmpty().trim()
val password = view?.formPassValue.orEmpty().trim() val password = view?.formPassValue.orEmpty().trim()
val host = view?.formHostValue.orEmpty().trim() val host = view?.formHostValue.orEmpty().trim()
val domainSuffix = view?.formDomainSuffix.orEmpty().trim() val domainSuffix = view?.formDomainSuffix.orEmpty().trim().takeIf {
"customSuffix" in host
}.orEmpty()
val symbol = view?.formHostSymbol.orEmpty().trim() val symbol = view?.formHostSymbol.orEmpty().trim()
if (!validateCredentials(email, password, host)) return if (!validateCredentials(email, password, host)) return

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.login.form package io.github.wulkanowy.ui.modules.login.form
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginData
@ -58,6 +59,10 @@ interface LoginFormView : BaseView {
fun showContent(show: Boolean) fun showContent(show: Boolean)
fun showAdminMessage(message: AdminMessage?)
fun openInternetBrowser(url: String)
fun showDomainSuffixInput(show: Boolean) fun showDomainSuffixInput(show: Boolean)
fun showOtherOptionsButton(show: Boolean) fun showOtherOptionsButton(show: Boolean)

View File

@ -11,7 +11,9 @@ import androidx.core.view.updateMargins
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.enums.MessageFolder.* import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED
import io.github.wulkanowy.data.enums.MessageFolder.SENT
import io.github.wulkanowy.data.enums.MessageFolder.TRASHED
import io.github.wulkanowy.databinding.FragmentMessageBinding import io.github.wulkanowy.databinding.FragmentMessageBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter
@ -49,6 +51,7 @@ class MessageFragment : BaseFragment<FragmentMessageBinding>(R.layout.fragment_m
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding = FragmentMessageBinding.bind(view) binding = FragmentMessageBinding.bind(view)
messageContainer = binding.messageViewPager
presenter.onAttachView(this) presenter.onAttachView(this)
} }
@ -95,6 +98,10 @@ class MessageFragment : BaseFragment<FragmentMessageBinding>(R.layout.fragment_m
binding.messageProgress.visibility = if (show) VISIBLE else INVISIBLE binding.messageProgress.visibility = if (show) VISIBLE else INVISIBLE
} }
override fun showMessage(messageId: Int) {
showMessage(getString(messageId))
}
override fun showNewMessage(show: Boolean) { override fun showNewMessage(show: Boolean) {
binding.openSendMessageButton.run { binding.openSendMessageButton.run {
if (show) show() else hide() if (show) show() else hide()

View File

@ -1,5 +1,7 @@
package io.github.wulkanowy.ui.modules.message package io.github.wulkanowy.ui.modules.message
import io.github.wulkanowy.R
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
@ -9,7 +11,8 @@ import javax.inject.Inject
class MessagePresenter @Inject constructor( class MessagePresenter @Inject constructor(
errorHandler: ErrorHandler, errorHandler: ErrorHandler,
studentRepository: StudentRepository studentRepository: StudentRepository,
private val preferencesRepository: PreferencesRepository,
) : BasePresenter<MessageView>(errorHandler, studentRepository) { ) : BasePresenter<MessageView>(errorHandler, studentRepository) {
override fun onAttachView(view: MessageView) { override fun onAttachView(view: MessageView) {
@ -19,6 +22,14 @@ class MessagePresenter @Inject constructor(
Timber.i("Message view was initialized") Timber.i("Message view was initialized")
loadData() loadData()
} }
showIncognitoModeReminderMessage()
}
private fun showIncognitoModeReminderMessage() {
if (preferencesRepository.isIncognitoMode) {
view?.showMessage(R.string.message_incognito_mode_on)
}
} }
fun onPageSelected(index: Int) { fun onPageSelected(index: Int) {

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.message package io.github.wulkanowy.ui.modules.message
import androidx.annotation.StringRes
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
interface MessageView : BaseView { interface MessageView : BaseView {
@ -12,6 +13,8 @@ interface MessageView : BaseView {
fun showProgress(show: Boolean) fun showProgress(show: Boolean)
fun showMessage(@StringRes messageId: Int)
fun showNewMessage(show: Boolean) fun showNewMessage(show: Boolean)
fun showTabLayout(show: Boolean) fun showTabLayout(show: Boolean)

View File

@ -12,6 +12,7 @@ import android.view.View.VISIBLE
import android.webkit.WebResourceRequest import android.webkit.WebResourceRequest
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import androidx.annotation.StringRes
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -164,6 +165,10 @@ class MessagePreviewFragment :
binding.messagePreviewErrorRetry.setOnClickListener { callback() } binding.messagePreviewErrorRetry.setOnClickListener { callback() }
} }
override fun showMessage(@StringRes messageId: Int) {
showMessage(getString(messageId))
}
override fun openMessageReply(message: Message?) { override fun openMessageReply(message: Message?) {
context?.let { it.startActivity(SendMessageActivity.getStartIntent(it, message, true)) } context?.let { it.startActivity(SendMessageActivity.getStartIntent(it, message, true)) }
} }

View File

@ -2,11 +2,13 @@ package io.github.wulkanowy.ui.modules.message.preview
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.core.text.parseAsHtml import androidx.core.text.parseAsHtml
import io.github.wulkanowy.R
import io.github.wulkanowy.data.* import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageAttachment import io.github.wulkanowy.data.db.entities.MessageAttachment
import io.github.wulkanowy.data.enums.MessageFolder import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.repositories.MessageRepository import io.github.wulkanowy.data.repositories.MessageRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
@ -20,6 +22,7 @@ class MessagePreviewPresenter @Inject constructor(
errorHandler: ErrorHandler, errorHandler: ErrorHandler,
studentRepository: StudentRepository, studentRepository: StudentRepository,
private val messageRepository: MessageRepository, private val messageRepository: MessageRepository,
private val preferencesRepository: PreferencesRepository,
private val analytics: AnalyticsHelper private val analytics: AnalyticsHelper
) : BasePresenter<MessagePreviewView>(errorHandler, studentRepository) { ) : BasePresenter<MessagePreviewView>(errorHandler, studentRepository) {
@ -54,7 +57,11 @@ class MessagePreviewPresenter @Inject constructor(
private fun loadData(messageToLoad: Message) { private fun loadData(messageToLoad: Message) {
flatResourceFlow { flatResourceFlow {
val student = studentRepository.getCurrentStudent() val student = studentRepository.getCurrentStudent()
messageRepository.getMessage(student, messageToLoad, true) messageRepository.getMessage(
student = student,
message = messageToLoad,
markAsRead = !preferencesRepository.isIncognitoMode,
)
} }
.logResourceStatus("message ${messageToLoad.messageId} preview") .logResourceStatus("message ${messageToLoad.messageId} preview")
.onResourceData { .onResourceData {
@ -65,6 +72,10 @@ class MessagePreviewPresenter @Inject constructor(
setMessageWithAttachment(it) setMessageWithAttachment(it)
showContent(true) showContent(true)
initOptions() initOptions()
if (preferencesRepository.isIncognitoMode && it.message.unread) {
showMessage(R.string.message_incognito_description)
}
} }
} else { } else {
view?.run { view?.run {

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.message.preview package io.github.wulkanowy.ui.modules.message.preview
import androidx.annotation.StringRes
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageWithAttachment import io.github.wulkanowy.data.db.entities.MessageWithAttachment
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
@ -43,4 +44,6 @@ interface MessagePreviewView : BaseView {
fun popView() fun popView()
fun printDocument(html: String, jobName: String) fun printDocument(html: String, jobName: String)
fun showMessage(@StringRes messageId: Int)
} }

View File

@ -7,6 +7,7 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.View.* import android.view.View.*
import android.widget.CompoundButton import android.widget.CompoundButton
import androidx.annotation.StringRes
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
@ -134,14 +135,20 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
} }
} }
@Deprecated("Deprecated in Java")
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater) super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.action_menu_message_tab, menu) inflater.inflate(R.menu.action_menu_message_tab, menu)
val searchView = menu.findItem(R.id.action_search).actionView as SearchView initializeSearchView(menu)
searchView.queryHint = getString(R.string.all_search_hint) }
searchView.maxWidth = Int.MAX_VALUE
private fun initializeSearchView(menu: Menu) {
val searchView = (menu.findItem(R.id.action_search).actionView as SearchView).apply {
queryHint = getString(R.string.all_search_hint)
maxWidth = Int.MAX_VALUE
}
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String) = false override fun onQueryTextSubmit(query: String) = false
override fun onQueryTextChange(query: String): Boolean { override fun onQueryTextChange(query: String): Boolean {
@ -207,8 +214,8 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
binding.messageTabSwipe.isRefreshing = show binding.messageTabSwipe.isRefreshing = show
} }
override fun showMessagesDeleted() { override fun showMessage(@StringRes messageId: Int) {
showMessage(getString(R.string.message_messages_deleted)) showMessage(getString(messageId))
} }
override fun notifyParentShowNewMessage(show: Boolean) { override fun notifyParentShowNewMessage(show: Boolean) {

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.message.tab package io.github.wulkanowy.ui.modules.message.tab
import io.github.wulkanowy.R
import io.github.wulkanowy.data.* import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Mailbox import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
@ -26,7 +27,7 @@ class MessageTabPresenter @Inject constructor(
errorHandler: ErrorHandler, errorHandler: ErrorHandler,
studentRepository: StudentRepository, studentRepository: StudentRepository,
private val messageRepository: MessageRepository, private val messageRepository: MessageRepository,
private val analytics: AnalyticsHelper private val analytics: AnalyticsHelper,
) : BasePresenter<MessageTabView>(errorHandler, studentRepository) { ) : BasePresenter<MessageTabView>(errorHandler, studentRepository) {
lateinit var folder: MessageFolder lateinit var folder: MessageFolder
@ -135,7 +136,7 @@ class MessageTabPresenter @Inject constructor(
messageRepository.deleteMessages(student, selectedMailbox, messageList) messageRepository.deleteMessages(student, selectedMailbox, messageList)
} }
.onFailure(errorHandler::dispatch) .onFailure(errorHandler::dispatch)
.onSuccess { view?.showMessagesDeleted() } .onSuccess { view?.showMessage(R.string.message_messages_deleted) }
} }
} }

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.message.tab package io.github.wulkanowy.ui.modules.message.tab
import androidx.annotation.StringRes
import io.github.wulkanowy.data.db.entities.Mailbox import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
@ -26,7 +27,7 @@ interface MessageTabView : BaseView {
fun showEmpty(show: Boolean) fun showEmpty(show: Boolean)
fun showMessagesDeleted() fun showMessage(@StringRes messageId: Int)
fun showErrorView(show: Boolean) fun showErrorView(show: Boolean)

View File

@ -12,7 +12,9 @@ import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.databinding.ItemTimetableBinding import io.github.wulkanowy.databinding.ItemTimetableBinding
import io.github.wulkanowy.databinding.ItemTimetableEmptyBinding
import io.github.wulkanowy.databinding.ItemTimetableSmallBinding import io.github.wulkanowy.databinding.ItemTimetableSmallBinding
import io.github.wulkanowy.utils.getPlural
import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
import javax.inject.Inject import javax.inject.Inject
@ -29,9 +31,14 @@ class TimetableAdapter @Inject constructor() :
TimetableItemType.SMALL -> SmallViewHolder( TimetableItemType.SMALL -> SmallViewHolder(
ItemTimetableSmallBinding.inflate(inflater, parent, false) ItemTimetableSmallBinding.inflate(inflater, parent, false)
) )
TimetableItemType.NORMAL -> NormalViewHolder( TimetableItemType.NORMAL -> NormalViewHolder(
ItemTimetableBinding.inflate(inflater, parent, false) ItemTimetableBinding.inflate(inflater, parent, false)
) )
TimetableItemType.EMPTY -> EmptyViewHolder(
ItemTimetableEmptyBinding.inflate(inflater, parent, false)
)
} }
} }
@ -40,12 +47,12 @@ class TimetableAdapter @Inject constructor() :
position: Int, position: Int,
payloads: MutableList<Any> payloads: MutableList<Any>
) { ) {
if (payloads.isEmpty()) return super.onBindViewHolder(holder, position, payloads) if (payloads.isNotEmpty() && holder is NormalViewHolder) {
updateTimeLeft(
if (holder is NormalViewHolder) updateTimeLeft( binding = holder.binding,
binding = holder.binding, timeLeft = (getItem(position) as TimetableItem.Normal).timeLeft,
timeLeft = (getItem(position) as TimetableItem.Normal).timeLeft, )
) } else super.onBindViewHolder(holder, position, payloads)
} }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
@ -54,10 +61,16 @@ class TimetableAdapter @Inject constructor() :
binding = holder.binding, binding = holder.binding,
item = getItem(position) as TimetableItem.Small, item = getItem(position) as TimetableItem.Small,
) )
is NormalViewHolder -> bindNormalView( is NormalViewHolder -> bindNormalView(
binding = holder.binding, binding = holder.binding,
item = getItem(position) as TimetableItem.Normal, item = getItem(position) as TimetableItem.Normal,
) )
is EmptyViewHolder -> bindEmptyView(
binding = holder.binding,
item = getItem(position) as TimetableItem.Empty,
)
} }
} }
@ -100,6 +113,19 @@ class TimetableAdapter @Inject constructor() :
} }
} }
private fun bindEmptyView(binding: ItemTimetableEmptyBinding, item: TimetableItem.Empty) {
with(binding) {
timetableEmptyItemNumber.text = when (item.numFrom) {
item.numTo -> item.numFrom.toString()
else -> "${item.numFrom}-${item.numTo}"
}
timetableEmptyItemSubject.text = timetableEmptyItemSubject.context.getPlural(
R.plurals.timetable_no_lesson,
item.numTo - item.numFrom + 1
)
}
}
private fun updateTimeLeft(binding: ItemTimetableBinding, timeLeft: TimeLeft?) { private fun updateTimeLeft(binding: ItemTimetableBinding, timeLeft: TimeLeft?) {
with(binding) { with(binding) {
when { when {
@ -137,6 +163,7 @@ class TimetableAdapter @Inject constructor() :
timetableItemTimeLeft.visibility = VISIBLE timetableItemTimeLeft.visibility = VISIBLE
timetableItemTimeLeft.text = root.context.getString(R.string.timetable_finished) timetableItemTimeLeft.text = root.context.getString(R.string.timetable_finished)
} }
else -> { else -> {
timetableItemTimeUntil.visibility = GONE timetableItemTimeUntil.visibility = GONE
timetableItemTimeLeft.visibility = GONE timetableItemTimeLeft.visibility = GONE
@ -191,7 +218,8 @@ class TimetableAdapter @Inject constructor() :
) )
} else { } else {
timetableItemDescription.visibility = GONE timetableItemDescription.visibility = GONE
timetableItemRoom.isVisible = lesson.room.isNotBlank() || lesson.roomOld.isNotBlank() timetableItemRoom.isVisible =
lesson.room.isNotBlank() || lesson.roomOld.isNotBlank()
timetableItemGroup.isVisible = item.showGroupsInPlan && lesson.group.isNotBlank() timetableItemGroup.isVisible = item.showGroupsInPlan && lesson.group.isNotBlank()
timetableItemTeacher.visibility = VISIBLE timetableItemTeacher.visibility = VISIBLE
} }
@ -274,6 +302,9 @@ class TimetableAdapter @Inject constructor() :
private class SmallViewHolder(val binding: ItemTimetableSmallBinding) : private class SmallViewHolder(val binding: ItemTimetableSmallBinding) :
RecyclerView.ViewHolder(binding.root) RecyclerView.ViewHolder(binding.root)
private class EmptyViewHolder(val binding: ItemTimetableEmptyBinding) :
RecyclerView.ViewHolder(binding.root)
companion object { companion object {
private val differ = object : DiffUtil.ItemCallback<TimetableItem>() { private val differ = object : DiffUtil.ItemCallback<TimetableItem>() {
override fun areItemsTheSame(oldItem: TimetableItem, newItem: TimetableItem): Boolean = override fun areItemsTheSame(oldItem: TimetableItem, newItem: TimetableItem): Boolean =
@ -281,9 +312,11 @@ class TimetableAdapter @Inject constructor() :
oldItem is TimetableItem.Small && newItem is TimetableItem.Small -> { oldItem is TimetableItem.Small && newItem is TimetableItem.Small -> {
oldItem.lesson.start == newItem.lesson.start oldItem.lesson.start == newItem.lesson.start
} }
oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal -> { oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal -> {
oldItem.lesson.start == newItem.lesson.start oldItem.lesson.start == newItem.lesson.start
} }
else -> oldItem == newItem else -> oldItem == newItem
} }

View File

@ -16,6 +16,11 @@ sealed class TimetableItem(val type: TimetableItemType) {
val timeLeft: TimeLeft?, val timeLeft: TimeLeft?,
val onClick: (Timetable) -> Unit, val onClick: (Timetable) -> Unit,
) : TimetableItem(TimetableItemType.NORMAL) ) : TimetableItem(TimetableItemType.NORMAL)
data class Empty(
val numFrom: Int,
val numTo: Int
) : TimetableItem(TimetableItemType.EMPTY)
} }
data class TimeLeft( data class TimeLeft(
@ -27,4 +32,5 @@ data class TimeLeft(
enum class TimetableItemType { enum class TimetableItemType {
SMALL, SMALL,
NORMAL, NORMAL,
EMPTY
} }

View File

@ -1,23 +1,44 @@
package io.github.wulkanowy.ui.modules.timetable package io.github.wulkanowy.ui.modules.timetable
import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.enums.TimetableGapsMode.BETWEEN_AND_BEFORE_LESSONS
import io.github.wulkanowy.data.enums.TimetableGapsMode.NO_GAPS
import io.github.wulkanowy.data.enums.TimetableMode import io.github.wulkanowy.data.enums.TimetableMode
import io.github.wulkanowy.data.flatResourceFlow
import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.onResourceData
import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceIntermediate
import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.TimetableRepository import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.* import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.capitalise
import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday
import io.github.wulkanowy.utils.isHolidays
import io.github.wulkanowy.utils.isJustFinished
import io.github.wulkanowy.utils.isShowTimeUntil
import io.github.wulkanowy.utils.left
import io.github.wulkanowy.utils.nextOrSameSchoolDay
import io.github.wulkanowy.utils.nextSchoolDay
import io.github.wulkanowy.utils.previousSchoolDay
import io.github.wulkanowy.utils.toFormattedString
import io.github.wulkanowy.utils.until
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import timber.log.Timber import timber.log.Timber
import java.time.Instant import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDate.* import java.time.LocalDate.now
import java.util.* import java.time.LocalDate.of
import java.time.LocalDate.ofEpochDay
import java.util.Timer
import javax.inject.Inject import javax.inject.Inject
import kotlin.concurrent.timer import kotlin.concurrent.timer
@ -192,16 +213,38 @@ class TimetablePresenter @Inject constructor(
compareBy({ item -> item.number }, { item -> !item.isStudentPlan }) compareBy({ item -> item.number }, { item -> !item.isStudentPlan })
) )
return filteredItems.mapIndexed { i, it -> var prevNum = when (prefRepository.showTimetableGaps) {
if (it.isStudentPlan) TimetableItem.Normal( BETWEEN_AND_BEFORE_LESSONS -> 0
lesson = it, else -> null
showGroupsInPlan = prefRepository.showGroupsInPlan, }
timeLeft = filteredItems.getTimeLeftForLesson(it, i), return buildList {
onClick = ::onTimetableItemSelected filteredItems.forEachIndexed { i, it ->
) else TimetableItem.Small( if (prefRepository.showTimetableGaps != NO_GAPS && prevNum != null && it.number > prevNum!! + 1) {
lesson = it, val emptyLesson = TimetableItem.Empty(
onClick = ::onTimetableItemSelected numFrom = prevNum!! + 1,
) numTo = it.number - 1
)
add(emptyLesson)
}
if (it.isStudentPlan) {
val normalLesson = TimetableItem.Normal(
lesson = it,
showGroupsInPlan = prefRepository.showGroupsInPlan,
timeLeft = filteredItems.getTimeLeftForLesson(it, i),
onClick = ::onTimetableItemSelected
)
add(normalLesson)
} else {
val smallLesson = TimetableItem.Small(
lesson = it,
onClick = ::onTimetableItemSelected
)
add(smallLesson)
}
prevNum = it.number
}
} }
} }

View File

@ -16,6 +16,9 @@ import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.enums.TimetableGapsMode.BETWEEN_AND_BEFORE_LESSONS
import io.github.wulkanowy.data.enums.TimetableGapsMode.NO_GAPS
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.TimetableRepository import io.github.wulkanowy.data.repositories.TimetableRepository
@ -24,6 +27,7 @@ import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Co
import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getStudentWidgetKey import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getStudentWidgetKey
import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getTodayLastLessonEndDateTimeWidgetKey import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getTodayLastLessonEndDateTimeWidgetKey
import io.github.wulkanowy.utils.getCompatColor import io.github.wulkanowy.utils.getCompatColor
import io.github.wulkanowy.utils.getPlural
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import timber.log.Timber import timber.log.Timber
@ -35,11 +39,12 @@ class TimetableWidgetFactory(
private val studentRepository: StudentRepository, private val studentRepository: StudentRepository,
private val semesterRepository: SemesterRepository, private val semesterRepository: SemesterRepository,
private val sharedPref: SharedPrefProvider, private val sharedPref: SharedPrefProvider,
private val prefRepository: PreferencesRepository,
private val context: Context, private val context: Context,
private val intent: Intent? private val intent: Intent?
) : RemoteViewsService.RemoteViewsFactory { ) : RemoteViewsService.RemoteViewsFactory {
private var lessons = emptyList<Timetable>() private var items = emptyList<TimetableWidgetItem>()
private var timetableCanceledColor: Int? = null private var timetableCanceledColor: Int? = null
@ -47,18 +52,13 @@ class TimetableWidgetFactory(
private var timetableChangeColor: Int? = null private var timetableChangeColor: Int? = null
private var lastSyncInstant: Instant? = null
override fun getLoadingView() = null override fun getLoadingView() = null
override fun hasStableIds() = true override fun hasStableIds() = true
override fun getCount() = when { override fun getCount() = items.size
lessons.isEmpty() -> 0
else -> lessons.size + 1
}
override fun getViewTypeCount() = 2 override fun getViewTypeCount() = 3
override fun getItemId(position: Int) = position.toLong() override fun getItemId(position: Int) = position.toLong()
@ -75,9 +75,10 @@ class TimetableWidgetFactory(
runBlocking { runBlocking {
val student = getStudent(studentId) ?: return@runBlocking val student = getStudent(studentId) ?: return@runBlocking
val semester = semesterRepository.getCurrentSemester(student) val semester = semesterRepository.getCurrentSemester(student)
lessons = getLessons(student, semester, date) items = createItems(
lastSyncInstant = lessons = getLessons(student, semester, date),
timetableRepository.getLastRefreshTimestamp(semester, date, date) lastSync = timetableRepository.getLastRefreshTimestamp(semester, date, date)
)
if (date == LocalDate.now()) { if (date == LocalDate.now()) {
updateTodayLastLessonEnd(appWidgetId) updateTodayLastLessonEnd(appWidgetId)
} }
@ -101,8 +102,33 @@ class TimetableWidgetFactory(
return lessons.sortedBy { it.number } return lessons.sortedBy { it.number }
} }
private fun createItems(
lessons: List<Timetable>,
lastSync: Instant?,
): List<TimetableWidgetItem> {
var prevNum = when (prefRepository.showTimetableGaps) {
BETWEEN_AND_BEFORE_LESSONS -> 0
else -> null
}
return buildList {
lessons.forEach {
if (prefRepository.showTimetableGaps != NO_GAPS && prevNum != null && it.number > prevNum!! + 1) {
val emptyItem = TimetableWidgetItem.Empty(
numFrom = prevNum!! + 1,
numTo = it.number - 1
)
add(emptyItem)
}
add(TimetableWidgetItem.Normal(it))
prevNum = it.number
}
add(TimetableWidgetItem.Synchronized(lastSync ?: Instant.MIN))
}
}
private fun updateTodayLastLessonEnd(appWidgetId: Int) { private fun updateTodayLastLessonEnd(appWidgetId: Int) {
val todayLastLessonEnd = lessons.maxOfOrNull { it.end } ?: return val todayLastLessonEnd = items.filterIsInstance<TimetableWidgetItem.Normal>()
.maxOfOrNull { it.lesson.end } ?: return
val key = getTodayLastLessonEndDateTimeWidgetKey(appWidgetId) val key = getTodayLastLessonEndDateTimeWidgetKey(appWidgetId)
sharedPref.putLong(key, todayLastLessonEnd.epochSecond, true) sharedPref.putLong(key, todayLastLessonEnd.epochSecond, true)
} }
@ -112,44 +138,81 @@ class TimetableWidgetFactory(
} }
override fun getViewAt(position: Int): RemoteViews? { override fun getViewAt(position: Int): RemoteViews? {
if (position == lessons.size) { return when (val item = items.getOrNull(position) ?: return null) {
val synchronizationInstant = lastSyncInstant ?: Instant.MIN is TimetableWidgetItem.Normal -> getNormalItemRemoteView(item)
val synchronizationText = getSynchronizationInfoText(synchronizationInstant) is TimetableWidgetItem.Empty -> getEmptyItemRemoteView(item)
return RemoteViews(context.packageName, R.layout.item_widget_timetable_footer).apply { is TimetableWidgetItem.Synchronized -> getSynchronizedItemRemoteView(item)
setTextViewText(R.id.timetableWidgetSynchronizationTime, synchronizationText)
}
} }
}
val lesson = lessons.getOrNull(position) ?: return null private fun getNormalItemRemoteView(item: TimetableWidgetItem.Normal): RemoteViews {
val lesson = item.lesson
val lessonStartTime = lesson.start.toFormattedString(TIME_FORMAT_STYLE) val lessonStartTime = lesson.start.toFormattedString(TIME_FORMAT_STYLE)
val lessonEndTime = lesson.end.toFormattedString(TIME_FORMAT_STYLE) val lessonEndTime = lesson.end.toFormattedString(TIME_FORMAT_STYLE)
val roomText = "${context.getString(R.string.timetable_room)} ${lesson.room}"
val remoteViews = RemoteViews(context.packageName, R.layout.item_widget_timetable).apply { val remoteViews = RemoteViews(context.packageName, R.layout.item_widget_timetable).apply {
setTextViewText(R.id.timetableWidgetItemNumber, lesson.number.toString()) setTextViewText(R.id.timetableWidgetItemNumber, lesson.number.toString())
setTextViewText(R.id.timetableWidgetItemTimeStart, lessonStartTime) setTextViewText(R.id.timetableWidgetItemTimeStart, lessonStartTime)
setTextViewText(R.id.timetableWidgetItemTimeFinish, lessonEndTime) setTextViewText(R.id.timetableWidgetItemTimeFinish, lessonEndTime)
setTextViewText(R.id.timetableWidgetItemSubject, lesson.subject) setTextViewText(R.id.timetableWidgetItemSubject, lesson.subject)
setTextViewText(R.id.timetableWidgetItemRoom, roomText)
setTextViewText(R.id.timetableWidgetItemTeacher, lesson.teacher) setTextViewText(R.id.timetableWidgetItemTeacher, lesson.teacher)
setTextViewText(R.id.timetableWidgetItemDescription, lesson.info) setTextViewText(R.id.timetableWidgetItemDescription, lesson.info)
setOnClickFillInIntent(R.id.timetableWidgetItemContainer, Intent()) setOnClickFillInIntent(R.id.timetableWidgetItemContainer, Intent())
} }
updateTheme() updateTheme()
clearLessonStyles(remoteViews) clearLessonStyles(remoteViews)
if (lesson.room.isBlank()) {
remoteViews.setViewVisibility(R.id.timetableWidgetItemRoom, GONE)
} else {
remoteViews.setTextViewText(R.id.timetableWidgetItemRoom, lesson.room)
}
when { when {
lesson.canceled -> applyCancelledLessonStyles(remoteViews) lesson.canceled -> applyCancelledLessonStyles(remoteViews)
lesson.changes or lesson.info.isNotBlank() -> applyChangedLessonStyles( lesson.changes or lesson.info.isNotBlank() -> applyChangedLessonStyles(
remoteViews, lesson remoteViews = remoteViews,
lesson = lesson,
) )
} }
return remoteViews return remoteViews
} }
private fun getEmptyItemRemoteView(item: TimetableWidgetItem.Empty): RemoteViews {
return RemoteViews(
context.packageName,
R.layout.item_widget_timetable_empty
).apply {
setTextViewText(
R.id.timetableWidgetEmptyItemNumber,
when (item.numFrom) {
item.numTo -> item.numFrom.toString()
else -> "${item.numFrom}-${item.numTo}"
}
)
setTextViewText(
R.id.timetableWidgetEmptyItemText,
context.getPlural(
R.plurals.timetable_no_lesson,
item.numTo - item.numFrom + 1
)
)
setOnClickFillInIntent(R.id.timetableWidgetEmptyItemContainer, Intent())
}
}
private fun getSynchronizedItemRemoteView(item: TimetableWidgetItem.Synchronized): RemoteViews {
return RemoteViews(
context.packageName,
R.layout.item_widget_timetable_footer
).apply {
setTextViewText(
R.id.timetableWidgetSynchronizationTime,
getSynchronizationInfoText(item.timestamp)
)
}
}
private fun updateTheme() { private fun updateTheme() {
when (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { when (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
Configuration.UI_MODE_NIGHT_YES -> { Configuration.UI_MODE_NIGHT_YES -> {

View File

@ -0,0 +1,26 @@
package io.github.wulkanowy.ui.modules.timetablewidget
import io.github.wulkanowy.data.db.entities.Timetable
import java.time.Instant
sealed class TimetableWidgetItem(val type: TimetableWidgetItemType) {
data class Normal(
val lesson: Timetable,
) : TimetableWidgetItem(TimetableWidgetItemType.NORMAL)
data class Empty(
val numFrom: Int,
val numTo: Int
) : TimetableWidgetItem(TimetableWidgetItemType.EMPTY)
data class Synchronized(
val timestamp: Instant,
) : TimetableWidgetItem(TimetableWidgetItemType.SYNCHRONIZED)
}
enum class TimetableWidgetItemType {
NORMAL,
EMPTY,
SYNCHRONIZED,
}

View File

@ -8,6 +8,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.graphics.Bitmap
import android.widget.RemoteViews import android.widget.RemoteViews
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.graphics.drawable.DrawableCompat import androidx.core.graphics.drawable.DrawableCompat
@ -76,110 +77,151 @@ class TimetableWidgetProvider : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
GlobalScope.launch { GlobalScope.launch {
when (intent.action) { when (intent.action) {
ACTION_APPWIDGET_UPDATE -> onUpdate(context, intent) ACTION_APPWIDGET_UPDATE -> onWidgetUpdate(context, intent)
ACTION_APPWIDGET_DELETED -> onDelete(intent) ACTION_APPWIDGET_DELETED -> onWidgetDeleted(intent)
} }
} }
} }
private suspend fun onUpdate(context: Context, intent: Intent) { private suspend fun onWidgetUpdate(context: Context, intent: Intent) {
if (intent.getStringExtra(EXTRA_BUTTON_TYPE) == null) { val pressedButton = intent.getPressedButton()
val isFromConfigure = intent.getBooleanExtra(EXTRA_FROM_CONFIGURE, false)
val appWidgetIds = intent.getIntArrayExtra(EXTRA_APPWIDGET_IDS) ?: return
appWidgetIds.forEach { appWidgetId -> if (pressedButton == null) {
val student = val updatedWidgetIds = intent.getWidgetIds() ?: return
getStudent(sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0), appWidgetId) updatedWidgetIds.forEach { updateWidgetLayout(context, it) }
val savedDataEpochDay = sharedPref.getLong(getDateWidgetKey(appWidgetId), 0)
val dateToLoad = if (isFromConfigure && savedDataEpochDay != 0L) {
LocalDate.ofEpochDay(savedDataEpochDay)
} else {
getWidgetDefaultDateToLoad(appWidgetId)
}
updateWidget(context, appWidgetId, dateToLoad, student)
}
} else { } else {
val buttonType = intent.getStringExtra(EXTRA_BUTTON_TYPE) val widgetId = intent.getToggledWidgetId() ?: return
val toggledWidgetId = intent.getIntExtra(EXTRA_TOGGLED_WIDGET_ID, 0) reportChangedDay(pressedButton)
val student = getStudent( updateSavedWidgetDate(widgetId, pressedButton)
sharedPref.getLong(getStudentWidgetKey(toggledWidgetId), 0), toggledWidgetId updateWidgetLayout(context, widgetId)
)
val savedDate =
LocalDate.ofEpochDay(sharedPref.getLong(getDateWidgetKey(toggledWidgetId), 0))
val date = when (buttonType) {
BUTTON_RESET -> getWidgetDefaultDateToLoad(toggledWidgetId)
BUTTON_NEXT -> savedDate.nextSchoolDay
BUTTON_PREV -> savedDate.previousSchoolDay
else -> getWidgetDefaultDateToLoad(toggledWidgetId)
}
if (!buttonType.isNullOrBlank()) {
analytics.logEvent(
"changed_timetable_widget_day", "button" to buttonType
)
}
updateWidget(context, toggledWidgetId, date, student)
} }
} }
private fun onDelete(intent: Intent) { private fun Intent.getPressedButton(): String? {
val appWidgetId = intent.getIntExtra(EXTRA_APPWIDGET_ID, 0) return getStringExtra(EXTRA_BUTTON_TYPE)
}
if (appWidgetId != 0) { private fun Intent.getWidgetIds(): IntArray? {
with(sharedPref) { return getIntArrayExtra(EXTRA_APPWIDGET_IDS)
delete(getStudentWidgetKey(appWidgetId)) }
delete(getDateWidgetKey(appWidgetId))
} private fun Intent.getToggledWidgetId(): Int? {
val toggledWidgetId = getIntExtra(EXTRA_TOGGLED_WIDGET_ID, INVALID_APPWIDGET_ID)
return toggledWidgetId.takeIf { it != INVALID_APPWIDGET_ID }
}
private fun reportChangedDay(buttonType: String) {
if (buttonType.isNotBlank()) {
analytics.logEvent("changed_timetable_widget_day", "button" to buttonType)
} }
} }
private fun updateWidget( private fun updateSavedWidgetDate(widgetId: Int, buttonType: String) {
context: Context, appWidgetId: Int, date: LocalDate, student: Student? val savedDate = getSavedWidgetDate(widgetId)
val newDate = savedDate?.let { getNewDate(it, widgetId, buttonType) }
?: getWidgetDefaultDateToLoad(widgetId)
setWidgetDate(widgetId, newDate)
}
private fun getSavedWidgetDate(widgetId: Int): LocalDate? {
val epochDay = sharedPref.getLong(getDateWidgetKey(widgetId), 0)
return if (epochDay == 0L) null else LocalDate.ofEpochDay(epochDay)
}
private fun getNewDate(
currentDate: LocalDate,
widgetId: Int,
selectedButton: String
): LocalDate {
return when (selectedButton) {
BUTTON_NEXT -> currentDate.nextSchoolDay
BUTTON_PREV -> currentDate.previousSchoolDay
else -> getWidgetDefaultDateToLoad(widgetId)
}
}
private fun setWidgetDate(widgetId: Int, dateToSet: LocalDate) {
val widgetDateKey = getDateWidgetKey(widgetId)
sharedPref.putLong(widgetDateKey, dateToSet.toEpochDay(), true)
}
private fun getWidgetDefaultDateToLoad(widgetId: Int): LocalDate {
val lastLessonEndDateTime = getLastLessonDateTime(widgetId)
val todayDate = LocalDate.now()
val isLastLessonToday = lastLessonEndDateTime.toLocalDate() == todayDate
val isEndOfLessons = LocalDateTime.now() > lastLessonEndDateTime
return if (isLastLessonToday && isEndOfLessons) {
todayDate.nextSchoolDay
} else {
todayDate.nextOrSameSchoolDay
}
}
private fun getLastLessonDateTime(widgetId: Int): LocalDateTime {
val lastLessonTimestamp = sharedPref
.getLong(getTodayLastLessonEndDateTimeWidgetKey(widgetId), 0)
return LocalDateTime.ofEpochSecond(lastLessonTimestamp, 0, ZoneOffset.UTC)
}
private suspend fun updateWidgetLayout(
context: Context, widgetId: Int
) { ) {
val nextNavIntent = createNavIntent(context, appWidgetId, appWidgetId, BUTTON_NEXT) val widgetRemoteViews = RemoteViews(context.packageName, R.layout.widget_timetable)
val prevNavIntent = createNavIntent(context, -appWidgetId, appWidgetId, BUTTON_PREV)
val resetNavIntent =
createNavIntent(context, Int.MAX_VALUE - appWidgetId, appWidgetId, BUTTON_RESET)
val adapterIntent = Intent(context, TimetableWidgetService::class.java).apply {
putExtra(EXTRA_APPWIDGET_ID, appWidgetId)
action = appWidgetId.toString() //make Intent unique
}
val appIntent = PendingIntent.getActivity(
context,
TIMETABLE_PENDING_INTENT_ID,
SplashActivity.getStartIntent(context, Destination.Timetable()),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
)
// Apply the click action intent
val appIntent = createPendingAppIntent(context)
widgetRemoteViews.setPendingIntentTemplate(R.id.timetableWidgetList, appIntent)
// Display saved date
val date = getSavedWidgetDate(widgetId) ?: getWidgetDefaultDateToLoad(widgetId)
val formattedDate = date.toFormattedString("EEE, dd.MM").capitalise() val formattedDate = date.toFormattedString("EEE, dd.MM").capitalise()
val remoteView = RemoteViews(context.packageName, R.layout.widget_timetable).apply { widgetRemoteViews.setTextViewText(R.id.timetableWidgetDate, formattedDate)
setEmptyView(R.id.timetableWidgetList, R.id.timetableWidgetEmpty)
setTextViewText(R.id.timetableWidgetDate, formattedDate) // Apply intents to the date switcher buttons
setRemoteAdapter(R.id.timetableWidgetList, adapterIntent) val nextNavIntent = createNavButtonIntent(context, widgetId, widgetId, BUTTON_NEXT)
val prevNavIntent = createNavButtonIntent(context, -widgetId, widgetId, BUTTON_PREV)
val resetNavIntent =
createNavButtonIntent(context, Int.MAX_VALUE - widgetId, widgetId, BUTTON_RESET)
widgetRemoteViews.run {
setOnClickPendingIntent(R.id.timetableWidgetNext, nextNavIntent) setOnClickPendingIntent(R.id.timetableWidgetNext, nextNavIntent)
setOnClickPendingIntent(R.id.timetableWidgetPrev, prevNavIntent) setOnClickPendingIntent(R.id.timetableWidgetPrev, prevNavIntent)
setOnClickPendingIntent(R.id.timetableWidgetDate, resetNavIntent) setOnClickPendingIntent(R.id.timetableWidgetDate, resetNavIntent)
setPendingIntentTemplate(R.id.timetableWidgetList, appIntent)
} }
student?.let { // Setup the lesson list adapter
setupAccountView(context, student, remoteView, appWidgetId) val lessonListAdapterIntent = createLessonListAdapterIntent(context, widgetId)
// --- Ensure the selected date is stored in the shared preferences,
// --- on which the TimetableWidgetFactory relies
setWidgetDate(widgetId, date)
// ---
widgetRemoteViews.apply {
setEmptyView(R.id.timetableWidgetList, R.id.timetableWidgetEmpty)
setRemoteAdapter(R.id.timetableWidgetList, lessonListAdapterIntent)
} }
with(sharedPref) { // Setup profile picture
putLong(getDateWidgetKey(appWidgetId), date.toEpochDay(), true) getWidgetStudent(widgetId)?.let { student ->
setupAccountView(context, student, widgetRemoteViews, widgetId)
} }
// Apply updates
with(appWidgetManager) { with(appWidgetManager) {
partiallyUpdateAppWidget(appWidgetId, remoteView) partiallyUpdateAppWidget(widgetId, widgetRemoteViews)
notifyAppWidgetViewDataChanged(appWidgetId, R.id.timetableWidgetList) notifyAppWidgetViewDataChanged(widgetId, R.id.timetableWidgetList)
} }
Timber.d("TimetableWidgetProvider updated") Timber.d("TimetableWidgetProvider updated")
} }
private fun createNavIntent( private fun createPendingAppIntent(context: Context) = PendingIntent.getActivity(
context, TIMETABLE_PENDING_INTENT_ID,
SplashActivity.getStartIntent(context, Destination.Timetable()),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
)
private fun createNavButtonIntent(
context: Context, code: Int, appWidgetId: Int, buttonType: String context: Context, code: Int, appWidgetId: Int, buttonType: String
) = PendingIntent.getBroadcast( ) = PendingIntent.getBroadcast(
context, code, Intent(context, TimetableWidgetProvider::class.java).apply { context, code, Intent(context, TimetableWidgetProvider::class.java).apply {
@ -189,6 +231,17 @@ class TimetableWidgetProvider : BroadcastReceiver() {
}, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
) )
private fun createLessonListAdapterIntent(context: Context, widgetId: Int) =
Intent(context, TimetableWidgetService::class.java).apply {
putExtra(EXTRA_APPWIDGET_ID, widgetId)
action = widgetId.toString() //make Intent unique
}
private suspend fun getWidgetStudent(widgetId: Int): Student? {
val studentId = sharedPref.getLong(getStudentWidgetKey(widgetId), 0)
return getStudent(studentId, widgetId)
}
private suspend fun getStudent(studentId: Long, appWidgetId: Int) = try { private suspend fun getStudent(studentId: Long, appWidgetId: Int) = try {
val students = studentRepository.getSavedStudents(false) val students = studentRepository.getSavedStudents(false)
val student = students.singleOrNull { it.student.id == studentId }?.student val student = students.singleOrNull { it.student.id == studentId }?.student
@ -199,6 +252,7 @@ class TimetableWidgetProvider : BroadcastReceiver() {
sharedPref.putLong(getStudentWidgetKey(appWidgetId), it.id) sharedPref.putLong(getStudentWidgetKey(appWidgetId), it.id)
} }
} }
else -> null else -> null
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -208,60 +262,64 @@ class TimetableWidgetProvider : BroadcastReceiver() {
null null
} }
private fun getWidgetDefaultDateToLoad(appWidgetId: Int): LocalDate { private fun setupAccountView(
val lastLessonEndTimestamp = context: Context, student: Student, remoteViews: RemoteViews, widgetId: Int
sharedPref.getLong(getTodayLastLessonEndDateTimeWidgetKey(appWidgetId), 0) ) {
val lastLessonEndDateTime = val accountInitials = getAccountInitials(student.nickOrName)
LocalDateTime.ofEpochSecond(lastLessonEndTimestamp, 0, ZoneOffset.UTC) val accountPickerPendingIntent = createAccountPickerPendingIntent(context, widgetId)
val todayDate = LocalDate.now() getAvatarBackgroundBitmap(context, student.avatarColor)?.let {
val isLastLessonEndDateNow = lastLessonEndDateTime.toLocalDate() == todayDate remoteViews.setImageViewBitmap(R.id.timetableWidgetAccountBackground, it)
val isLastLessonEndDateAfterNowTime = LocalDateTime.now() > lastLessonEndDateTime }
return if (isLastLessonEndDateNow && isLastLessonEndDateAfterNowTime) { remoteViews.apply {
todayDate.nextSchoolDay setTextViewText(R.id.timetableWidgetAccountInitials, accountInitials)
} else { setOnClickPendingIntent(R.id.timetableWidgetAccount, accountPickerPendingIntent)
todayDate.nextOrSameSchoolDay
} }
} }
private fun setupAccountView( private fun getAccountInitials(name: String): String {
context: Context, val firstLetters = name.split(" ").mapNotNull { it.firstOrNull() }
student: Student, return firstLetters.joinToString(separator = "").uppercase()
remoteViews: RemoteViews, }
appWidgetId: Int
) {
val accountInitials = student.nickOrName
.split(" ")
.mapNotNull { it.firstOrNull() }.take(2)
.joinToString(separator = "").uppercase()
val accountPickerIntent = PendingIntent.getActivity( private fun createAccountPickerPendingIntent(context: Context, widgetId: Int) =
PendingIntent.getActivity(
context, context,
-Int.MAX_VALUE + appWidgetId, -Int.MAX_VALUE + widgetId,
Intent(context, TimetableWidgetConfigureActivity::class.java).apply { Intent(context, TimetableWidgetConfigureActivity::class.java).apply {
addFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK) addFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK)
putExtra(EXTRA_APPWIDGET_ID, appWidgetId) putExtra(EXTRA_APPWIDGET_ID, widgetId)
putExtra(EXTRA_FROM_PROVIDER, true) putExtra(EXTRA_FROM_PROVIDER, true)
}, },
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
) )
// Create background bitmap private fun getAvatarBackgroundBitmap(context: Context, avatarColor: Long): Bitmap? {
val avatarDrawableResource = R.drawable.background_timetable_widget_avatar val avatarDrawableResource = R.drawable.background_timetable_widget_avatar
AppCompatResources.getDrawable(context, avatarDrawableResource)?.let { drawable -> return AppCompatResources.getDrawable(context, avatarDrawableResource)?.let { drawable ->
val screenDensity = context.resources.displayMetrics.density val screenDensity = context.resources.displayMetrics.density
val avatarSize = (48 * screenDensity).toInt() val avatarSize = (48 * screenDensity).toInt()
val backgroundBitmap = DrawableCompat.wrap(drawable).run { DrawableCompat.wrap(drawable).run {
DrawableCompat.setTint(this, student.avatarColor.toInt()) DrawableCompat.setTint(this, avatarColor.toInt())
toBitmap(avatarSize, avatarSize) toBitmap(avatarSize, avatarSize)
} }
remoteViews.setImageViewBitmap(R.id.timetableWidgetAccountBackground, backgroundBitmap)
} }
}
remoteViews.apply { private fun onWidgetDeleted(intent: Intent) {
setTextViewText(R.id.timetableWidgetAccountInitials, accountInitials) val deletedWidgetId = intent.getWidgetId()
setOnClickPendingIntent(R.id.timetableWidgetAccount, accountPickerIntent) deleteWidgetPreferences(deletedWidgetId)
}
private fun Intent.getWidgetId(): Int {
return getIntExtra(EXTRA_APPWIDGET_ID, INVALID_APPWIDGET_ID)
}
private fun deleteWidgetPreferences(widgetId: Int) {
with(sharedPref) {
delete(getStudentWidgetKey(widgetId))
delete(getDateWidgetKey(widgetId))
} }
} }
} }

View File

@ -1,16 +1,27 @@
package io.github.wulkanowy.utils package io.github.wulkanowy.utils
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import java.time.LocalDate
import java.time.LocalDate.now import java.time.LocalDate.now
import java.time.Month
inline val Semester.isCurrent: Boolean fun Semester.isCurrent(now: LocalDate = now()): Boolean {
get() = now() in start..end val shiftedStart = if (start.month == Month.SEPTEMBER) {
start.minusDays(3)
} else start
val shiftedEnd = if (end.month == Month.AUGUST || end.month == Month.SEPTEMBER) {
end.minusDays(3)
} else end
return now in shiftedStart..shiftedEnd
}
fun List<Semester>.getCurrentOrLast(): Semester { fun List<Semester>.getCurrentOrLast(): Semester {
if (isEmpty()) throw RuntimeException("Empty semester list") if (isEmpty()) throw RuntimeException("Empty semester list")
// when there is only one current semester // when there is only one current semester
singleOrNull { it.isCurrent }?.let { return it } singleOrNull { it.isCurrent() }?.let { return it }
// when there is more than one current semester - find one with higher id // when there is more than one current semester - find one with higher id
singleOrNull { semester -> semester.semesterId == maxByOrNull { it.semesterId }?.semesterId }?.let { return it } singleOrNull { semester -> semester.semesterId == maxByOrNull { it.semesterId }?.semesterId }?.let { return it }

View File

@ -1,7 +1,8 @@
Wersja 2.0.8 Wersja 2.1.0
— poprawiliśmy wyświetlanie kilku rodzajów zmian w planie lekcji — dodaliśmy tryb incognito w wiadomościach
— dodaliśmy limit znaków w okienku usprawiedliwiania — dodaliśmy wyświetlanie pustych lekcji (okienek) w planie lekcji
— naprawiliśmy wyświetlanie frekwencji w szkołach, gdzie działa już system eduOne (ciągle jednak brak opcji usprawiedliwiania) — poprawiliśmy widżet planu lekcji (będzie teraz trochę bardziej kompaktowy)
— zmieniliśmy datę rozpoczęcia roku szkolnego na 3 dni przed 1 września (sorry)
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases

View File

@ -2,5 +2,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<solid android:color="#FF000000" /> <solid android:color="#FF000000" />
<corners android:radius="12dp" /> <corners android:radius="14dp" />
</shape> </shape>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"> <shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#FFFFFBFF" /> <solid android:color="#FFFFFBFF" />
<corners android:radius="12dp" /> <corners android:radius="14dp" />
</shape> </shape>

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="20dp"
android:height="24dp" android:height="20dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path <path

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="20dp"
android:height="24dp" android:height="20dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path <path

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View File

@ -5,10 +5,10 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@drawable/background_widget_timetable" android:background="@drawable/background_widget_timetable"
android:backgroundTint="?attr/colorSurface" android:backgroundTint="?attr/colorSecondaryContainer"
android:clipToOutline="true" android:clipToOutline="true"
android:orientation="vertical" android:orientation="vertical"
android:paddingHorizontal="16dp" android:paddingHorizontal="12dp"
android:theme="@style/Wulkanowy.Widget.Theme" android:theme="@style/Wulkanowy.Widget.Theme"
tools:context=".ui.modules.timetablewidget.TimetableWidgetProvider"> tools:context=".ui.modules.timetablewidget.TimetableWidgetProvider">
@ -16,23 +16,47 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal" android:paddingTop="12dp"
android:paddingVertical="16dp"> android:paddingBottom="8dp">
<FrameLayout
android:layout_width="48dp"
android:layout_height="48dp">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:importantForAccessibility="no"
android:src="@drawable/background_timetable_widget_avatar"
android:tint="?attr/colorPrimary"
app:tint="?attr/colorPrimary"
tools:ignore="UseAppTint" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="JK"
android:textAppearance="?attr/textAppearanceTitleLarge"
android:textColor="?attr/colorOnPrimary"
android:textSize="18sp"
tools:ignore="HardcodedText" />
</FrameLayout>
<TextView <TextView
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginHorizontal="12dp"
android:layout_weight="1" android:layout_weight="1"
android:lines="1" android:lines="1"
android:text="Pon, 03.10" android:text="Pon, 19.05"
android:textAppearance="?attr/textAppearanceHeadline5" android:textSize="18sp"
tools:ignore="HardcodedText" /> tools:ignore="HardcodedText" />
<ImageButton <ImageButton
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:layout_marginStart="12dp"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/all_prev" android:contentDescription="@string/all_prev"
android:rotation="180" android:rotation="180"
@ -51,46 +75,273 @@
app:tint="?attr/colorPrimary" app:tint="?attr/colorPrimary"
tools:ignore="UseAppTint" /> tools:ignore="UseAppTint" />
<TextView
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="12dp"
android:background="@drawable/background_timetable_widget_avatar"
android:backgroundTint="?attr/colorPrimary"
android:contentDescription="@string/account_quick_manager"
android:gravity="center"
android:text="AW"
android:textAppearance="?attr/textAppearanceTitleLarge"
android:textColor="?attr/colorOnPrimary" />
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:showDividers="middle"> android:paddingBottom="12dp">
<ImageView <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="56dp" android:layout_height="wrap_content"
android:layout_marginBottom="4dp" android:layout_marginBottom="4dp"
android:importantForAccessibility="no" android:background="@drawable/background_widget_item_timetable"
android:src="@drawable/background_widget_item_timetable" android:backgroundTint="?attr/colorSurface"
app:tint="?attr/backgroundColor" /> android:gravity="center_vertical"
android:minHeight="48dp"
android:orientation="horizontal"
android:paddingHorizontal="12dp"
android:paddingVertical="8dp">
<ImageView <TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="1"
android:textSize="22sp"
tools:ignore="HardcodedText" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:gravity="center_vertical"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="08:00"
android:textAppearance="?attr/textAppearanceBodySmall"
tools:ignore="HardcodedText" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="09:45"
android:textAppearance="?attr/textAppearanceBodySmall"
tools:ignore="HardcodedText" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lines="1"
android:text="Wychowanie fizyczne"
android:textSize="14sp"
tools:ignore="HardcodedText" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:text="213"
android:textSize="12sp"
tools:ignore="HardcodedText" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lines="1"
android:text="Dorota Nowak"
android:textAppearance="?attr/textAppearanceBodySmall"
tools:ignore="HardcodedText" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="56dp" android:layout_height="wrap_content"
android:layout_marginBottom="4dp" android:layout_marginBottom="4dp"
android:importantForAccessibility="no" android:background="@drawable/background_widget_item_timetable"
android:src="@drawable/background_widget_item_timetable" android:backgroundTint="?attr/colorSurface"
app:tint="?attr/backgroundColor" /> android:gravity="center_vertical"
android:minHeight="48dp"
android:orientation="horizontal"
android:paddingHorizontal="12dp"
android:paddingVertical="8dp">
<ImageView <TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="2"
android:textSize="22sp"
tools:ignore="HardcodedText"
tools:textColor="?attr/colorTimetableChange" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:gravity="center_vertical"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="08:50"
android:textAppearance="?attr/textAppearanceBodySmall"
tools:ignore="HardcodedText" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="09:35"
android:textAppearance="?attr/textAppearanceBodySmall"
tools:ignore="HardcodedText" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lines="1"
android:text="Język polski"
android:textColor="?attr/colorTimetableChange"
android:textSize="14sp"
tools:ignore="HardcodedText" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:text="125"
android:textSize="12sp"
tools:ignore="HardcodedText" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lines="1"
android:text="Karolina Kowalska"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorTimetableChange"
tools:ignore="HardcodedText" />
</LinearLayout>
</LinearLayout>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:contentDescription="@string/timetable_changes"
android:tint="?attr/colorTimetableChange"
app:tint="?attr/colorTimetableChange"
tools:ignore="UseAppTint"
tools:src="@drawable/ic_timetable_widget_swap" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="56dp" android:layout_height="wrap_content"
android:importantForAccessibility="no" android:background="@drawable/background_widget_item_timetable"
android:src="@drawable/background_widget_item_timetable" android:backgroundTint="?attr/colorSurface"
app:tint="?attr/backgroundColor" /> android:gravity="center_vertical"
android:minHeight="48dp"
android:orientation="horizontal"
android:paddingHorizontal="12dp"
android:paddingVertical="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="3"
android:textSize="22sp"
tools:ignore="HardcodedText"
tools:textColor="?attr/colorTimetableCanceled" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:gravity="center_vertical"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="09:45"
android:textAppearance="?attr/textAppearanceBodySmall"
tools:ignore="HardcodedText" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="10:30"
android:textAppearance="?attr/textAppearanceBodySmall"
tools:ignore="HardcodedText" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lines="1"
android:text="Wiedza o społeczeństwie"
android:textColor="?attr/colorTimetableCanceled"
android:textSize="14sp"
tools:ignore="HardcodedText" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Lekcja odwołana: uczniowie zwolnieni do domu"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorTimetableCanceled"
tools:ignore="HardcodedText" />
</LinearLayout>
</LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@ -105,6 +105,18 @@
android:background="?android:attr/listDivider" /> android:background="?android:attr/listDivider" />
</LinearLayout> </LinearLayout>
<include
android:id="@+id/login_form_message"
layout="@layout/item_dashboard_admin_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/loginFormContact"
app:layout_constraintVertical_chainStyle="packed"
tools:visibility="visible" />
<TextView <TextView
android:id="@+id/loginFormHeader" android:id="@+id/loginFormHeader"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -119,9 +131,8 @@
app:layout_constraintBottom_toTopOf="@+id/loginFormUsernameLayout" app:layout_constraintBottom_toTopOf="@+id/loginFormUsernameLayout"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginFormContact" app:layout_constraintTop_toBottomOf="@+id/login_form_message"
app:layout_constraintVertical_bias="0" app:layout_constraintVertical_bias="0"
app:layout_constraintVertical_chainStyle="packed"
app:layout_goneMarginTop="64dp" /> app:layout_goneMarginTop="64dp" />
<TextView <TextView

View File

@ -0,0 +1,43 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:paddingStart="8dp"
android:paddingTop="6dp"
android:paddingEnd="12dp"
android:paddingBottom="6dp"
tools:context=".ui.modules.timetable.TimetableAdapter">
<TextView
android:id="@+id/timetableEmptyItemNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:includeFontPadding="false"
android:maxLength="5"
android:minWidth="40dp"
android:minHeight="40dp"
android:textColor="?android:textColorHint"
android:textSize="32sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="1-4" />
<TextView
android:id="@+id/timetableEmptyItemSubject"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorHint"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="@+id/timetableEmptyItemNumber"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/timetableEmptyItemNumber"
app:layout_constraintTop_toTopOf="@+id/timetableEmptyItemNumber"
tools:text="No lessons" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -6,11 +6,12 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/background_widget_item_timetable" android:background="@drawable/background_widget_item_timetable"
android:backgroundTint="?attr/backgroundColor" android:backgroundTint="?attr/colorSurface"
android:gravity="center_vertical" android:gravity="center_vertical"
android:minHeight="48dp"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingHorizontal="16dp" android:paddingHorizontal="12dp"
android:paddingVertical="12dp" android:paddingVertical="8dp"
android:theme="@style/Wulkanowy.Widget.Theme" android:theme="@style/Wulkanowy.Widget.Theme"
tools:context=".ui.modules.timetablewidget.TimetableWidgetFactory"> tools:context=".ui.modules.timetablewidget.TimetableWidgetFactory">
@ -18,15 +19,14 @@
android:id="@+id/timetableWidgetItemNumber" android:id="@+id/timetableWidgetItemNumber"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceHeadline6" android:textSize="22sp"
android:textSize="24sp"
tools:text="1" tools:text="1"
tools:textColor="?attr/colorTimetableChange" /> tools:textColor="?attr/colorTimetableChange" />
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="12dp" android:layout_marginStart="10dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="vertical"> android:orientation="vertical">
@ -41,7 +41,7 @@
android:id="@+id/timetableWidgetItemTimeFinish" android:id="@+id/timetableWidgetItemTimeFinish"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:layout_marginTop="2dp"
android:textAppearance="?attr/textAppearanceBodySmall" android:textAppearance="?attr/textAppearanceBodySmall"
tools:text="09:45" /> tools:text="09:45" />
@ -60,7 +60,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="end" android:ellipsize="end"
android:lines="1" android:lines="1"
android:textAppearance="?attr/textAppearanceTitleMedium" android:textSize="14sp"
tools:text="Programowanie aplikacji mobilnych i desktopowych" /> tools:text="Programowanie aplikacji mobilnych i desktopowych" />
<LinearLayout <LinearLayout
@ -72,15 +72,15 @@
android:id="@+id/timetableWidgetItemRoom" android:id="@+id/timetableWidgetItemRoom"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:textAppearance="?attr/textAppearanceBodySmall" android:textAppearance="?attr/textAppearanceBodySmall"
tools:text="Sala 213" tools:text="213"
tools:textColor="?attr/colorTimetableChange" /> tools:textColor="?attr/colorTimetableChange" />
<TextView <TextView
android:id="@+id/timetableWidgetItemTeacher" android:id="@+id/timetableWidgetItemTeacher"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:ellipsize="end" android:ellipsize="end"
android:lines="1" android:lines="1"
android:textAppearance="?attr/textAppearanceBodySmall" android:textAppearance="?attr/textAppearanceBodySmall"

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/timetableWidgetEmptyItemContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/background_widget_item_timetable"
android:backgroundTint="?attr/colorSurface"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="12dp"
android:paddingVertical="8dp"
android:theme="@style/Wulkanowy.Widget.Theme"
tools:context=".ui.modules.timetablewidget.TimetableWidgetFactory">
<TextView
android:id="@+id/timetableWidgetEmptyItemNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceHeadline6"
android:textColor="?android:textColorHint"
android:textSize="22sp"
tools:text="1-4" />
<TextView
android:id="@+id/timetableWidgetEmptyItemText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:ellipsize="end"
android:lines="1"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?android:textColorHint"
tools:text="No lessons" />
</LinearLayout>

View File

@ -5,35 +5,70 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@drawable/background_widget_timetable" android:background="@drawable/background_widget_timetable"
android:backgroundTint="?attr/colorSurface" android:backgroundTint="?attr/colorSecondaryContainer"
android:clipToOutline="true" android:clipToOutline="true"
android:orientation="vertical" android:orientation="vertical"
android:paddingHorizontal="16dp" android:paddingHorizontal="12dp"
android:theme="@style/Wulkanowy.Widget.Theme" android:theme="@style/Wulkanowy.Widget.Theme"
tools:context=".ui.modules.timetablewidget.TimetableWidgetProvider"> tools:context=".ui.modules.timetablewidget.TimetableWidgetProvider"
tools:targetApi="s">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal" android:paddingTop="12dp"
android:paddingVertical="16dp"> android:paddingBottom="8dp">
<FrameLayout
android:id="@+id/timetableWidgetAccount"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/background_timetable_widget_avatar"
android:backgroundTint="@android:color/transparent"
android:clickable="true"
android:clipToOutline="true"
android:contentDescription="@string/account_quick_manager"
android:focusable="true"
android:foreground="?attr/selectableItemBackgroundBorderless"
android:importantForAccessibility="yes"
android:outlineProvider="background"
tools:targetApi="s">
<ImageView
android:id="@+id/timetableWidgetAccountBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:importantForAccessibility="no"
tools:src="@drawable/background_timetable_widget_avatar"
tools:tint="?attr/colorPrimary" />
<TextView
android:id="@+id/timetableWidgetAccountInitials"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textAppearance="?attr/textAppearanceTitleLarge"
android:textColor="@android:color/white"
android:textSize="18sp"
tools:text="JK" />
</FrameLayout>
<TextView <TextView
android:id="@+id/timetableWidgetDate" android:id="@+id/timetableWidgetDate"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginHorizontal="12dp"
android:layout_weight="1" android:layout_weight="1"
android:lines="1" android:lines="1"
android:textAppearance="?attr/textAppearanceHeadline5" android:textSize="18sp"
tools:text="Pon, 12.05" /> tools:text="Friday, 19.05" />
<ImageButton <ImageButton
android:id="@+id/timetableWidgetPrev" android:id="@+id/timetableWidgetPrev"
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:layout_marginStart="12dp"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/all_prev" android:contentDescription="@string/all_prev"
android:rotation="180" android:rotation="180"
@ -53,39 +88,6 @@
app:tint="?attr/colorPrimary" app:tint="?attr/colorPrimary"
tools:ignore="UseAppTint" /> tools:ignore="UseAppTint" />
<FrameLayout
android:id="@+id/timetableWidgetAccount"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="12dp"
android:background="@drawable/background_timetable_widget_avatar"
android:backgroundTint="@android:color/transparent"
android:clickable="true"
android:clipToOutline="true"
android:contentDescription="@string/account_quick_manager"
android:focusable="true"
android:foreground="?attr/selectableItemBackgroundBorderless"
android:importantForAccessibility="yes"
android:outlineProvider="background">
<ImageView
android:id="@+id/timetableWidgetAccountBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:importantForAccessibility="no"
tools:src="@drawable/background_timetable_widget_avatar"
tools:tint="?attr/colorPrimary" />
<TextView
android:id="@+id/timetableWidgetAccountInitials"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textAppearance="?attr/textAppearanceTitleLarge"
android:textColor="@android:color/white"
android:textSize="20sp"
tools:text="AW" />
</FrameLayout>
</LinearLayout> </LinearLayout>
<FrameLayout <FrameLayout
@ -99,6 +101,7 @@
android:clipToPadding="false" android:clipToPadding="false"
android:divider="@android:color/transparent" android:divider="@android:color/transparent"
android:dividerHeight="4dp" android:dividerHeight="4dp"
android:listSelector="@android:color/transparent"
android:paddingBottom="16dp" android:paddingBottom="16dp"
tools:listfooter="@layout/item_widget_timetable_footer" tools:listfooter="@layout/item_widget_timetable_footer"
tools:listitem="@layout/item_widget_timetable" /> tools:listitem="@layout/item_widget_timetable" />
@ -111,5 +114,7 @@
android:text="@string/widget_timetable_no_items" android:text="@string/widget_timetable_no_items"
android:textAppearance="?attr/textAppearanceBody1" android:textAppearance="?attr/textAppearanceBody1"
android:visibility="gone" /> android:visibility="gone" />
</FrameLayout> </FrameLayout>
</LinearLayout> </LinearLayout>

View File

@ -51,6 +51,11 @@
<item>Průměr z průměrů z obou semestrů</item> <item>Průměr z průměrů z obou semestrů</item>
<item>Průměr známek z celého roku</item> <item>Průměr známek z celého roku</item>
</string-array> </string-array>
<string-array name="timetable_show_gaps_entries">
<item>Don\'t show</item>
<item>Only between lessons</item>
<item>Before and between lessons</item>
</string-array>
<string-array name="dashboard_tile_entries"> <string-array name="dashboard_tile_entries">
<item>Šťastné číslo</item> <item>Šťastné číslo</item>
<item>Nepřečtené zprávy</item> <item>Nepřečtené zprávy</item>

View File

@ -37,7 +37,7 @@
<string name="login_login_pesel_email_hint">Přihlášení, číslo PESEL nebo e-mail</string> <string name="login_login_pesel_email_hint">Přihlášení, číslo PESEL nebo e-mail</string>
<string name="login_password_hint">Heslo</string> <string name="login_password_hint">Heslo</string>
<string name="login_host_hint">Variace deníku UONET+</string> <string name="login_host_hint">Variace deníku UONET+</string>
<string name="login_domain_suffix_hint">Custom domain suffix</string> <string name="login_domain_suffix_hint">Vlastní přípona domény</string>
<string name="login_type_api">Mobile API</string> <string name="login_type_api">Mobile API</string>
<string name="login_type_scrapper">Scraper</string> <string name="login_type_scrapper">Scraper</string>
<string name="login_type_hybrid">Hybridní</string> <string name="login_type_hybrid">Hybridní</string>
@ -185,6 +185,12 @@
<string name="timetable_notify_change_room">Změna učebny z %1$s na %2$s</string> <string name="timetable_notify_change_room">Změna učebny z %1$s na %2$s</string>
<string name="timetable_notify_change_teacher">Změna učitele z %1$s na %2$s</string> <string name="timetable_notify_change_teacher">Změna učitele z %1$s na %2$s</string>
<string name="timetable_notify_change_subject">Změna předmětu z %1$s na %2$s</string> <string name="timetable_notify_change_subject">Změna předmětu z %1$s na %2$s</string>
<plurals name="timetable_no_lesson">
<item quantity="one">No lesson</item>
<item quantity="few">No lessons</item>
<item quantity="many">No lessons</item>
<item quantity="other">No lessons</item>
</plurals>
<plurals name="timetable_notify_new_items_title"> <plurals name="timetable_notify_new_items_title">
<item quantity="one">Změna plánu lekcí</item> <item quantity="one">Změna plánu lekcí</item>
<item quantity="few">Změny plánu lekcí</item> <item quantity="few">Změny plánu lekcí</item>
@ -352,6 +358,8 @@
</plurals> </plurals>
<string name="message_messages_deleted">Zprávy odstraněné</string> <string name="message_messages_deleted">Zprávy odstraněné</string>
<string name="message_mailbox_chooser_title">Vyberte poštovní schránku</string> <string name="message_mailbox_chooser_title">Vyberte poštovní schránku</string>
<string name="message_incognito_mode_on">Anonymní režim je zapnutý</string>
<string name="message_incognito_description">Díky anonymnímu režimu není odesílatel upozorněn, když si zprávu přečtete</string>
<!--Note--> <!--Note-->
<string name="note_no_items">Žádné informace o poznámkách</string> <string name="note_no_items">Žádné informace o poznámkách</string>
<string name="note_points">Body</string> <string name="note_points">Body</string>
@ -698,6 +706,7 @@
<string name="pref_view_expand_grade">Rozvíjení známek</string> <string name="pref_view_expand_grade">Rozvíjení známek</string>
<string name="pref_view_timetable_show_timers">Označit aktuální lekci</string> <string name="pref_view_timetable_show_timers">Označit aktuální lekci</string>
<string name="pref_view_timetable_show_groups">Zobrazit skupiny vedle předmětů</string> <string name="pref_view_timetable_show_groups">Zobrazit skupiny vedle předmětů</string>
<string name="pref_view_timetable_show_gaps">Show empty tiles where there\'s no lesson</string>
<string name="pref_view_grade_statistics_list">Zobrazit seznam grafů v známkách třídy</string> <string name="pref_view_grade_statistics_list">Zobrazit seznam grafů v známkách třídy</string>
<string name="pref_view_subjects_without_grades">Zobrazit předměty bez známek</string> <string name="pref_view_subjects_without_grades">Zobrazit předměty bez známek</string>
<string name="pref_view_grade_color_scheme">Známky barevné schéma</string> <string name="pref_view_grade_color_scheme">Známky barevné schéma</string>
@ -738,6 +747,8 @@
<string name="pref_other_grade_modifier_minus">Hodnota mínusu</string> <string name="pref_other_grade_modifier_minus">Hodnota mínusu</string>
<string name="pref_other_fill_message_content">Odpovědět s historií zpráv</string> <string name="pref_other_fill_message_content">Odpovědět s historií zpráv</string>
<string name="pref_other_optional_arithmetic_average">Vypočítat aritmetický průměr, pokud žádná známka nemá váhu</string> <string name="pref_other_optional_arithmetic_average">Vypočítat aritmetický průměr, pokud žádná známka nemá váhu</string>
<string name="pref_other_incognito_mode">Anonymní režim</string>
<string name="pref_other_incognito_mode_summary">Neinformovat o přečtení zprávy</string>
<string name="pref_ads_support_category_name">Podpora</string> <string name="pref_ads_support_category_name">Podpora</string>
<string name="pref_ads_privacy_policy">Ochrana osobních údajů</string> <string name="pref_ads_privacy_policy">Ochrana osobních údajů</string>
<string name="pref_ads_agreements">Souhlasy</string> <string name="pref_ads_agreements">Souhlasy</string>

View File

@ -51,6 +51,11 @@
<item>Average of averages from both semesters</item> <item>Average of averages from both semesters</item>
<item>Average of grades from the whole year</item> <item>Average of grades from the whole year</item>
</string-array> </string-array>
<string-array name="timetable_show_gaps_entries">
<item>Don\'t show</item>
<item>Only between lessons</item>
<item>Before and between lessons</item>
</string-array>
<string-array name="dashboard_tile_entries"> <string-array name="dashboard_tile_entries">
<item>Lucky number</item> <item>Lucky number</item>
<item>Unread messages</item> <item>Unread messages</item>

View File

@ -171,6 +171,10 @@
<string name="timetable_notify_change_room">Change of room from %1$s to %2$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_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> <string name="timetable_notify_change_subject">Change of subject from %1$s to %2$s</string>
<plurals name="timetable_no_lesson">
<item quantity="one">No lesson</item>
<item quantity="other">No lessons</item>
</plurals>
<plurals name="timetable_notify_new_items_title"> <plurals name="timetable_notify_new_items_title">
<item quantity="one">Timetable change</item> <item quantity="one">Timetable change</item>
<item quantity="other">Timetable changes</item> <item quantity="other">Timetable changes</item>
@ -310,6 +314,8 @@
</plurals> </plurals>
<string name="message_messages_deleted">Messages deleted</string> <string name="message_messages_deleted">Messages deleted</string>
<string name="message_mailbox_chooser_title">Choose mailbox</string> <string name="message_mailbox_chooser_title">Choose mailbox</string>
<string name="message_incognito_mode_on">Incognito mode is on</string>
<string name="message_incognito_description">Thanks to incognito mode sender is not notified when you read the message</string>
<!--Note--> <!--Note-->
<string name="note_no_items">No info about notes</string> <string name="note_no_items">No info about notes</string>
<string name="note_points">Points</string> <string name="note_points">Points</string>
@ -610,6 +616,7 @@
<string name="pref_view_expand_grade">Grades expanding</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_timers">Mark current lesson</string>
<string name="pref_view_timetable_show_groups">Show groups next to subjects</string> <string name="pref_view_timetable_show_groups">Show groups next to subjects</string>
<string name="pref_view_timetable_show_gaps">Show empty tiles where there\'s no lesson</string>
<string name="pref_view_grade_statistics_list">Show chart list in class grades</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_subjects_without_grades">Show subjects without grades</string>
<string name="pref_view_grade_color_scheme">Grades color scheme</string> <string name="pref_view_grade_color_scheme">Grades color scheme</string>
@ -650,6 +657,8 @@
<string name="pref_other_grade_modifier_minus">Value of the minus</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_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_other_optional_arithmetic_average">Show arithmetic average when no weights provided</string>
<string name="pref_other_incognito_mode">Incognito mode</string>
<string name="pref_other_incognito_mode_summary">Do not inform about reading the message</string>
<string name="pref_ads_support_category_name">Support</string> <string name="pref_ads_support_category_name">Support</string>
<string name="pref_ads_privacy_policy">Privacy Policy</string> <string name="pref_ads_privacy_policy">Privacy Policy</string>
<string name="pref_ads_agreements">Agreements</string> <string name="pref_ads_agreements">Agreements</string>

View File

@ -51,6 +51,11 @@
<item>Durchschnittswert der Durchschnittswerte beider Semester</item> <item>Durchschnittswert der Durchschnittswerte beider Semester</item>
<item>Durchschnitt der Noten aus dem ganzen Jahr</item> <item>Durchschnitt der Noten aus dem ganzen Jahr</item>
</string-array> </string-array>
<string-array name="timetable_show_gaps_entries">
<item>Don\'t show</item>
<item>Only between lessons</item>
<item>Before and between lessons</item>
</string-array>
<string-array name="dashboard_tile_entries"> <string-array name="dashboard_tile_entries">
<item>Glückszahl</item> <item>Glückszahl</item>
<item>Ungelesene Nachrichten</item> <item>Ungelesene Nachrichten</item>

View File

@ -171,6 +171,10 @@
<string name="timetable_notify_change_room">Änderung des Raumes von %1$s zu %2$s</string> <string name="timetable_notify_change_room">Änderung des Raumes von %1$s zu %2$s</string>
<string name="timetable_notify_change_teacher">Wechsel des Lehrers von %1$s zu %2$s</string> <string name="timetable_notify_change_teacher">Wechsel des Lehrers von %1$s zu %2$s</string>
<string name="timetable_notify_change_subject">Thema von %1$s zu %2$s wechseln</string> <string name="timetable_notify_change_subject">Thema von %1$s zu %2$s wechseln</string>
<plurals name="timetable_no_lesson">
<item quantity="one">No lesson</item>
<item quantity="other">No lessons</item>
</plurals>
<plurals name="timetable_notify_new_items_title"> <plurals name="timetable_notify_new_items_title">
<item quantity="one">Änderung des Zeitplans</item> <item quantity="one">Änderung des Zeitplans</item>
<item quantity="other">Änderungen des Zeitplans</item> <item quantity="other">Änderungen des Zeitplans</item>
@ -310,6 +314,8 @@
</plurals> </plurals>
<string name="message_messages_deleted">Nachrichten gelöscht</string> <string name="message_messages_deleted">Nachrichten gelöscht</string>
<string name="message_mailbox_chooser_title">Postfach auswählen</string> <string name="message_mailbox_chooser_title">Postfach auswählen</string>
<string name="message_incognito_mode_on">Incognito mode is on</string>
<string name="message_incognito_description">Thanks to incognito mode sender is not notified when you read the message</string>
<!--Note--> <!--Note-->
<string name="note_no_items">Keine Informationen über Eintragen</string> <string name="note_no_items">Keine Informationen über Eintragen</string>
<string name="note_points">Punkte</string> <string name="note_points">Punkte</string>
@ -610,6 +616,7 @@
<string name="pref_view_expand_grade">Steigende Sorten</string> <string name="pref_view_expand_grade">Steigende Sorten</string>
<string name="pref_view_timetable_show_timers">Aktuelle Lektion markieren</string> <string name="pref_view_timetable_show_timers">Aktuelle Lektion markieren</string>
<string name="pref_view_timetable_show_groups">Gruppen neben Schulfächen anzeigen</string> <string name="pref_view_timetable_show_groups">Gruppen neben Schulfächen anzeigen</string>
<string name="pref_view_timetable_show_gaps">Show empty tiles where there\'s no lesson</string>
<string name="pref_view_grade_statistics_list">Liste der Diagramme in Klassenbewertungen anzeigen</string> <string name="pref_view_grade_statistics_list">Liste der Diagramme in Klassenbewertungen anzeigen</string>
<string name="pref_view_subjects_without_grades">Schulfächer ohne Noten anzeigen</string> <string name="pref_view_subjects_without_grades">Schulfächer ohne Noten anzeigen</string>
<string name="pref_view_grade_color_scheme">Farbschema der Noten</string> <string name="pref_view_grade_color_scheme">Farbschema der Noten</string>
@ -650,6 +657,8 @@
<string name="pref_other_grade_modifier_minus">Wert des Minus</string> <string name="pref_other_grade_modifier_minus">Wert des Minus</string>
<string name="pref_other_fill_message_content">Antwort mit Nachrichtenhistorie</string> <string name="pref_other_fill_message_content">Antwort mit Nachrichtenhistorie</string>
<string name="pref_other_optional_arithmetic_average">Arithmetisches Mittel anzeigen, wenn keine Gewichte angegeben sind</string> <string name="pref_other_optional_arithmetic_average">Arithmetisches Mittel anzeigen, wenn keine Gewichte angegeben sind</string>
<string name="pref_other_incognito_mode">Incognito mode</string>
<string name="pref_other_incognito_mode_summary">Do not inform about reading the message</string>
<string name="pref_ads_support_category_name">Unterstützung</string> <string name="pref_ads_support_category_name">Unterstützung</string>
<string name="pref_ads_privacy_policy">Datenschutz-Bestimmungen</string> <string name="pref_ads_privacy_policy">Datenschutz-Bestimmungen</string>
<string name="pref_ads_agreements">Vereinbarungen</string> <string name="pref_ads_agreements">Vereinbarungen</string>

View File

@ -51,6 +51,11 @@
<item>Average of averages from both semesters</item> <item>Average of averages from both semesters</item>
<item>Average of grades from the whole year</item> <item>Average of grades from the whole year</item>
</string-array> </string-array>
<string-array name="timetable_show_gaps_entries">
<item>Don\'t show</item>
<item>Only between lessons</item>
<item>Before and between lessons</item>
</string-array>
<string-array name="dashboard_tile_entries"> <string-array name="dashboard_tile_entries">
<item>Lucky number</item> <item>Lucky number</item>
<item>Unread messages</item> <item>Unread messages</item>

View File

@ -171,6 +171,10 @@
<string name="timetable_notify_change_room">Change of room from %1$s to %2$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_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> <string name="timetable_notify_change_subject">Change of subject from %1$s to %2$s</string>
<plurals name="timetable_no_lesson">
<item quantity="one">No lesson</item>
<item quantity="other">No lessons</item>
</plurals>
<plurals name="timetable_notify_new_items_title"> <plurals name="timetable_notify_new_items_title">
<item quantity="one">Timetable change</item> <item quantity="one">Timetable change</item>
<item quantity="other">Timetable changes</item> <item quantity="other">Timetable changes</item>
@ -310,6 +314,8 @@
</plurals> </plurals>
<string name="message_messages_deleted">Messages deleted</string> <string name="message_messages_deleted">Messages deleted</string>
<string name="message_mailbox_chooser_title">Choose mailbox</string> <string name="message_mailbox_chooser_title">Choose mailbox</string>
<string name="message_incognito_mode_on">Incognito mode is on</string>
<string name="message_incognito_description">Thanks to incognito mode sender is not notified when you read the message</string>
<!--Note--> <!--Note-->
<string name="note_no_items">No info about notes</string> <string name="note_no_items">No info about notes</string>
<string name="note_points">Points</string> <string name="note_points">Points</string>
@ -610,6 +616,7 @@
<string name="pref_view_expand_grade">Grades expanding</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_timers">Mark current lesson</string>
<string name="pref_view_timetable_show_groups">Show groups next to subjects</string> <string name="pref_view_timetable_show_groups">Show groups next to subjects</string>
<string name="pref_view_timetable_show_gaps">Show empty tiles where there\'s no lesson</string>
<string name="pref_view_grade_statistics_list">Show chart list in class grades</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_subjects_without_grades">Show subjects without grades</string>
<string name="pref_view_grade_color_scheme">Grades color scheme</string> <string name="pref_view_grade_color_scheme">Grades color scheme</string>
@ -650,6 +657,8 @@
<string name="pref_other_grade_modifier_minus">Value of the minus</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_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_other_optional_arithmetic_average">Show arithmetic average when no weights provided</string>
<string name="pref_other_incognito_mode">Incognito mode</string>
<string name="pref_other_incognito_mode_summary">Do not inform about reading the message</string>
<string name="pref_ads_support_category_name">Support</string> <string name="pref_ads_support_category_name">Support</string>
<string name="pref_ads_privacy_policy">Privacy Policy</string> <string name="pref_ads_privacy_policy">Privacy Policy</string>
<string name="pref_ads_agreements">Agreements</string> <string name="pref_ads_agreements">Agreements</string>

View File

@ -0,0 +1,70 @@
<?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="timetable_show_gaps_entries">
<item>Don\'t show</item>
<item>Only between lessons</item>
<item>Before and between lessons</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,756 @@
<?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>
<string name="menu_order_title">Menu configuartion</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_domain_suffix_hint">Custom domain suffix</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_no_lesson">
<item quantity="one">No lesson</item>
<item quantity="other">No lessons</item>
</plurals>
<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>
<string name="message_incognito_mode_on">Incognito mode is on</string>
<string name="message_incognito_description">Thanks to incognito mode sender is not notified when you read the message</string>
<!--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>
<string name="all_cancel">Cancel</string>
<!--Timetable Widget-->
<string name="widget_timetable_no_items">No lessons</string>
<string name="widget_timetable_last_synchronization">Synchronized on %1$s at %2$s</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_timetable_show_gaps">Show empty tiles where there\'s no lesson</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_view_menu_order_title">Menu configuration</string>
<string name="pref_view_menu_order_summary">Set the order of functions in the menu</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_other_incognito_mode">Incognito mode</string>
<string name="pref_other_incognito_mode_summary">Do not inform about reading the message</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>
<!--Menu order-->
<string name="menu_order_confirm_title">Application restart</string>
<string name="menu_order_confirm_content">The application must restart for the changes to be saved</string>
<string name="menu_order_confirm_restart">Restart</string>
<!--Auth-->
<string name="auth_api_error">Authorization has been rejected. The data provided does not match the records in the secretary\'s office.</string>
<string name="auth_invalid_error">Invalid PESEL</string>
<string name="auth_pesel">PESEL</string>
<string name="auth_button">Authorize</string>
<string name="auth_success">Authorization completed successfully</string>
<string name="auth_title">Authorization</string>
<string name="auth_description">To operate the application, we need to confirm your identity. Please enter the student\'s PESEL &lt;b&gt;%1$s&lt;/b&gt; in the field below</string>
<string name="auth_button_skip">Skip for now</string>
<!--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

@ -51,6 +51,11 @@
<item>Średnia ze średnich z obu semestrów</item> <item>Średnia ze średnich z obu semestrów</item>
<item>Średnia wszystkich ocen z całego roku</item> <item>Średnia wszystkich ocen z całego roku</item>
</string-array> </string-array>
<string-array name="timetable_show_gaps_entries">
<item>Nie pokauj</item>
<item>Tylko między lekcjami</item>
<item>Przed i między lekcjami</item>
</string-array>
<string-array name="dashboard_tile_entries"> <string-array name="dashboard_tile_entries">
<item>Szczęśliwy numerek</item> <item>Szczęśliwy numerek</item>
<item>Nieprzeczytane wiadomości</item> <item>Nieprzeczytane wiadomości</item>

View File

@ -185,6 +185,12 @@
<string name="timetable_notify_change_room">Zmiana sali z %1$s na %2$s</string> <string name="timetable_notify_change_room">Zmiana sali z %1$s na %2$s</string>
<string name="timetable_notify_change_teacher">Zmiana nauczyciela z %1$s na %2$s</string> <string name="timetable_notify_change_teacher">Zmiana nauczyciela z %1$s na %2$s</string>
<string name="timetable_notify_change_subject">Zmiana przedmiotu z %1$s na %2$s</string> <string name="timetable_notify_change_subject">Zmiana przedmiotu z %1$s na %2$s</string>
<plurals name="timetable_no_lesson">
<item quantity="one">Brak lekcji</item>
<item quantity="few">Brak lekcji</item>
<item quantity="many">Brak lekcji</item>
<item quantity="other">Brak lekcji</item>
</plurals>
<plurals name="timetable_notify_new_items_title"> <plurals name="timetable_notify_new_items_title">
<item quantity="one">Zmiana planu lekcji</item> <item quantity="one">Zmiana planu lekcji</item>
<item quantity="few">Zmiany planu lekcji</item> <item quantity="few">Zmiany planu lekcji</item>
@ -352,6 +358,8 @@
</plurals> </plurals>
<string name="message_messages_deleted">Wiadomości zostały usunięte</string> <string name="message_messages_deleted">Wiadomości zostały usunięte</string>
<string name="message_mailbox_chooser_title">Wybierz skrzynkę</string> <string name="message_mailbox_chooser_title">Wybierz skrzynkę</string>
<string name="message_incognito_mode_on">Tryb incognito jest włączony</string>
<string name="message_incognito_description">Dzięki trybowi incognito nadawca nie zobaczy, że przeczytałeś tę wiadomość</string>
<!--Note--> <!--Note-->
<string name="note_no_items">Brak informacji o uwagach</string> <string name="note_no_items">Brak informacji o uwagach</string>
<string name="note_points">Punkty</string> <string name="note_points">Punkty</string>
@ -698,6 +706,7 @@
<string name="pref_view_expand_grade">Rozwijanie ocen</string> <string name="pref_view_expand_grade">Rozwijanie ocen</string>
<string name="pref_view_timetable_show_timers">Oznaczaj bieżącą lekcję</string> <string name="pref_view_timetable_show_timers">Oznaczaj bieżącą lekcję</string>
<string name="pref_view_timetable_show_groups">Pokazuj grupę obok przedmiotu</string> <string name="pref_view_timetable_show_groups">Pokazuj grupę obok przedmiotu</string>
<string name="pref_view_timetable_show_gaps">Pokazuj puste kafelki gdzie nie ma lekcji</string>
<string name="pref_view_grade_statistics_list">Pokazuj listę wykresów w ocenach klasy</string> <string name="pref_view_grade_statistics_list">Pokazuj listę wykresów w ocenach klasy</string>
<string name="pref_view_subjects_without_grades">Pokazuj przedmioty bez ocen</string> <string name="pref_view_subjects_without_grades">Pokazuj przedmioty bez ocen</string>
<string name="pref_view_grade_color_scheme">Schemat kolorów ocen</string> <string name="pref_view_grade_color_scheme">Schemat kolorów ocen</string>
@ -738,6 +747,8 @@
<string name="pref_other_grade_modifier_minus">Wartość minusa</string> <string name="pref_other_grade_modifier_minus">Wartość minusa</string>
<string name="pref_other_fill_message_content">Odpowiadaj z historią wiadomości</string> <string name="pref_other_fill_message_content">Odpowiadaj z historią wiadomości</string>
<string name="pref_other_optional_arithmetic_average">Licz średnią arytmetyczną, gdy żadna ocena nie ma wagi</string> <string name="pref_other_optional_arithmetic_average">Licz średnią arytmetyczną, gdy żadna ocena nie ma wagi</string>
<string name="pref_other_incognito_mode">Tryb incognito</string>
<string name="pref_other_incognito_mode_summary">Nie informuj o przeczytaniu wiadomości</string>
<string name="pref_ads_support_category_name">Wsparcie</string> <string name="pref_ads_support_category_name">Wsparcie</string>
<string name="pref_ads_privacy_policy">Polityka prywatności</string> <string name="pref_ads_privacy_policy">Polityka prywatności</string>
<string name="pref_ads_agreements">Zgody</string> <string name="pref_ads_agreements">Zgody</string>

View File

@ -51,6 +51,11 @@
<item>Средняя из средних оценок семестров</item> <item>Средняя из средних оценок семестров</item>
<item>Средняя из оценок со всего года</item> <item>Средняя из оценок со всего года</item>
</string-array> </string-array>
<string-array name="timetable_show_gaps_entries">
<item>Don\'t show</item>
<item>Only between lessons</item>
<item>Before and between lessons</item>
</string-array>
<string-array name="dashboard_tile_entries"> <string-array name="dashboard_tile_entries">
<item>Счастливый номер</item> <item>Счастливый номер</item>
<item>Непрочитанные письма</item> <item>Непрочитанные письма</item>

View File

@ -185,6 +185,12 @@
<string name="timetable_notify_change_room">Аудитория изменена с %1$s на %2$s</string> <string name="timetable_notify_change_room">Аудитория изменена с %1$s на %2$s</string>
<string name="timetable_notify_change_teacher">Учитель изменён с %1$s на %2$s</string> <string name="timetable_notify_change_teacher">Учитель изменён с %1$s на %2$s</string>
<string name="timetable_notify_change_subject">Тема изменена с %1$s на %2$s</string> <string name="timetable_notify_change_subject">Тема изменена с %1$s на %2$s</string>
<plurals name="timetable_no_lesson">
<item quantity="one">No lesson</item>
<item quantity="few">No lessons</item>
<item quantity="many">No lessons</item>
<item quantity="other">No lessons</item>
</plurals>
<plurals name="timetable_notify_new_items_title"> <plurals name="timetable_notify_new_items_title">
<item quantity="one">Изменение расписания</item> <item quantity="one">Изменение расписания</item>
<item quantity="few">Изменения расписания</item> <item quantity="few">Изменения расписания</item>
@ -352,6 +358,8 @@
</plurals> </plurals>
<string name="message_messages_deleted">Сообщение удалено</string> <string name="message_messages_deleted">Сообщение удалено</string>
<string name="message_mailbox_chooser_title">Выбрать почтовый ящик</string> <string name="message_mailbox_chooser_title">Выбрать почтовый ящик</string>
<string name="message_incognito_mode_on">Incognito mode is on</string>
<string name="message_incognito_description">Thanks to incognito mode sender is not notified when you read the message</string>
<!--Note--> <!--Note-->
<string name="note_no_items">Нет записей о замечаниях и свершениях</string> <string name="note_no_items">Нет записей о замечаниях и свершениях</string>
<string name="note_points">Баллы</string> <string name="note_points">Баллы</string>
@ -698,6 +706,7 @@
<string name="pref_view_expand_grade">Разворачивание оценок</string> <string name="pref_view_expand_grade">Разворачивание оценок</string>
<string name="pref_view_timetable_show_timers">Отметить текущий урок</string> <string name="pref_view_timetable_show_timers">Отметить текущий урок</string>
<string name="pref_view_timetable_show_groups">Показать группы рядом с темами</string> <string name="pref_view_timetable_show_groups">Показать группы рядом с темами</string>
<string name="pref_view_timetable_show_gaps">Show empty tiles where there\'s no lesson</string>
<string name="pref_view_grade_statistics_list">Показывать диаграммы в оценках класса</string> <string name="pref_view_grade_statistics_list">Показывать диаграммы в оценках класса</string>
<string name="pref_view_subjects_without_grades">Показать предметы без оценок</string> <string name="pref_view_subjects_without_grades">Показать предметы без оценок</string>
<string name="pref_view_grade_color_scheme">Цветовая схема оценок</string> <string name="pref_view_grade_color_scheme">Цветовая схема оценок</string>
@ -738,6 +747,8 @@
<string name="pref_other_grade_modifier_minus">Стоимость минуса</string> <string name="pref_other_grade_modifier_minus">Стоимость минуса</string>
<string name="pref_other_fill_message_content">Отвечать с историей сообщений</string> <string name="pref_other_fill_message_content">Отвечать с историей сообщений</string>
<string name="pref_other_optional_arithmetic_average">Показывать среднее арифметическое при отсутствии стоимости</string> <string name="pref_other_optional_arithmetic_average">Показывать среднее арифметическое при отсутствии стоимости</string>
<string name="pref_other_incognito_mode">Incognito mode</string>
<string name="pref_other_incognito_mode_summary">Do not inform about reading the message</string>
<string name="pref_ads_support_category_name">Поддержка</string> <string name="pref_ads_support_category_name">Поддержка</string>
<string name="pref_ads_privacy_policy">Политика приватности</string> <string name="pref_ads_privacy_policy">Политика приватности</string>
<string name="pref_ads_agreements">Соглашения</string> <string name="pref_ads_agreements">Соглашения</string>

View File

@ -51,6 +51,11 @@
<item>Priemer z priemerov z oboch semestrov</item> <item>Priemer z priemerov z oboch semestrov</item>
<item>Priemer známok z celého roka</item> <item>Priemer známok z celého roka</item>
</string-array> </string-array>
<string-array name="timetable_show_gaps_entries">
<item>Don\'t show</item>
<item>Only between lessons</item>
<item>Before and between lessons</item>
</string-array>
<string-array name="dashboard_tile_entries"> <string-array name="dashboard_tile_entries">
<item>Šťastné číslo</item> <item>Šťastné číslo</item>
<item>Neprečítané správy</item> <item>Neprečítané správy</item>

View File

@ -37,7 +37,7 @@
<string name="login_login_pesel_email_hint">Prihlásenie, číslo PESEL alebo e-mail</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_password_hint">Heslo</string>
<string name="login_host_hint">Variácia denníka UONET+</string> <string name="login_host_hint">Variácia denníka UONET+</string>
<string name="login_domain_suffix_hint">Custom domain suffix</string> <string name="login_domain_suffix_hint">Vlastná prípona domény</string>
<string name="login_type_api">Mobile API</string> <string name="login_type_api">Mobile API</string>
<string name="login_type_scrapper">Scraper</string> <string name="login_type_scrapper">Scraper</string>
<string name="login_type_hybrid">Hybridné</string> <string name="login_type_hybrid">Hybridné</string>
@ -185,6 +185,12 @@
<string name="timetable_notify_change_room">Zmena učebne z %1$s na %2$s</string> <string name="timetable_notify_change_room">Zmena učebne z %1$s na %2$s</string>
<string name="timetable_notify_change_teacher">Zmena učiteľa z %1$s na %2$s</string> <string name="timetable_notify_change_teacher">Zmena učiteľa z %1$s na %2$s</string>
<string name="timetable_notify_change_subject">Zmena predmetu z %1$s na %2$s</string> <string name="timetable_notify_change_subject">Zmena predmetu z %1$s na %2$s</string>
<plurals name="timetable_no_lesson">
<item quantity="one">No lesson</item>
<item quantity="few">No lessons</item>
<item quantity="many">No lessons</item>
<item quantity="other">No lessons</item>
</plurals>
<plurals name="timetable_notify_new_items_title"> <plurals name="timetable_notify_new_items_title">
<item quantity="one">Zmena plánu lekcií</item> <item quantity="one">Zmena plánu lekcií</item>
<item quantity="few">Zmeny plánu lekcií</item> <item quantity="few">Zmeny plánu lekcií</item>
@ -352,6 +358,8 @@
</plurals> </plurals>
<string name="message_messages_deleted">Správy odstránené</string> <string name="message_messages_deleted">Správy odstránené</string>
<string name="message_mailbox_chooser_title">Vyberte poštovú schránku</string> <string name="message_mailbox_chooser_title">Vyberte poštovú schránku</string>
<string name="message_incognito_mode_on">Režim inkognito je zapnutý</string>
<string name="message_incognito_description">Vďaka inkognito režimu nie je odosielateľ upozornený, keď si správu prečítate</string>
<!--Note--> <!--Note-->
<string name="note_no_items">Žiadne informácie o poznámkach</string> <string name="note_no_items">Žiadne informácie o poznámkach</string>
<string name="note_points">Body</string> <string name="note_points">Body</string>
@ -698,6 +706,7 @@
<string name="pref_view_expand_grade">Rozvijanie známok</string> <string name="pref_view_expand_grade">Rozvijanie známok</string>
<string name="pref_view_timetable_show_timers">Označiť aktuálne lekciu</string> <string name="pref_view_timetable_show_timers">Označiť aktuálne lekciu</string>
<string name="pref_view_timetable_show_groups">Zobraziť skupiny vedľa predmetov</string> <string name="pref_view_timetable_show_groups">Zobraziť skupiny vedľa predmetov</string>
<string name="pref_view_timetable_show_gaps">Show empty tiles where there\'s no lesson</string>
<string name="pref_view_grade_statistics_list">Zobraziť zoznam grafov v známkach triedy</string> <string name="pref_view_grade_statistics_list">Zobraziť zoznam grafov v známkach triedy</string>
<string name="pref_view_subjects_without_grades">Zobraziť predmety bez známok</string> <string name="pref_view_subjects_without_grades">Zobraziť predmety bez známok</string>
<string name="pref_view_grade_color_scheme">Známky farebnú schému</string> <string name="pref_view_grade_color_scheme">Známky farebnú schému</string>
@ -738,6 +747,8 @@
<string name="pref_other_grade_modifier_minus">Hodnota mínusu</string> <string name="pref_other_grade_modifier_minus">Hodnota mínusu</string>
<string name="pref_other_fill_message_content">Odpovedať s históriou správ</string> <string name="pref_other_fill_message_content">Odpovedať s históriou správ</string>
<string name="pref_other_optional_arithmetic_average">Vypočítať aritmetický priemer, ak žiadna známka nemá váhu</string> <string name="pref_other_optional_arithmetic_average">Vypočítať aritmetický priemer, ak žiadna známka nemá váhu</string>
<string name="pref_other_incognito_mode">Režim inkognito</string>
<string name="pref_other_incognito_mode_summary">Neinformovať o prečítaní správy</string>
<string name="pref_ads_support_category_name">Podpora</string> <string name="pref_ads_support_category_name">Podpora</string>
<string name="pref_ads_privacy_policy">Ochrana osobných údajov</string> <string name="pref_ads_privacy_policy">Ochrana osobných údajov</string>
<string name="pref_ads_agreements">Súhlasy</string> <string name="pref_ads_agreements">Súhlasy</string>

View File

@ -51,6 +51,11 @@
<item>Середнє значення з обох семестрів</item> <item>Середнє значення з обох семестрів</item>
<item>Середня оцінка з цілого року</item> <item>Середня оцінка з цілого року</item>
</string-array> </string-array>
<string-array name="timetable_show_gaps_entries">
<item>Don\'t show</item>
<item>Only between lessons</item>
<item>Before and between lessons</item>
</string-array>
<string-array name="dashboard_tile_entries"> <string-array name="dashboard_tile_entries">
<item>Щасливий номер</item> <item>Щасливий номер</item>
<item>Непрочитані листи</item> <item>Непрочитані листи</item>

View File

@ -37,7 +37,7 @@
<string name="login_login_pesel_email_hint">Логін, PESEL або e-mail</string> <string name="login_login_pesel_email_hint">Логін, PESEL або e-mail</string>
<string name="login_password_hint">Пароль</string> <string name="login_password_hint">Пароль</string>
<string name="login_host_hint">Тип щоденника UONET+</string> <string name="login_host_hint">Тип щоденника UONET+</string>
<string name="login_domain_suffix_hint">Custom domain suffix</string> <string name="login_domain_suffix_hint">Користувацький суфікс домену</string>
<string name="login_type_api">Мobile API</string> <string name="login_type_api">Мobile API</string>
<string name="login_type_scrapper">Scraper</string> <string name="login_type_scrapper">Scraper</string>
<string name="login_type_hybrid">Hybrid</string> <string name="login_type_hybrid">Hybrid</string>
@ -185,6 +185,12 @@
<string name="timetable_notify_change_room">Зміна аудіторії з %1$s на %2$s</string> <string name="timetable_notify_change_room">Зміна аудіторії з %1$s на %2$s</string>
<string name="timetable_notify_change_teacher">Зміна вчителя з %1$s на %2$s</string> <string name="timetable_notify_change_teacher">Зміна вчителя з %1$s на %2$s</string>
<string name="timetable_notify_change_subject">Зміна теми з %1$s на %2$s</string> <string name="timetable_notify_change_subject">Зміна теми з %1$s на %2$s</string>
<plurals name="timetable_no_lesson">
<item quantity="one">No lesson</item>
<item quantity="few">No lessons</item>
<item quantity="many">No lessons</item>
<item quantity="other">No lessons</item>
</plurals>
<plurals name="timetable_notify_new_items_title"> <plurals name="timetable_notify_new_items_title">
<item quantity="one">Зміна у розкладі</item> <item quantity="one">Зміна у розкладі</item>
<item quantity="few">Зміни у розкладі</item> <item quantity="few">Зміни у розкладі</item>
@ -352,6 +358,8 @@
</plurals> </plurals>
<string name="message_messages_deleted">Листи видалено</string> <string name="message_messages_deleted">Листи видалено</string>
<string name="message_mailbox_chooser_title">Вибрати поштову скриньку</string> <string name="message_mailbox_chooser_title">Вибрати поштову скриньку</string>
<string name="message_incognito_mode_on">Режим анонімності включено</string>
<string name="message_incognito_description">Завдяки режиму анонімності, відправник не буде сповіщений коли ви прочитаєте повідомлення</string>
<!--Note--> <!--Note-->
<string name="note_no_items">Немає інформації о зауваженнях</string> <string name="note_no_items">Немає інформації о зауваженнях</string>
<string name="note_points">Бали</string> <string name="note_points">Бали</string>
@ -698,6 +706,7 @@
<string name="pref_view_expand_grade">Розгортання оцінок</string> <string name="pref_view_expand_grade">Розгортання оцінок</string>
<string name="pref_view_timetable_show_timers">Позначити поточний урок</string> <string name="pref_view_timetable_show_timers">Позначити поточний урок</string>
<string name="pref_view_timetable_show_groups">Показувати групи поруч з темами</string> <string name="pref_view_timetable_show_groups">Показувати групи поруч з темами</string>
<string name="pref_view_timetable_show_gaps">Show empty tiles where there\'s no lesson</string>
<string name="pref_view_grade_statistics_list">Показувати діаграми в оцінках класу</string> <string name="pref_view_grade_statistics_list">Показувати діаграми в оцінках класу</string>
<string name="pref_view_subjects_without_grades">Показати предмети без оцінок</string> <string name="pref_view_subjects_without_grades">Показати предмети без оцінок</string>
<string name="pref_view_grade_color_scheme">Схема кольорів оцінок</string> <string name="pref_view_grade_color_scheme">Схема кольорів оцінок</string>
@ -738,6 +747,8 @@
<string name="pref_other_grade_modifier_minus">Вартість мінуса</string> <string name="pref_other_grade_modifier_minus">Вартість мінуса</string>
<string name="pref_other_fill_message_content">Відповісти з історією повідомлень</string> <string name="pref_other_fill_message_content">Відповісти з історією повідомлень</string>
<string name="pref_other_optional_arithmetic_average">Вилічити середню аритметичну, якщо оцінка немає вартості</string> <string name="pref_other_optional_arithmetic_average">Вилічити середню аритметичну, якщо оцінка немає вартості</string>
<string name="pref_other_incognito_mode">Анонімний режим</string>
<string name="pref_other_incognito_mode_summary">Не повідомляти про прочитання повідомлення</string>
<string name="pref_ads_support_category_name">Підтримка</string> <string name="pref_ads_support_category_name">Підтримка</string>
<string name="pref_ads_privacy_policy">Політика конфіденційності</string> <string name="pref_ads_privacy_policy">Політика конфіденційності</string>
<string name="pref_ads_agreements">Угоди</string> <string name="pref_ads_agreements">Угоди</string>
@ -814,8 +825,8 @@
<string name="auth_api_error">Авторизацію відхилено. Надані дані не збігаються із записами в кабінеті секретаря.</string> <string name="auth_api_error">Авторизацію відхилено. Надані дані не збігаються із записами в кабінеті секретаря.</string>
<string name="auth_invalid_error">Неправильний PESEL</string> <string name="auth_invalid_error">Неправильний PESEL</string>
<string name="auth_pesel">Число PESEL</string> <string name="auth_pesel">Число PESEL</string>
<string name="auth_button">Authorize</string> <string name="auth_button">Авторизовать</string>
<string name="auth_success">Authorization completed successfully</string> <string name="auth_success">Авторизація пройшла успішно</string>
<string name="auth_title">Авторизувати</string> <string name="auth_title">Авторизувати</string>
<string name="auth_description">Для роботи програми нам потрібно підтвердити вашу особу. Будь ласка, введіть число PESEL &lt;b&gt;%1$s&lt;/b&gt; студента в поле нижче</string> <string name="auth_description">Для роботи програми нам потрібно підтвердити вашу особу. Будь ласка, введіть число PESEL &lt;b&gt;%1$s&lt;/b&gt; студента в поле нижче</string>
<string name="auth_button_skip">Поки що пропустити</string> <string name="auth_button_skip">Поки що пропустити</string>

View File

@ -23,6 +23,7 @@
<string name="pref_default_timetable_show_whole_class">no</string> <string name="pref_default_timetable_show_whole_class">no</string>
<string name="pref_default_grade_sorting_mode">alphabetic</string> <string name="pref_default_grade_sorting_mode">alphabetic</string>
<bool name="pref_default_timetable_show_timers">false</bool> <bool name="pref_default_timetable_show_timers">false</bool>
<string name="pref_default_timetable_show_gaps">between</string>
<bool name="pref_default_subjects_without_grades">false</bool> <bool name="pref_default_subjects_without_grades">false</bool>
<bool name="pref_default_optional_arithmetic_average">false</bool> <bool name="pref_default_optional_arithmetic_average">false</bool>
<string name="pref_default_last_sync_date">0</string> <string name="pref_default_last_sync_date">0</string>
@ -38,4 +39,5 @@
</string-array> </string-array>
<bool name="pref_default_ads_enabled">false</bool> <bool name="pref_default_ads_enabled">false</bool>
<bool name="pref_default_ads_consent_data_processing">false</bool> <bool name="pref_default_ads_consent_data_processing">false</bool>
<bool name="pref_default_incognito_mode">false</bool>
</resources> </resources>

View File

@ -28,6 +28,7 @@
<string name="pref_key_timetable_show_whole_class">show_whole_class_plan</string> <string name="pref_key_timetable_show_whole_class">show_whole_class_plan</string>
<string name="pref_key_timetable_show_groups">show_groups_in_plan</string> <string name="pref_key_timetable_show_groups">show_groups_in_plan</string>
<string name="pref_key_timetable_show_timers">timetable_show_timers</string> <string name="pref_key_timetable_show_timers">timetable_show_timers</string>
<string name="pref_key_timetable_show_gaps">timetable_show_gaps</string>
<string name="pref_key_subjects_without_grades">subjects_without_grades</string> <string name="pref_key_subjects_without_grades">subjects_without_grades</string>
<string name="pref_key_optional_arithmetic_average">optional_arithmetic_average</string> <string name="pref_key_optional_arithmetic_average">optional_arithmetic_average</string>
<string name="pref_key_message_draft">message_draft</string> <string name="pref_key_message_draft">message_draft</string>
@ -39,5 +40,6 @@
<string name="pref_key_ads_privacy_policy">ads_privacy_policy</string> <string name="pref_key_ads_privacy_policy">ads_privacy_policy</string>
<string name="pref_key_ads_consent_data_processing">ads_consent_data_processing</string> <string name="pref_key_ads_consent_data_processing">ads_consent_data_processing</string>
<string name="pref_key_ads_over_eighteen">ads_over_eighteen</string> <string name="pref_key_ads_over_eighteen">ads_over_eighteen</string>
<string name="pref_key_incognito_moge">incognito_mode</string>
<string name="pref_key_menu_order">appearance_menu_order</string> <string name="pref_key_menu_order">appearance_menu_order</string>
</resources> </resources>

View File

@ -123,6 +123,17 @@
<item>all_year</item> <item>all_year</item>
</string-array> </string-array>
<string-array name="timetable_show_gaps_entries">
<item>Don\'t show</item>
<item>Only between lessons</item>
<item>Before and between lessons</item>
</string-array>
<string-array name="timetable_show_gaps_values" translatable="false">
<item>no_gaps</item>
<item>between</item>
<item>before_and_between</item>
</string-array>
<string-array name="dashboard_tile_entries"> <string-array name="dashboard_tile_entries">
<item>Lucky number</item> <item>Lucky number</item>
<item>Unread messages</item> <item>Unread messages</item>

View File

@ -186,6 +186,10 @@
<string name="timetable_notify_change_room">Change of room from %1$s to %2$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_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> <string name="timetable_notify_change_subject">Change of subject from %1$s to %2$s</string>
<plurals name="timetable_no_lesson">
<item quantity="one">No lesson</item>
<item quantity="other">No lessons</item>
</plurals>
<plurals name="timetable_notify_new_items_title"> <plurals name="timetable_notify_new_items_title">
<item quantity="one">Timetable change</item> <item quantity="one">Timetable change</item>
<item quantity="other">Timetable changes</item> <item quantity="other">Timetable changes</item>
@ -339,6 +343,8 @@
</plurals> </plurals>
<string name="message_messages_deleted">Messages deleted</string> <string name="message_messages_deleted">Messages deleted</string>
<string name="message_mailbox_chooser_title">Choose mailbox</string> <string name="message_mailbox_chooser_title">Choose mailbox</string>
<string name="message_incognito_mode_on">Incognito mode is on</string>
<string name="message_incognito_description">Thanks to incognito mode sender is not notified when you read the message</string>
<!--Note--> <!--Note-->
@ -688,6 +694,7 @@
<string name="pref_view_expand_grade">Grades expanding</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_timers">Mark current lesson</string>
<string name="pref_view_timetable_show_groups">Show groups next to subjects</string> <string name="pref_view_timetable_show_groups">Show groups next to subjects</string>
<string name="pref_view_timetable_show_gaps">Show empty tiles where there\'s no lesson</string>
<string name="pref_view_grade_statistics_list">Show chart list in class grades</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_subjects_without_grades">Show subjects without grades</string>
<string name="pref_view_grade_color_scheme">Grades color scheme</string> <string name="pref_view_grade_color_scheme">Grades color scheme</string>
@ -728,6 +735,9 @@
<string name="pref_other_grade_modifier_minus">Value of the minus</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_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_other_optional_arithmetic_average">Show arithmetic average when no weights provided</string>
<string name="pref_other_incognito_mode">Incognito mode</string>
<string name="pref_other_incognito_mode_summary">Do not inform about reading the message</string>
<string name="pref_ads_support_category_name">Support</string> <string name="pref_ads_support_category_name">Support</string>
<string name="pref_ads_privacy_policy">Privacy Policy</string> <string name="pref_ads_privacy_policy">Privacy Policy</string>
<string name="pref_ads_agreements">Agreements</string> <string name="pref_ads_agreements">Agreements</string>

View File

@ -3,15 +3,15 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:configure="io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetConfigureActivity" android:configure="io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetConfigureActivity"
android:initialLayout="@layout/widget_timetable" android:initialLayout="@layout/widget_timetable"
android:minWidth="250dp" android:minWidth="245dp"
android:minHeight="110dp" android:minHeight="102dp"
android:minResizeWidth="250dp" android:minResizeWidth="245dp"
android:minResizeHeight="110dp" android:minResizeHeight="102dp"
android:previewImage="@drawable/img_timetable_widget_preview" android:previewImage="@drawable/img_timetable_widget_preview"
android:previewLayout="@layout/widget_timetable_preview" android:previewLayout="@layout/widget_timetable_preview"
android:resizeMode="horizontal|vertical" android:resizeMode="horizontal|vertical"
android:targetCellWidth="3" android:targetCellWidth="3"
android:targetCellHeight="2" android:targetCellHeight="3"
android:updatePeriodMillis="3600000" android:updatePeriodMillis="3600000"
android:widgetCategory="home_screen" android:widgetCategory="home_screen"
tools:targetApi="s" /> tools:targetApi="s" />

View File

@ -53,5 +53,12 @@
app:key="@string/pref_key_fill_message_content" app:key="@string/pref_key_fill_message_content"
app:singleLineTitle="false" app:singleLineTitle="false"
app:title="@string/pref_other_fill_message_content" /> app:title="@string/pref_other_fill_message_content" />
<SwitchPreferenceCompat
app:defaultValue="@bool/pref_default_incognito_mode"
app:iconSpaceReserved="false"
app:key="@string/pref_key_incognito_moge"
app:singleLineTitle="false"
app:title="@string/pref_other_incognito_mode"
app:summary="@string/pref_other_incognito_mode_summary" />
</PreferenceCategory> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>

View File

@ -111,5 +111,13 @@
app:title="@string/pref_view_timetable_show_whole_class" app:title="@string/pref_view_timetable_show_whole_class"
app:useSimpleSummaryProvider="true" /> app:useSimpleSummaryProvider="true" />
--> -->
<ListPreference
app:defaultValue="@string/pref_default_timetable_show_gaps"
app:entries="@array/timetable_show_gaps_entries"
app:entryValues="@array/timetable_show_gaps_values"
app:iconSpaceReserved="false"
app:key="@string/pref_key_timetable_show_gaps"
app:title="@string/pref_view_timetable_show_gaps"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>

View File

@ -2,7 +2,8 @@ package io.github.wulkanowy
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher import org.junit.rules.TestWatcher
@ -10,17 +11,14 @@ import org.junit.runner.Description
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class MainCoroutineRule( class MainCoroutineRule(
private val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher() private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
) : TestWatcher() { ) : TestWatcher() {
override fun starting(description: Description?) { override fun starting(description: Description) {
super.starting(description)
Dispatchers.setMain(testDispatcher) Dispatchers.setMain(testDispatcher)
} }
override fun finished(description: Description?) { override fun finished(description: Description) {
super.finished(description)
Dispatchers.resetMain() Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
} }
} }

View File

@ -22,7 +22,8 @@ abstract class AbstractMigrationTest {
@get:Rule @get:Rule
val helper: MigrationTestHelper = MigrationTestHelper( val helper: MigrationTestHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(), InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java.canonicalName, AppDatabase::class.java,
listOf(Migration55()),
FrameworkSQLiteOpenHelperFactory() FrameworkSQLiteOpenHelperFactory()
) )

View File

@ -22,12 +22,12 @@ class Migration12Test : AbstractMigrationTest() {
fun twoNotRelatedStudents() { fun twoNotRelatedStudents() {
helper.createDatabase(dbName, 11).apply { helper.createDatabase(dbName, 11).apply {
// user 1 // user 1
createStudent(this, 1, true) createStudent(this, 1)
createSemester(this, 1, false, 5, 1) createSemester(this, 1, false, 5, 1)
createSemester(this, 1, true, 5, 2) createSemester(this, 1, true, 5, 2)
// user 2 // user 2
createStudent(this, 2, true) createStudent(this, 2)
createSemester(this, 2, false, 6, 1) createSemester(this, 2, false, 6, 1)
createSemester(this, 2, true, 6, 2) createSemester(this, 2, true, 6, 2)
close() close()
@ -56,9 +56,9 @@ class Migration12Test : AbstractMigrationTest() {
fun removeStudentsWithoutClassId() { fun removeStudentsWithoutClassId() {
helper.createDatabase(dbName, 11).apply { helper.createDatabase(dbName, 11).apply {
// user 1 // user 1
createStudent(this, 1, true) createStudent(this, 1)
createSemester(this, 1, false, 0, 2) createSemester(this, 1, false, 0, 2)
createStudent(this, 2, true) createStudent(this, 2)
createSemester(this, 2, true, 1, 2) createSemester(this, 2, true, 1, 2)
close() close()
} }
@ -81,11 +81,11 @@ class Migration12Test : AbstractMigrationTest() {
fun ensureThereIsOnlyOneCurrentStudent() { fun ensureThereIsOnlyOneCurrentStudent() {
helper.createDatabase(dbName, 11).apply { helper.createDatabase(dbName, 11).apply {
// user 1 // user 1
createStudent(this, 1, true) createStudent(this, 1)
createSemester(this, 1, true, 5, 2) createSemester(this, 1, true, 5, 2)
createStudent(this, 2, true) createStudent(this, 2)
createSemester(this, 2, true, 6, 2) createSemester(this, 2, true, 6, 2)
createStudent(this, 3, true) createStudent(this, 3)
createSemester(this, 3, false, 7, 2) createSemester(this, 3, false, 7, 2)
close() close()
} }
@ -112,7 +112,7 @@ class Migration12Test : AbstractMigrationTest() {
db.close() db.close()
} }
private fun createStudent(db: SupportSQLiteDatabase, studentId: Int, isCurrent: Boolean) { private fun createStudent(db: SupportSQLiteDatabase, studentId: Int) {
db.insert("Students", CONFLICT_FAIL, ContentValues().apply { db.insert("Students", CONFLICT_FAIL, ContentValues().apply {
put("endpoint", "https://fakelog.cf") put("endpoint", "https://fakelog.cf")
put("loginType", "STANDARD") put("loginType", "STANDARD")
@ -123,7 +123,7 @@ class Migration12Test : AbstractMigrationTest() {
put("student_name", "Jan Kowalski") put("student_name", "Jan Kowalski")
put("school_id", "000123") put("school_id", "000123")
put("school_name", "") put("school_name", "")
put("is_current", isCurrent) put("is_current", true)
put("registration_date", "0") put("registration_date", "0")
}) })
} }

View File

@ -95,22 +95,22 @@ class Migration13Test : AbstractMigrationTest() {
fun markAtLeastAndOnlyOneSemesterAtCurrent() { fun markAtLeastAndOnlyOneSemesterAtCurrent() {
helper.createDatabase(dbName, 12).apply { helper.createDatabase(dbName, 12).apply {
createStudent(this, 1, "", 5) createStudent(this, 1, "", 5)
createSemester(this, 1, 5, 1, 1, false) createSemester(this, 1, 1, 1, false)
createSemester(this, 1, 5, 2, 1, false) createSemester(this, 1, 2, 1, false)
createSemester(this, 1, 5, 3, 2, false) createSemester(this, 1, 3, 2, false)
createSemester(this, 1, 5, 4, 2, false) createSemester(this, 1, 4, 2, false)
createStudent(this, 2, "", 5) createStudent(this, 2, "", 5)
createSemester(this, 2, 5, 5, 5, true) createSemester(this, 2, 5, 5, true)
createSemester(this, 2, 5, 6, 5, true) createSemester(this, 2, 6, 5, true)
createSemester(this, 2, 5, 7, 55, true) createSemester(this, 2, 7, 55, true)
createSemester(this, 2, 5, 8, 55, true) createSemester(this, 2, 8, 55, true)
createStudent(this, 3, "", 5) createStudent(this, 3, "", 5)
createSemester(this, 3, 5, 11, 99, false) createSemester(this, 3, 11, 99, false)
createSemester(this, 3, 5, 12, 99, false) createSemester(this, 3, 12, 99, false)
createSemester(this, 3, 5, 13, 100, false) createSemester(this, 3, 13, 100, false)
createSemester(this, 3, 5, 14, 100, true) createSemester(this, 3, 14, 100, true)
close() close()
} }
@ -198,7 +198,13 @@ class Migration13Test : AbstractMigrationTest() {
}) })
} }
private fun createSemester(db: SupportSQLiteDatabase, studentId: Int, classId: Int, semesterId: Int, diaryId: Int, isCurrent: Boolean = false) { private fun createSemester(
db: SupportSQLiteDatabase,
studentId: Int,
semesterId: Int,
diaryId: Int,
isCurrent: Boolean = false
) {
db.insert("Semesters", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply { db.insert("Semesters", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply {
put("student_id", studentId) put("student_id", studentId)
put("diary_id", diaryId) put("diary_id", diaryId)
@ -206,7 +212,7 @@ class Migration13Test : AbstractMigrationTest() {
put("semester_id", semesterId) put("semester_id", semesterId)
put("semester_name", "1") put("semester_name", "1")
put("is_current", isCurrent) put("is_current", isCurrent)
put("class_id", classId) put("class_id", 5)
put("unit_id", "99") put("unit_id", "99")
}) })
} }

View File

@ -15,6 +15,7 @@ import io.mockk.impl.annotations.MockK
import io.mockk.impl.annotations.SpyK import io.mockk.impl.annotations.SpyK
import io.mockk.just import io.mockk.just
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals import org.junit.Assert.assertNotEquals
import org.junit.Before import org.junit.Before
@ -81,15 +82,15 @@ class SemesterRepositoryTest {
} }
@Test @Test
fun getSemesters_invalidDiary_scrapper() { fun getSemesters_invalidDiary_scrapper() = runTest {
val badSemesters = listOf( val badSemesters = listOf(
getSemesterPojo(0, 1, now().minusMonths(6), now().minusMonths(3)), getSemesterPojo(0, 2, now().minusMonths(6), now()),
getSemesterPojo(0, 2, now().minusMonths(3), now()) getSemesterPojo(0, 2, now(), now().plusMonths(6)),
) )
val goodSemesters = listOf( val goodSemesters = listOf(
getSemesterPojo(1, 1, now().minusMonths(6), now().minusMonths(3)), getSemesterPojo(1, 2, now().minusMonths(6), now()),
getSemesterPojo(1, 2, now().minusMonths(3), now()) getSemesterPojo(2, 3, now(), now().plusMonths(6)),
) )
coEvery { semesterDb.loadAll(student.studentId, student.classId) } returnsMany listOf( coEvery { semesterDb.loadAll(student.studentId, student.classId) } returnsMany listOf(
@ -101,7 +102,9 @@ class SemesterRepositoryTest {
coEvery { semesterDb.deleteAll(any()) } just Runs coEvery { semesterDb.deleteAll(any()) } just Runs
coEvery { semesterDb.insertSemesters(any()) } returns listOf() coEvery { semesterDb.insertSemesters(any()) } returns listOf()
val items = runBlocking { semesterRepository.getSemesters(student.copy(loginMode = Sdk.Mode.SCRAPPER.name)) } val items = semesterRepository.getSemesters(
student = student.copy(loginMode = Sdk.Mode.SCRAPPER.name)
)
assertEquals(2, items.size) assertEquals(2, items.size)
assertNotEquals(0, items[0].diaryId) assertNotEquals(0, items[0].diaryId)
} }
@ -188,15 +191,15 @@ class SemesterRepositoryTest {
} }
@Test @Test
fun getSemesters_doubleCurrent_refreshOnNoCurrent() { fun getSemesters_doubleCurrent_refreshOnNoCurrent() = runTest {
val semesters = listOf( val semesters = listOf(
getSemesterEntity(1, 1, now(), now()), getSemesterEntity(1, 1, now().minusMonths(1), now().plusMonths(1)),
getSemesterEntity(1, 2, now(), now()) getSemesterEntity(1, 2, now().minusMonths(1), now().plusMonths(1))
) )
coEvery { semesterDb.loadAll(student.studentId, student.classId) } returns semesters coEvery { semesterDb.loadAll(student.studentId, student.classId) } returns semesters
val items = runBlocking { semesterRepository.getSemesters(student, refreshOnNoCurrent = true) } val items = semesterRepository.getSemesters(student, refreshOnNoCurrent = true)
assertEquals(2, items.size) assertEquals(2, items.size)
} }

View File

@ -2,7 +2,9 @@ package io.github.wulkanowy.ui.modules.login.form
import io.github.wulkanowy.MainCoroutineRule import io.github.wulkanowy.MainCoroutineRule
import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.scrapper.Scrapper import io.github.wulkanowy.sdk.scrapper.Scrapper
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
@ -40,6 +42,12 @@ class LoginFormPresenterTest {
@MockK @MockK
lateinit var appInfo: AppInfo lateinit var appInfo: AppInfo
@MockK
lateinit var getAppropriateAdminMessageUseCase: GetAppropriateAdminMessageUseCase
@MockK
lateinit var preferencesRepository: PreferencesRepository
private lateinit var presenter: LoginFormPresenter private lateinit var presenter: LoginFormPresenter
private val registerUser = RegisterUser( private val registerUser = RegisterUser(
@ -72,6 +80,8 @@ class LoginFormPresenterTest {
loginErrorHandler = errorHandler, loginErrorHandler = errorHandler,
appInfo = appInfo, appInfo = appInfo,
analytics = analytics, analytics = analytics,
getAppropriateAdminMessageUseCase = getAppropriateAdminMessageUseCase,
preferencesRepository = preferencesRepository,
) )
presenter.onAttachView(loginFormView) presenter.onAttachView(loginFormView)
} }

View File

@ -8,6 +8,40 @@ import kotlin.test.assertEquals
class SemesterExtensionKtTest { class SemesterExtensionKtTest {
@Test
fun `check is first semester is current`() {
val first = getSemesterEntity(
semesterName = 1,
start = LocalDate.of(2023, 9, 1),
end = LocalDate.of(2024, 1, 31),
)
// first boundary - school-year start
assertEquals(false, first.isCurrent(LocalDate.of(2023, 8, 28)))
assertEquals(true, first.isCurrent(LocalDate.of(2023, 8, 29)))
// second boundary
assertEquals(true, first.isCurrent(LocalDate.of(2024, 1, 31)))
assertEquals(false, first.isCurrent(LocalDate.of(2024, 2, 1)))
}
@Test
fun `check is second semester is current`() {
val second = getSemesterEntity(
semesterName = 2,
start = LocalDate.of(2024, 2, 1),
end = LocalDate.of(2024, 9, 1),
)
// first boundary
assertEquals(false, second.isCurrent(LocalDate.of(2024, 1, 31)))
assertEquals(true, second.isCurrent(LocalDate.of(2024, 2, 1)))
// second boundary - school-year end
assertEquals(true, second.isCurrent(LocalDate.of(2024, 8, 29)))
assertEquals(false, second.isCurrent(LocalDate.of(2024, 8, 30)))
}
@Test(expected = IllegalArgumentException::class) @Test(expected = IllegalArgumentException::class)
fun `get current semester when current is doubled`() { fun `get current semester when current is doubled`() {
val semesters = listOf( val semesters = listOf(

View File

@ -1,8 +1,8 @@
buildscript { buildscript {
ext { ext {
kotlin_version = '1.8.21' kotlin_version = '1.9.0'
about_libraries = '10.7.0' about_libraries = '10.8.3'
hilt_version = "2.46.1" hilt_version = "2.47"
} }
repositories { repositories {
mavenCentral() mavenCentral()
@ -13,14 +13,15 @@ buildscript {
dependencies { dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath 'com.android.tools.build:gradle:8.0.2' classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$kotlin_version-1.0.11"
classpath 'com.android.tools.build:gradle:8.1.0'
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
classpath 'com.google.gms:google-services:4.3.15' classpath 'com.google.gms:google-services:4.3.15'
classpath 'com.huawei.agconnect:agcp:1.9.0.300' classpath 'com.huawei.agconnect:agcp:1.9.1.300'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.5' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.8'
classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "com.github.triplet.gradle:play-publisher:3.8.4"
classpath "ru.cian:huawei-publish-gradle-plugin:1.4.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.4.0"
classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.1.0.3113" classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.3.0.3225"
classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0" classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0"
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libraries" classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libraries"
} }
@ -36,6 +37,6 @@ allprojects {
} }
} }
task clean(type: Delete) { tasks.register('clean', Delete) {
delete rootProject.buildDir delete rootProject.buildDir
} }

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip
networkTimeout=10000 networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@ -1,4 +1,4 @@
plugins { plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.5.0' id 'org.gradle.toolchains.foojay-resolver-convention' version '0.7.0'
} }
include ':app' include ':app'