1
0

Compare commits

..

48 Commits

Author SHA1 Message Date
05c6c8bc96 Merge branch 'release/2.6.13' 2024-05-23 23:26:19 +02:00
ca9caeca66 Version 2.6.13 2024-05-23 23:26:06 +02:00
e971eb7821 Add full panic mode (#2569) 2024-05-23 23:15:47 +02:00
c7afa262d9 Merge branch 'release/2.6.12' into develop 2024-05-23 07:55:17 +02:00
1f0b400e5a Merge branch 'release/2.6.12' 2024-05-23 07:55:06 +02:00
b622c09e56 Version 2.6.12 2024-05-23 07:55:02 +02:00
87fb1916d8 Add panic button 2024-05-23 07:37:49 +02:00
718bbea329 Merge branch 'release/2.6.11' 2024-05-22 22:06:00 +02:00
5ca9ac3978 Version 2.6.11 2024-05-22 22:05:11 +02:00
c76ace40eb Fix mapping refresh key 2024-05-22 21:05:17 +02:00
4a585fc56e Fix mapping refresh key 2024-05-22 21:00:09 +02:00
f3afe7fdb7 Bump sdk to 2.6.10-SNAPSHOT 2024-05-22 20:31:40 +02:00
859c6ef154 Add auto refresh to wulkanowy repo 2024-05-22 20:23:57 +02:00
24abe47332 Version 2.6.10 2024-05-22 19:26:01 +02:00
52c1878f6b Bump sdk to 2.6.9-SNAPSHOT 2024-05-22 19:26:01 +02:00
8390ccad20 Merge branch 'release/2.6.10' 2024-05-22 09:05:35 +02:00
bf9f048116 Version 2.6.10 2024-05-22 09:05:30 +02:00
56bfabdf11 Bump sdk to 2.6.9-SNAPSHOT 2024-05-22 08:14:11 +02:00
4060240368 New Crowdin updates (#2551) 2024-05-17 22:22:43 +02:00
f69816fbac Bump coroutines from 1.8.0 to 1.8.1 (#2560) 2024-05-17 20:21:52 +00:00
1a41e9e3ee Merge branch 'release/2.6.9' into develop 2024-05-17 21:23:40 +02:00
6e2bcfbe02 Merge branch 'release/2.6.9' 2024-05-17 21:23:34 +02:00
1c0df6c145 Version 2.6.9 2024-05-17 21:23:29 +02:00
2b61e883c5 Bump sdk to 2.6.8-SNAPSHOT 2024-05-17 19:45:53 +02:00
31a7ae6d15 Fix tests 2024-05-17 08:04:28 +02:00
4b3b4a21fa Merge branch 'release/2.6.8' into develop 2024-05-17 07:39:16 +02:00
440cc7ae89 Merge branch 'release/2.6.8' 2024-05-17 07:38:46 +02:00
9a6b17c9d9 Version 2.6.8 2024-05-17 07:38:13 +02:00
729e0f547b Add sandbox isolate 2024-05-16 23:23:06 +02:00
faa8d34e79 Bump sdk to 2.6.7-SNAPSHOT 2024-05-16 18:55:02 +02:00
b6f5ac91ad Merge branch 'release/2.6.7' into develop 2024-05-16 08:50:37 +02:00
51dd343299 Merge branch 'release/2.6.7' 2024-05-16 08:50:33 +02:00
d1d0caa1e3 Version 2.6.7 2024-05-16 08:50:28 +02:00
c40779f48f Merge branch 'release/2.6.6' into develop 2024-05-14 22:31:01 +02:00
5ee7fee09d Merge branch 'release/2.6.6' 2024-05-14 22:30:57 +02:00
594d2dbec5 Version 2.6.6 2024-05-14 22:30:51 +02:00
26596e8254 Merge branch 'release/2.6.5' into develop 2024-05-12 17:51:07 +02:00
ff56348586 Merge branch 'release/2.6.5' 2024-05-12 17:51:02 +02:00
6e472d6c5c Version 2.6.5 2024-05-12 17:50:56 +02:00
6f4826249c Add remote mapping for Wulkanowy SDK (#2550)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
2024-05-12 16:21:28 +02:00
2f3e1b6aae Merge branch 'release/2.6.4' into develop 2024-05-10 12:35:39 +02:00
a69361708e Merge branch 'release/2.6.4' 2024-05-10 12:34:11 +02:00
567d868f76 Version 2.6.4 2024-05-10 12:30:30 +02:00
12030efee2 Version 2.6.3 2024-05-09 22:59:20 +02:00
3cbe98d7b8 Merge branch 'release/2.6.3' 2024-05-09 22:58:03 +02:00
503a97bc4c Version 2.6.3 2024-05-09 22:57:58 +02:00
04c382643d Fix a typo in settings - apperance & behaviour - menu configuration (#2547) 2024-05-09 18:17:10 +02:00
fc140ad9c1 Merge branch 'release/2.6.2' into develop 2024-05-08 01:55:42 +02:00
44 changed files with 682 additions and 213 deletions

1
.gitignore vendored
View File

@ -127,3 +127,4 @@ google-services.json
!app/google-services.json !app/google-services.json
.idea/appInsightsSettings.xml

View File

@ -27,8 +27,8 @@ android {
testApplicationId "io.github.tests.wulkanowy" testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 34 targetSdkVersion 34
versionCode 162 versionCode 173
versionName "2.6.2" versionName "2.6.13"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "app_name", "Wulkanowy" resValue "string", "app_name", "Wulkanowy"
@ -160,8 +160,8 @@ play {
defaultToAppBundles = false defaultToAppBundles = false
track = 'production' track = 'production'
releaseStatus = ReleaseStatus.IN_PROGRESS releaseStatus = ReleaseStatus.IN_PROGRESS
userFraction = 0.99d userFraction = 0.1d
updatePriority = 5 updatePriority = 2
enabled.set(false) enabled.set(false)
} }
@ -187,16 +187,17 @@ ext {
room = "2.6.1" room = "2.6.1"
chucker = "4.0.0" chucker = "4.0.0"
mockk = "1.13.10" mockk = "1.13.10"
coroutines = "1.8.0" coroutines = "1.8.1"
} }
dependencies { dependencies {
implementation 'io.github.wulkanowy:sdk:2.6.1' implementation 'io.github.wulkanowy:sdk:2.6.11'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:$coroutines"
implementation 'androidx.core:core-ktx:1.13.1' implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.core:core-splashscreen:1.0.1' implementation 'androidx.core:core-splashscreen:1.0.1'
@ -204,6 +205,7 @@ dependencies {
implementation "androidx.appcompat:appcompat:1.6.1" implementation "androidx.appcompat:appcompat:1.6.1"
implementation "androidx.fragment:fragment-ktx:1.7.0" implementation "androidx.fragment:fragment-ktx:1.7.0"
implementation "androidx.annotation:annotation:1.7.1" implementation "androidx.annotation:annotation:1.7.1"
implementation "androidx.javascriptengine:javascriptengine:1.0.0-beta01"
implementation "androidx.preference:preference-ktx:1.2.1" implementation "androidx.preference:preference-ktx:1.2.1"
implementation "androidx.recyclerview:recyclerview:1.3.2" implementation "androidx.recyclerview:recyclerview:1.3.2"

View File

@ -3,6 +3,8 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:installLocation="internalOnly"> android:installLocation="internalOnly">
<uses-sdk tools:overrideLibrary="androidx.javascriptengine" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
@ -42,9 +44,9 @@
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:resizeableActivity="true"
android:supportsRtl="false" android:supportsRtl="false"
android:theme="@style/WulkanowyTheme" android:theme="@style/WulkanowyTheme"
android:resizeableActivity="true"
tools:ignore="DataExtractionRules,UnusedAttribute"> tools:ignore="DataExtractionRules,UnusedAttribute">
<activity <activity
android:name=".ui.modules.splash.SplashActivity" android:name=".ui.modules.splash.SplashActivity"

View File

@ -13,8 +13,8 @@ import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import io.github.wulkanowy.data.api.AdminMessageService import io.github.wulkanowy.data.api.services.SchoolsService
import io.github.wulkanowy.data.api.SchoolsService import io.github.wulkanowy.data.api.services.WulkanowyService
import io.github.wulkanowy.data.db.AppDatabase import io.github.wulkanowy.data.db.AppDatabase
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.PreferencesRepository
@ -71,7 +71,7 @@ internal class DataModule {
okHttpClient: OkHttpClient, okHttpClient: OkHttpClient,
json: Json, json: Json,
appInfo: AppInfo appInfo: AppInfo
): AdminMessageService = Retrofit.Builder() ): WulkanowyService = Retrofit.Builder()
.baseUrl(appInfo.messagesBaseUrl) .baseUrl(appInfo.messagesBaseUrl)
.client(okHttpClient) .client(okHttpClient)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType())) .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))

View File

@ -1,13 +1,21 @@
package io.github.wulkanowy.data package io.github.wulkanowy.data
import android.content.Context
import android.os.Build
import androidx.javascriptengine.JavaScriptSandbox
import com.chuckerteam.chucker.api.ChuckerInterceptor import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.google.common.util.concurrent.ListenableFuture
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.db.dao.StudentDao import io.github.wulkanowy.data.db.dao.StudentDao
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.StudentIsEduOne import io.github.wulkanowy.data.db.entities.StudentIsEduOne
import io.github.wulkanowy.data.repositories.WulkanowyRepository
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.scrapper.EvaluateHandler
import io.github.wulkanowy.utils.RemoteConfigHelper import io.github.wulkanowy.utils.RemoteConfigHelper
import io.github.wulkanowy.utils.WebkitCookieManagerProxy import io.github.wulkanowy.utils.WebkitCookieManagerProxy
import kotlinx.coroutines.guava.await
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import timber.log.Timber import timber.log.Timber
@ -16,18 +24,24 @@ import javax.inject.Singleton
@Singleton @Singleton
class WulkanowySdkFactory @Inject constructor( class WulkanowySdkFactory @Inject constructor(
@ApplicationContext private val context: Context,
private val chuckerInterceptor: ChuckerInterceptor, private val chuckerInterceptor: ChuckerInterceptor,
private val remoteConfig: RemoteConfigHelper, private val remoteConfig: RemoteConfigHelper,
private val webkitCookieManagerProxy: WebkitCookieManagerProxy, private val webkitCookieManagerProxy: WebkitCookieManagerProxy,
private val studentDb: StudentDao, private val studentDb: StudentDao,
private val wulkanowyRepository: WulkanowyRepository,
) { ) {
private val eduOneMutex = Mutex() private val eduOneMutex = Mutex()
private val migrationFailedStudentIds = mutableSetOf<Long>() private val migrationFailedStudentIds = mutableSetOf<Long>()
private val sandbox: ListenableFuture<JavaScriptSandbox>? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && JavaScriptSandbox.isSupported())
JavaScriptSandbox.createConnectedInstanceAsync(context)
else null
private val sdk = Sdk().apply { private val sdk = Sdk().apply {
androidVersion = android.os.Build.VERSION.RELEASE androidVersion = Build.VERSION.RELEASE
buildTag = android.os.Build.MODEL buildTag = Build.MODEL
userAgentTemplate = remoteConfig.userAgentTemplate userAgentTemplate = remoteConfig.userAgentTemplate
setSimpleHttpLogger { Timber.d(it) } setSimpleHttpLogger { Timber.d(it) }
setAdditionalCookieManager(webkitCookieManagerProxy) setAdditionalCookieManager(webkitCookieManagerProxy)
@ -36,14 +50,46 @@ class WulkanowySdkFactory @Inject constructor(
addInterceptor(chuckerInterceptor, network = true) addInterceptor(chuckerInterceptor, network = true)
} }
fun create() = sdk fun createBase() = sdk
suspend fun create(): Sdk {
val mapping = wulkanowyRepository.getMapping()
return createBase().apply {
if (mapping != null) {
endpointsMapping = mapping.endpoints
vTokenMapping = mapping.vTokens
vHeaders = mapping.vHeaders
vParamsEvaluation = createIsolate()
}
}
}
private suspend fun createIsolate(): suspend () -> EvaluateHandler {
return {
val isolate = sandbox?.await()?.createIsolate()
object : EvaluateHandler {
override suspend fun evaluate(code: String): String? {
return isolate?.evaluateJavaScriptAsync(code)?.await()
}
override fun close() {
isolate?.close()
}
}
}
}
suspend fun create(student: Student, semester: Semester? = null): Sdk { suspend fun create(student: Student, semester: Semester? = null): Sdk {
val overrideIsEduOne = checkEduOneAndMigrateIfNecessary(student) val overrideIsEduOne = checkEduOneAndMigrateIfNecessary(student)
return buildSdk(student, semester, overrideIsEduOne) return buildSdk(student, semester, overrideIsEduOne)
} }
private fun buildSdk(student: Student, semester: Semester?, isStudentEduOne: Boolean): Sdk { private suspend fun buildSdk(
student: Student,
semester: Semester?,
isStudentEduOne: Boolean
): Sdk {
return create().apply { return create().apply {
email = student.email email = student.email
password = student.password password = student.password

View File

@ -0,0 +1,20 @@
package io.github.wulkanowy.data.api.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Mapping(
@SerialName("endpoints")
val endpoints: Map<String, Map<String, Map<String, String>>>,
@SerialName("vTokens")
val vTokens: Map<String, Map<String, Map<String, String>>>,
@SerialName("vTokenScheme")
val vTokenScheme: Map<String, Map<String, String>> = emptyMap(),
@SerialName("vHeaders")
val vHeaders: Map<String, Map<String, Map<String, String>>> = emptyMap(),
)

View File

@ -1,4 +1,4 @@
package io.github.wulkanowy.data.api package io.github.wulkanowy.data.api.services
import io.github.wulkanowy.data.pojos.IntegrityRequest import io.github.wulkanowy.data.pojos.IntegrityRequest
import io.github.wulkanowy.data.pojos.LoginEvent import io.github.wulkanowy.data.pojos.LoginEvent

View File

@ -1,12 +1,16 @@
package io.github.wulkanowy.data.api package io.github.wulkanowy.data.api.services
import io.github.wulkanowy.data.api.models.Mapping
import io.github.wulkanowy.data.db.entities.AdminMessage import io.github.wulkanowy.data.db.entities.AdminMessage
import retrofit2.http.GET import retrofit2.http.GET
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
interface AdminMessageService { interface WulkanowyService {
@GET("/v1.json") @GET("/v1.json")
suspend fun getAdminMessages(): List<AdminMessage> suspend fun getAdminMessages(): List<AdminMessage>
}
@GET("/mapping2.json")
suspend fun getMapping(): Mapping
}

View File

@ -1,34 +0,0 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.api.AdminMessageService
import io.github.wulkanowy.data.db.dao.AdminMessageDao
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.networkBoundResource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AdminMessageRepository @Inject constructor(
private val adminMessageService: AdminMessageService,
private val adminMessageDao: AdminMessageDao,
) {
private val saveFetchResultMutex = Mutex()
fun getAdminMessages(): Flow<Resource<List<AdminMessage>>> =
networkBoundResource(
mutex = saveFetchResultMutex,
isResultEmpty = { false },
query = { adminMessageDao.loadAll() },
fetch = { adminMessageService.getAdminMessages() },
shouldFetch = { true },
saveFetchResult = { oldItems, newItems ->
adminMessageDao.removeOldAndSaveNew(oldItems, newItems)
},
)
.filterNot { it is Resource.Intermediate }
}

View File

@ -9,6 +9,7 @@ import com.fredporciuncula.flow.preferences.Preference
import com.fredporciuncula.flow.preferences.Serializer import com.fredporciuncula.flow.preferences.Serializer
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.api.models.Mapping
import io.github.wulkanowy.data.enums.AppTheme import io.github.wulkanowy.data.enums.AppTheme
import io.github.wulkanowy.data.enums.AttendanceCalculatorSortingMode import io.github.wulkanowy.data.enums.AttendanceCalculatorSortingMode
import io.github.wulkanowy.data.enums.GradeColorTheme import io.github.wulkanowy.data.enums.GradeColorTheme
@ -375,6 +376,15 @@ class PreferencesRepository @Inject constructor(
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) }
var mapping: Mapping?
get() {
val value = sharedPref.getString("mapping", null)
return value?.let { json.decodeFromString(it) }
}
set(value) = sharedPref.edit(commit = true) {
putString("mapping", value?.let { json.encodeToString(it) })
}
init { init {
if (installationId.isEmpty()) { if (installationId.isEmpty()) {
installationId = UUID.randomUUID().toString() installationId = UUID.randomUUID().toString()

View File

@ -1,7 +1,7 @@
package io.github.wulkanowy.data.repositories package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.WulkanowySdkFactory import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.api.SchoolsService import io.github.wulkanowy.data.api.services.SchoolsService
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.StudentWithSemesters import io.github.wulkanowy.data.db.entities.StudentWithSemesters

View File

@ -0,0 +1,66 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.api.models.Mapping
import io.github.wulkanowy.data.api.services.WulkanowyService
import io.github.wulkanowy.data.db.dao.AdminMessageDao
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.sync.Mutex
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class WulkanowyRepository @Inject constructor(
private val wulkanowyService: WulkanowyService,
private val adminMessageDao: AdminMessageDao,
private val preferencesRepository: PreferencesRepository,
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "mapping_refresh_key"
fun getAdminMessages(): Flow<Resource<List<AdminMessage>>> =
networkBoundResource(
mutex = saveFetchResultMutex,
isResultEmpty = { false },
query = { adminMessageDao.loadAll() },
fetch = { wulkanowyService.getAdminMessages() },
shouldFetch = { true },
saveFetchResult = { oldItems, newItems ->
adminMessageDao.removeOldAndSaveNew(oldItems, newItems)
},
)
.filterNot { it is Resource.Intermediate }
suspend fun getMapping(): Mapping? {
var savedMapping = preferencesRepository.mapping
val isExpired = refreshHelper.shouldBeRefreshed(
key = getRefreshKey(cacheKey)
)
if (savedMapping == null || isExpired) {
fetchMapping()
savedMapping = preferencesRepository.mapping
}
return savedMapping
}
suspend fun fetchMapping() {
runCatching { wulkanowyService.getMapping() }
.onFailure { Timber.e(it) }
.onSuccess {
preferencesRepository.mapping = it
refreshHelper.updateLastRefreshTimestamp(cacheKey)
}
}
}

View File

@ -5,14 +5,14 @@ import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageType import io.github.wulkanowy.data.enums.MessageType
import io.github.wulkanowy.data.mapResourceData import io.github.wulkanowy.data.mapResourceData
import io.github.wulkanowy.data.repositories.AdminMessageRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.WulkanowyRepository
import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.AppInfo
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import javax.inject.Inject import javax.inject.Inject
class GetAppropriateAdminMessageUseCase @Inject constructor( class GetAppropriateAdminMessageUseCase @Inject constructor(
private val adminMessageRepository: AdminMessageRepository, private val wulkanowyRepository: WulkanowyRepository,
private val preferencesRepository: PreferencesRepository, private val preferencesRepository: PreferencesRepository,
private val appInfo: AppInfo private val appInfo: AppInfo
) { ) {
@ -22,7 +22,7 @@ class GetAppropriateAdminMessageUseCase @Inject constructor(
} }
operator fun invoke(scrapperBaseUrl: String, type: MessageType): Flow<Resource<AdminMessage?>> { operator fun invoke(scrapperBaseUrl: String, type: MessageType): Flow<Resource<AdminMessage?>> {
return adminMessageRepository.getAdminMessages().mapResourceData { adminMessages -> return wulkanowyRepository.getAdminMessages().mapResourceData { adminMessages ->
adminMessages adminMessages
.asSequence() .asSequence()
.filter { it.isNotDismissed() } .filter { it.isNotDismissed() }

View File

@ -59,7 +59,7 @@ class GetMailboxByStudentUseCase @Inject constructor(
private fun String.getUnauthorizedVersion(): String { private fun String.getUnauthorizedVersion(): String {
return normalizeStudentName().split(" ") return normalizeStudentName().split(" ")
.joinToString(" ") { .joinToString(" ") {
it.first() + "*".repeat(it.length - 1) it.firstOrNull()?.toString().orEmpty() + "*".repeat((it.length - 1).coerceAtLeast(0))
} }
} }
} }

View File

@ -59,7 +59,7 @@ class CaptchaDialog : BaseDialogFragment<DialogCaptchaBinding>() {
webView = this webView = this
with(settings) { with(settings) {
javaScriptEnabled = true javaScriptEnabled = true
userAgentString = wulkanowySdkFactory.create().userAgent userAgentString = wulkanowySdkFactory.createBase().userAgent
} }
webViewClient = object : WebViewClient() { webViewClient = object : WebViewClient() {

View File

@ -30,6 +30,7 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.message.MessageFragment import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.notificationscenter.NotificationsCenterFragment import io.github.wulkanowy.ui.modules.notificationscenter.NotificationsCenterFragment
import io.github.wulkanowy.ui.modules.panicmode.PanicModeFragment
import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
import io.github.wulkanowy.utils.capitalise import io.github.wulkanowy.utils.capitalise
@ -125,6 +126,7 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
mainActivity.pushView(ConferenceFragment.newInstance()) mainActivity.pushView(ConferenceFragment.newInstance())
} }
onAdminMessageClickListener = presenter::onAdminMessageSelected onAdminMessageClickListener = presenter::onAdminMessageSelected
onPanicButtonClickListener = presenter::onPanicButtonClicked
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed
registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
@ -208,7 +210,11 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
binding = binding.dashboardErrorAdminMessage, binding = binding.dashboardErrorAdminMessage,
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed, onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
onAdminMessageClickListener = presenter::onAdminMessageSelected, onAdminMessageClickListener = presenter::onAdminMessageSelected,
).bind(adminMessageItem.adminMessage) onPanicButtonClickListener = presenter::onPanicButtonClicked,
).bind(
item = adminMessageItem.adminMessage,
showPanicButton = true,
)
} }
} }
@ -236,6 +242,10 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
requireContext().openInternetBrowser(url) requireContext().openInternetBrowser(url)
} }
override fun openPanicWebView(url: String) {
(requireActivity() as MainActivity).pushView(PanicModeFragment.newInstance(url))
}
override fun onDestroyView() { override fun onDestroyView() {
dashboardAdapter.clearTimers() dashboardAdapter.clearTimers()
presenter.onDetachView() presenter.onDetachView()

View File

@ -11,6 +11,7 @@ import io.github.wulkanowy.data.errorOrNull
import io.github.wulkanowy.data.flatResourceFlow import io.github.wulkanowy.data.flatResourceFlow
import io.github.wulkanowy.data.mapResourceData import io.github.wulkanowy.data.mapResourceData
import io.github.wulkanowy.data.onResourceError import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.repositories.AttendanceSummaryRepository import io.github.wulkanowy.data.repositories.AttendanceSummaryRepository
import io.github.wulkanowy.data.repositories.ConferenceRepository import io.github.wulkanowy.data.repositories.ConferenceRepository
import io.github.wulkanowy.data.repositories.ExamRepository import io.github.wulkanowy.data.repositories.ExamRepository
@ -23,6 +24,7 @@ import io.github.wulkanowy.data.repositories.SchoolAnnouncementRepository
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.data.resourceFlow
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
import io.github.wulkanowy.domain.timetable.IsStudentHasLessonsOnWeekendUseCase import io.github.wulkanowy.domain.timetable.IsStudentHasLessonsOnWeekendUseCase
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
@ -44,6 +46,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.HttpUrl.Companion.toHttpUrl
import timber.log.Timber import timber.log.Timber
import java.time.Instant import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
@ -282,6 +285,22 @@ class DashboardPresenter @Inject constructor(
url?.let { view?.openInternetBrowser(it) } url?.let { view?.openInternetBrowser(it) }
} }
fun onPanicButtonClicked() {
resourceFlow { studentRepository.getCurrentStudent() }
.onResourceError { errorHandler.dispatch(it) }
.onResourceSuccess {
val baseUrl = it.scrapperBaseUrl.toHttpUrl()
val urlToOpen = baseUrl.newBuilder()
.host("uonetplus${it.scrapperDomainSuffix}.${baseUrl.host}")
.addPathSegment(it.symbol)
.build()
.toString()
view?.openPanicWebView(urlToOpen)
}
.launch("panic_button")
}
private fun loadHorizontalGroup(student: Student, forceRefresh: Boolean) { private fun loadHorizontalGroup(student: Student, forceRefresh: Boolean) {
flow { flow {
val selectedTiles = selectedDashboardTiles val selectedTiles = selectedDashboardTiles

View File

@ -31,4 +31,6 @@ interface DashboardView : BaseView {
fun openNotificationsCenterView() fun openNotificationsCenterView()
fun openInternetBrowser(url: String) fun openInternetBrowser(url: String)
fun openPanicWebView(url: String)
} }

View File

@ -59,6 +59,8 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
var onAdminMessageClickListener: (String?) -> Unit = {} var onAdminMessageClickListener: (String?) -> Unit = {}
var onPanicButtonClickListener: () -> Unit = {}
var onAdminMessageDismissClickListener: (AdminMessage) -> Unit = {} var onAdminMessageDismissClickListener: (AdminMessage) -> Unit = {}
val items = mutableListOf<DashboardItem>() val items = mutableListOf<DashboardItem>()
@ -86,35 +88,46 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
DashboardItem.Type.ACCOUNT.ordinal -> AccountViewHolder( DashboardItem.Type.ACCOUNT.ordinal -> AccountViewHolder(
ItemDashboardAccountBinding.inflate(inflater, parent, false) ItemDashboardAccountBinding.inflate(inflater, parent, false)
) )
DashboardItem.Type.HORIZONTAL_GROUP.ordinal -> HorizontalGroupViewHolder( DashboardItem.Type.HORIZONTAL_GROUP.ordinal -> HorizontalGroupViewHolder(
ItemDashboardHorizontalGroupBinding.inflate(inflater, parent, false) ItemDashboardHorizontalGroupBinding.inflate(inflater, parent, false)
) )
DashboardItem.Type.GRADES.ordinal -> GradesViewHolder( DashboardItem.Type.GRADES.ordinal -> GradesViewHolder(
ItemDashboardGradesBinding.inflate(inflater, parent, false) ItemDashboardGradesBinding.inflate(inflater, parent, false)
) )
DashboardItem.Type.LESSONS.ordinal -> LessonsViewHolder( DashboardItem.Type.LESSONS.ordinal -> LessonsViewHolder(
ItemDashboardLessonsBinding.inflate(inflater, parent, false) ItemDashboardLessonsBinding.inflate(inflater, parent, false)
) )
DashboardItem.Type.HOMEWORK.ordinal -> HomeworkViewHolder( DashboardItem.Type.HOMEWORK.ordinal -> HomeworkViewHolder(
ItemDashboardHomeworkBinding.inflate(inflater, parent, false) ItemDashboardHomeworkBinding.inflate(inflater, parent, false)
) )
DashboardItem.Type.ANNOUNCEMENTS.ordinal -> AnnouncementsViewHolder( DashboardItem.Type.ANNOUNCEMENTS.ordinal -> AnnouncementsViewHolder(
ItemDashboardAnnouncementsBinding.inflate(inflater, parent, false) ItemDashboardAnnouncementsBinding.inflate(inflater, parent, false)
) )
DashboardItem.Type.EXAMS.ordinal -> ExamsViewHolder( DashboardItem.Type.EXAMS.ordinal -> ExamsViewHolder(
ItemDashboardExamsBinding.inflate(inflater, parent, false) ItemDashboardExamsBinding.inflate(inflater, parent, false)
) )
DashboardItem.Type.CONFERENCES.ordinal -> ConferencesViewHolder( DashboardItem.Type.CONFERENCES.ordinal -> ConferencesViewHolder(
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, onAdminMessageDismissClickListener = onAdminMessageDismissClickListener,
onAdminMessageClickListener = onAdminMessageClickListener, onAdminMessageClickListener = onAdminMessageClickListener,
onPanicButtonClickListener = onPanicButtonClickListener,
) )
DashboardItem.Type.ADS.ordinal -> AdsViewHolder( DashboardItem.Type.ADS.ordinal -> AdsViewHolder(
ItemDashboardAdsBinding.inflate(inflater, parent, false) ItemDashboardAdsBinding.inflate(inflater, parent, false)
) )
else -> throw IllegalArgumentException() else -> throw IllegalArgumentException()
} }
} }
@ -129,7 +142,11 @@ 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 -> holder.bind((items[position] as DashboardItem.AdminMessages).adminMessage) is AdminMessageViewHolder -> holder.bind(
(items[position] as DashboardItem.AdminMessages).adminMessage,
showPanicButton = true
)
is AdsViewHolder -> bindAdsViewHolder(holder, position) is AdsViewHolder -> bindAdsViewHolder(holder, position)
} }
} }
@ -240,12 +257,15 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
attendancePercentage == null || attendancePercentage == .0 -> { attendancePercentage == null || attendancePercentage == .0 -> {
root.context.getThemeAttrColor(R.attr.colorOnSurface) root.context.getThemeAttrColor(R.attr.colorOnSurface)
} }
attendancePercentage <= ATTENDANCE_SECOND_WARNING_THRESHOLD -> { attendancePercentage <= ATTENDANCE_SECOND_WARNING_THRESHOLD -> {
root.context.getThemeAttrColor(R.attr.colorPrimary) root.context.getThemeAttrColor(R.attr.colorPrimary)
} }
attendancePercentage <= ATTENDANCE_FIRST_WARNING_THRESHOLD -> { attendancePercentage <= ATTENDANCE_FIRST_WARNING_THRESHOLD -> {
root.context.getThemeAttrColor(R.attr.colorTimetableChange) root.context.getThemeAttrColor(R.attr.colorTimetableChange)
} }
else -> root.context.getThemeAttrColor(R.attr.colorOnSurface) else -> root.context.getThemeAttrColor(R.attr.colorOnSurface)
} }
val attendanceString = if (attendancePercentage == null || attendancePercentage == .0) { val attendanceString = if (attendancePercentage == null || attendancePercentage == .0) {
@ -336,24 +356,28 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
binding.dashboardLessonsItemTitleTomorrow.isVisible = false binding.dashboardLessonsItemTitleTomorrow.isVisible = false
binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false
} }
tomorrowTimetable.isNotEmpty() -> { tomorrowTimetable.isNotEmpty() -> {
dateToNavigate = tomorrowDate dateToNavigate = tomorrowDate
updateLessonView(item, tomorrowTimetable, binding) updateLessonView(item, tomorrowTimetable, binding)
binding.dashboardLessonsItemTitleTomorrow.isVisible = true binding.dashboardLessonsItemTitleTomorrow.isVisible = true
binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false
} }
currentDayHeader != null && currentDayHeader.content.isNotBlank() -> { currentDayHeader != null && currentDayHeader.content.isNotBlank() -> {
dateToNavigate = currentDate dateToNavigate = currentDate
updateLessonView(item, emptyList(), binding, currentDayHeader) updateLessonView(item, emptyList(), binding, currentDayHeader)
binding.dashboardLessonsItemTitleTomorrow.isVisible = false binding.dashboardLessonsItemTitleTomorrow.isVisible = false
binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false
} }
tomorrowDayHeader != null && tomorrowDayHeader.content.isNotBlank() -> { tomorrowDayHeader != null && tomorrowDayHeader.content.isNotBlank() -> {
dateToNavigate = tomorrowDate dateToNavigate = tomorrowDate
updateLessonView(item, emptyList(), binding, tomorrowDayHeader) updateLessonView(item, emptyList(), binding, tomorrowDayHeader)
binding.dashboardLessonsItemTitleTomorrow.isVisible = true binding.dashboardLessonsItemTitleTomorrow.isVisible = true
binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false
} }
else -> { else -> {
dateToNavigate = currentDate dateToNavigate = currentDate
updateLessonView(item, emptyList(), binding) updateLessonView(item, emptyList(), binding)
@ -461,6 +485,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
firstTitleText = firstTitleText =
context.getString(R.string.dashboard_timetable_first_lesson_title_moment) context.getString(R.string.dashboard_timetable_first_lesson_title_moment)
} }
minutesToStartLesson < 240 -> { minutesToStartLesson < 240 -> {
firstTitleAndValueTextColor = firstTitleAndValueTextColor =
context.getThemeAttrColor(R.attr.colorOnSurface) context.getThemeAttrColor(R.attr.colorOnSurface)
@ -468,6 +493,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
firstTitleText = firstTitleText =
context.getString(R.string.dashboard_timetable_first_lesson_title_soon) context.getString(R.string.dashboard_timetable_first_lesson_title_soon)
} }
else -> { else -> {
firstTitleAndValueTextColor = firstTitleAndValueTextColor =
context.getThemeAttrColor(R.attr.colorOnSurface) context.getThemeAttrColor(R.attr.colorOnSurface)

View File

@ -13,9 +13,10 @@ class AdminMessageViewHolder(
private val binding: ItemDashboardAdminMessageBinding, private val binding: ItemDashboardAdminMessageBinding,
private val onAdminMessageDismissClickListener: (AdminMessage) -> Unit, private val onAdminMessageDismissClickListener: (AdminMessage) -> Unit,
private val onAdminMessageClickListener: (String?) -> Unit, private val onAdminMessageClickListener: (String?) -> Unit,
private val onPanicButtonClickListener: () -> Unit,
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: AdminMessage?) { fun bind(item: AdminMessage?, showPanicButton: Boolean = false) {
item ?: return item ?: return
val context = binding.root.context val context = binding.root.context
@ -48,10 +49,14 @@ class AdminMessageViewHolder(
dashboardAdminMessageItemClose.setOnClickListener { dashboardAdminMessageItemClose.setOnClickListener {
onAdminMessageDismissClickListener(item) onAdminMessageDismissClickListener(item)
} }
dashboardPanicSection.root.isVisible = showPanicButton
dashboardPanicSection.dashboardPanicButton.setOnClickListener {
onPanicButtonClickListener()
}
root.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) }) dashboardAdminMessage.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) })
item.destinationUrl?.let { url -> item.destinationUrl?.let { url ->
root.setOnClickListener { onAdminMessageClickListener(url) } dashboardAdminMessage.setOnClickListener { onAdminMessageClickListener(url) }
} }
} }
} }

View File

@ -118,5 +118,6 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
inAppUpdateHelper.onResume() inAppUpdateHelper.onResume()
presenter.updateSdkMappings()
} }
} }

View File

@ -1,12 +1,15 @@
package io.github.wulkanowy.ui.modules.login package io.github.wulkanowy.ui.modules.login
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.WulkanowyRepository
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 kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class LoginPresenter @Inject constructor( class LoginPresenter @Inject constructor(
private val wulkanowyRepository: WulkanowyRepository,
errorHandler: ErrorHandler, errorHandler: ErrorHandler,
studentRepository: StudentRepository studentRepository: StudentRepository
) : BasePresenter<LoginView>(errorHandler, studentRepository) { ) : BasePresenter<LoginView>(errorHandler, studentRepository) {
@ -16,4 +19,11 @@ class LoginPresenter @Inject constructor(
view.initView() view.initView()
Timber.i("Login view was initialized") Timber.i("Login view was initialized")
} }
fun updateSdkMappings() {
presenterScope.launch {
runCatching { wulkanowyRepository.fetchMapping() }
.onFailure { Timber.e(it) }
}
}
} }

View File

@ -238,6 +238,7 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
binding = binding.loginFormMessage, binding = binding.loginFormMessage,
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed, onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
onAdminMessageClickListener = presenter::onAdminMessageSelected, onAdminMessageClickListener = presenter::onAdminMessageSelected,
onPanicButtonClickListener = {},
).bind(message) ).bind(message)
binding.loginFormMessage.root.isVisible = message != null binding.loginFormMessage.root.isVisible = message != null
} }

View File

@ -118,6 +118,7 @@ class LoginStudentSelectFragment :
binding = binding.loginStudentSelectAdminMessage, binding = binding.loginStudentSelectAdminMessage,
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed, onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
onAdminMessageClickListener = presenter::onAdminMessageSelected, onAdminMessageClickListener = presenter::onAdminMessageSelected,
onPanicButtonClickListener = {},
).bind(adminMessage) ).bind(adminMessage)
binding.loginStudentSelectAdminMessage.root.isVisible = adminMessage != null binding.loginStudentSelectAdminMessage.root.isVisible = adminMessage != null
} }

View File

@ -188,6 +188,7 @@ class LoginSymbolFragment :
binding = binding.loginSymbolAdminMessage, binding = binding.loginSymbolAdminMessage,
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed, onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
onAdminMessageClickListener = presenter::onAdminMessageSelected, onAdminMessageClickListener = presenter::onAdminMessageSelected,
onPanicButtonClickListener = {},
).bind(adminMessage) ).bind(adminMessage)
binding.loginSymbolAdminMessage.root.isVisible = adminMessage != null binding.loginSymbolAdminMessage.root.isVisible = adminMessage != null
} }

View File

@ -138,6 +138,7 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
inAppUpdateHelper.onResume() inAppUpdateHelper.onResume()
presenter.updateSdkMappings()
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {

View File

@ -6,6 +6,7 @@ import io.github.wulkanowy.data.onResourceError
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.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.WulkanowyRepository
import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.services.sync.SyncManager import io.github.wulkanowy.services.sync.SyncManager
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
@ -29,6 +30,7 @@ class MainPresenter @Inject constructor(
errorHandler: ErrorHandler, errorHandler: ErrorHandler,
studentRepository: StudentRepository, studentRepository: StudentRepository,
private val preferencesRepository: PreferencesRepository, private val preferencesRepository: PreferencesRepository,
private val wulkanowyRepository: WulkanowyRepository,
private val syncManager: SyncManager, private val syncManager: SyncManager,
private val analytics: AnalyticsHelper, private val analytics: AnalyticsHelper,
private val json: Json, private val json: Json,
@ -199,4 +201,11 @@ class MainPresenter @Inject constructor(
.onFailure { errorHandler.dispatch(it) } .onFailure { errorHandler.dispatch(it) }
} }
} }
fun updateSdkMappings() {
presenterScope.launch {
runCatching { wulkanowyRepository.fetchMapping() }
.onFailure { Timber.e(it) }
}
}
} }

View File

@ -27,6 +27,7 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.message.MessageFragment import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog
import io.github.wulkanowy.ui.modules.message.preview.MessagePreviewFragment import io.github.wulkanowy.ui.modules.message.preview.MessagePreviewFragment
import io.github.wulkanowy.ui.modules.panicmode.PanicModeFragment
import io.github.wulkanowy.ui.widgets.DividerItemDecoration import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.getThemeAttrColor
@ -132,6 +133,7 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
) )
messageTabErrorRetry.setOnClickListener { presenter.onRetry() } messageTabErrorRetry.setOnClickListener { presenter.onRetry() }
messageTabErrorDetails.setOnClickListener { presenter.onDetailsClick() } messageTabErrorDetails.setOnClickListener { presenter.onDetailsClick() }
messageTabPanicSection.dashboardPanicButton.setOnClickListener { presenter.onPanicButtonClicked() }
} }
setFragmentResultListener(requireArguments().getString(MESSAGE_TAB_FOLDER_ID)!!) { _, bundle -> setFragmentResultListener(requireArguments().getString(MESSAGE_TAB_FOLDER_ID)!!) { _, bundle ->
@ -283,6 +285,10 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
) )
} }
override fun openPanicWebView(url: String) {
(requireActivity() as MainActivity).pushView(PanicModeFragment.newInstance(url))
}
override fun hideKeyboard() { override fun hideKeyboard() {
activity?.hideSoftInput() activity?.hideSoftInput()
} }

View File

@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.xdrop.fuzzywuzzy.FuzzySearch import me.xdrop.fuzzywuzzy.FuzzySearch
import okhttp3.HttpUrl.Companion.toHttpUrl
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.pow import kotlin.math.pow
@ -429,4 +430,20 @@ class MessageTabPresenter @Inject constructor(
+ dateRatio.toDouble().pow(2) * 2 + dateRatio.toDouble().pow(2) * 2
).toInt() ).toInt()
} }
fun onPanicButtonClicked() {
resourceFlow { studentRepository.getCurrentStudent() }
.onResourceError { errorHandler.dispatch(it) }
.onResourceSuccess {
val baseUrl = it.scrapperBaseUrl.toHttpUrl()
val urlToOpen = baseUrl.newBuilder()
.host("uonetplus${it.scrapperDomainSuffix}-wiadomosciplus.${baseUrl.host}")
.addPathSegment(it.symbol)
.build()
.toString()
view?.openPanicWebView(urlToOpen)
}
.launch("panic_button")
}
} }

View File

@ -50,4 +50,6 @@ interface MessageTabView : BaseView {
fun showRecyclerBottomPadding(show: Boolean) fun showRecyclerBottomPadding(show: Boolean)
fun showMailboxChooser(mailboxes: List<Mailbox>) fun showMailboxChooser(mailboxes: List<Mailbox>)
fun openPanicWebView(url: String)
} }

View File

@ -0,0 +1,99 @@
package io.github.wulkanowy.ui.modules.panicmode
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.View
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.addCallback
import androidx.core.os.bundleOf
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.databinding.FragmentPanicModeBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.WebkitCookieManagerProxy
import io.github.wulkanowy.utils.openInternetBrowser
import javax.inject.Inject
@AndroidEntryPoint
class PanicModeFragment : BaseFragment<FragmentPanicModeBinding>(R.layout.fragment_panic_mode),
MainView.TitledView {
@Inject
lateinit var wulkanowySdkFactory: WulkanowySdkFactory
@Inject
lateinit var webkitCookieManagerProxy: WebkitCookieManagerProxy
private var webView: WebView? = null
override val titleStringId: Int get() = R.string.panic_mode_title
companion object {
private const val PANIC_URL = "panic_mode_url"
fun newInstance(url: String?): PanicModeFragment {
return PanicModeFragment().apply {
arguments = bundleOf(PANIC_URL to url)
}
}
}
@SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentPanicModeBinding.bind(view)
binding.panicModeRefresh.setOnClickListener {
binding.panicModeWebview.loadUrl(
binding.panicModeWebview.url ?: arguments?.getString(PANIC_URL).orEmpty()
)
}
binding.panicModeBack.setOnClickListener { binding.panicModeWebview.goBack() }
binding.panicModeHome.setOnClickListener {
binding.panicModeWebview.loadUrl(
arguments?.getString(PANIC_URL).orEmpty()
)
}
binding.panicModeForward.setOnClickListener { binding.panicModeWebview.goForward() }
binding.panicModeShare.setOnClickListener {
requireContext().openInternetBrowser(
binding.panicModeWebview.url.toString(),
)
}
val onBackPressedCallback = requireActivity().onBackPressedDispatcher
.addCallback(viewLifecycleOwner) {
binding.panicModeWebview.goBack()
}
with(binding.panicModeWebview) {
webView = this
with(settings) {
javaScriptEnabled = true
userAgentString = wulkanowySdkFactory.createBase().userAgent
}
webViewClient = object : WebViewClient() {
override fun doUpdateVisitedHistory(
view: WebView?,
url: String?,
isReload: Boolean
) {
binding.panicModeBack.isEnabled = binding.panicModeWebview.canGoBack()
binding.panicModeForward.isEnabled = binding.panicModeWebview.canGoForward()
onBackPressedCallback.isEnabled = binding.panicModeWebview.canGoBack()
}
}
loadUrl(arguments?.getString(PANIC_URL).orEmpty())
}
}
override fun onDestroy() {
webkitCookieManagerProxy.webkitCookieManager?.flush()
webView?.destroy()
super.onDestroy()
}
}

View File

@ -30,6 +30,10 @@ fun getRefreshKey(name: String, mailbox: Mailbox?, folder: MessageFolder): Strin
return "${name}_${mailbox?.globalKey ?: "all"}_${folder.id}" return "${name}_${mailbox?.globalKey ?: "all"}_${folder.id}"
} }
fun getRefreshKey(name: String): String {
return name
}
class AutoRefreshHelper @Inject constructor( class AutoRefreshHelper @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val sharedPref: SharedPrefProvider private val sharedPref: SharedPrefProvider

View File

@ -1,5 +1,6 @@
Wersja 2.6.1 Wersja 2.6.13
obeszliśmy próby zablokowania naszej aplikacji w dostępie do danych ucznia i wiadomości (ale nie wiemy, czy eduOne działa) dodaliśmy tryb awaryjny (no w sensie taka przeglądarka z dziennikiem w apce, ale nie trzeba się ręcznie logować)
— naprawiliśmy ładowania ucznia na tle klasy i lekcji zrealizowanych
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases

View File

@ -60,6 +60,16 @@
tools:ignore="UseCompoundDrawables" tools:ignore="UseCompoundDrawables"
tools:visibility="invisible"> tools:visibility="invisible">
<include
android:id="@+id/message_tab_panic_section"
layout="@layout/item_dashboard_panic_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="16dp"
android:visibility="visible"
app:layout_constraintTop_toBottomOf="@id/dashboard_error_admin_message" />
<ImageView <ImageView
android:layout_width="100dp" android:layout_width="100dp"
android:layout_height="100dp" android:layout_height="100dp"

View File

@ -0,0 +1,69 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.modules.panicmode.PanicModeFragment">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorControlHighlight">
<com.google.android.material.button.MaterialButton
android:id="@+id/panic_mode_share"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
app:icon="@drawable/ic_share"
app:iconTint="?colorOnSurface" />
<com.google.android.material.button.MaterialButton
android:id="@+id/panic_mode_home"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
app:icon="@drawable/ic_all_home"
app:iconTint="?colorOnSurface" />
<com.google.android.material.button.MaterialButton
android:id="@+id/panic_mode_refresh"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:contentDescription="@string/logviewer_refresh"
app:icon="@drawable/ic_refresh"
app:iconTint="?colorOnSurface" />
<com.google.android.material.button.MaterialButton
android:id="@+id/panic_mode_back"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
app:icon="@drawable/ic_chevron_left"
app:iconTint="?colorOnSurface" />
<com.google.android.material.button.MaterialButton
android:id="@+id/panic_mode_forward"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
app:icon="@drawable/ic_chevron_right"
app:iconTint="?colorOnSurface" />
</LinearLayout>
<WebView
android:id="@+id/panic_mode_webview"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>

View File

@ -1,87 +1,105 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="12dp" android:orientation="vertical">
android:layout_marginVertical="6dp">
<androidx.constraintlayout.widget.ConstraintLayout <com.google.android.material.card.MaterialCardView
android:id="@+id/dashboard_admin_message_item_content" android:id="@+id/dashboard_admin_message"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="wrap_content"
android:layout_marginHorizontal="12dp"
android:layout_marginVertical="6dp">
<ImageView
android:id="@+id/dashboard_admin_message_item_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:src="@drawable/ic_error"
app:layout_constraintBottom_toBottomOf="@id/dashboard_admin_message_item_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/dashboard_admin_message_item_title"
tools:ignore="ContentDescription"
tools:tint="@android:color/black" />
<TextView <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/dashboard_admin_message_item_title" android:id="@+id/dashboard_admin_message_item_content"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent">
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@id/dashboard_admin_message_item_close"
app:layout_constraintStart_toEndOf="@id/dashboard_admin_message_item_icon"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/lorem" />
<ImageView <ImageView
android:id="@+id/dashboard_admin_message_item_close" android:id="@+id/dashboard_admin_message_item_icon"
android:layout_width="48dp" android:layout_width="24dp"
android:layout_height="48dp" android:layout_height="24dp"
android:layout_marginTop="4dp" android:layout_marginStart="16dp"
android:layout_marginEnd="4dp" android:layout_marginEnd="16dp"
android:padding="12dp" android:src="@drawable/ic_error"
android:background="?selectableItemBackgroundBorderless" app:layout_constraintBottom_toBottomOf="@id/dashboard_admin_message_item_title"
android:src="@drawable/ic_close" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@id/dashboard_admin_message_item_title"
app:layout_constraintTop_toTopOf="parent" tools:ignore="ContentDescription"
tools:ignore="ContentDescription" /> tools:tint="@android:color/black" />
<TextView <TextView
android:id="@+id/dashboard_admin_message_item_description" android:id="@+id/dashboard_admin_message_item_title"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="8dp"
android:layout_marginTop="8dp" android:layout_marginTop="16dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp" android:textSize="18sp"
android:textSize="14sp" android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/dashboard_admin_message_item_close"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/dashboard_admin_message_item_icon"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintTop_toBottomOf="@id/dashboard_admin_message_item_title" tools:text="@tools:sample/lorem" />
app:layout_constraintVertical_bias="0"
app:lineHeight="20dp"
tools:maxLines="5"
tools:text="@tools:sample/lorem/random" />
<com.google.android.material.button.MaterialButton <ImageView
android:id="@+id/dashboard_admin_message_item_dismiss" android:id="@+id/dashboard_admin_message_item_close"
style="@style/Widget.Material3.Button.TextButton.Dialog" android:layout_width="48dp"
android:layout_width="wrap_content" android:layout_height="48dp"
android:layout_height="wrap_content" android:layout_marginTop="4dp"
android:layout_marginTop="8dp" android:layout_marginEnd="4dp"
android:layout_marginEnd="16dp" android:background="?selectableItemBackgroundBorderless"
android:layout_marginBottom="8dp" android:padding="12dp"
android:text="@android:string/ok" android:src="@drawable/ic_close"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintTop_toBottomOf="@id/dashboard_admin_message_item_description" tools:ignore="ContentDescription" />
app:layout_constraintVertical_bias="0" />
</androidx.constraintlayout.widget.ConstraintLayout> <TextView
</com.google.android.material.card.MaterialCardView> android:id="@+id/dashboard_admin_message_item_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/dashboard_admin_message_item_title"
app:layout_constraintVertical_bias="0"
app:lineHeight="20dp"
tools:maxLines="5"
tools:text="@tools:sample/lorem/random" />
<com.google.android.material.button.MaterialButton
android:id="@+id/dashboard_admin_message_item_dismiss"
style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
android:text="@android:string/ok"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/dashboard_admin_message_item_description"
app:layout_constraintVertical_bias="0" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<include
android:id="@+id/dashboard_panic_section"
layout="@layout/item_dashboard_panic_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="16dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/dashboard_error_admin_message" />
</LinearLayout>

View File

@ -0,0 +1,28 @@
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="12dp"
android:layout_marginVertical="6dp">
<LinearLayout
android:layout_margin="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Aplikacja nie działa?"
android:textSize="18sp"
android:textStyle="bold" />
<com.google.android.material.button.MaterialButton
android:id="@+id/dashboard_panic_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="Otwórz stronę dziennika" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -872,6 +872,8 @@
<string name="captcha_dialog_title">Strona dziennika VULCAN wymaga weryfikacji</string> <string name="captcha_dialog_title">Strona dziennika VULCAN wymaga weryfikacji</string>
<string name="captcha_dialog_description"><b>Dlaczego to widzę?</b>\nStrona internetowa dziennika, z której Wulkanowy pobiera dane, wyświetla ten sam ekran jak powyżej, więc Wulkanowy musi również ją pokazać, aby móc pobrać dane z tej witryny. Nie da się tego obejść</string> <string name="captcha_dialog_description"><b>Dlaczego to widzę?</b>\nStrona internetowa dziennika, z której Wulkanowy pobiera dane, wyświetla ten sam ekran jak powyżej, więc Wulkanowy musi również ją pokazać, aby móc pobrać dane z tej witryny. Nie da się tego obejść</string>
<string name="captcha_verified_message">Pomyślnie zweryfikowano</string> <string name="captcha_verified_message">Pomyślnie zweryfikowano</string>
<!--Panic mode-->
<string name="panic_mode_title">Awaryjny dostęp</string>
<!--Errors--> <!--Errors-->
<string name="error_no_internet">Brak połączenia z internetem</string> <string name="error_no_internet">Brak połączenia z internetem</string>
<string name="error_invalid_device_datetime">Wystąpił błąd. Sprawdź poprawność daty w urządzeniu</string> <string name="error_invalid_device_datetime">Wystąpił błąd. Sprawdź poprawność daty w urządzeniu</string>

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="sort_alphabetically">Alphabetically</string> <string name="sort_alphabetically">По алфавиту</string>
<string name="sort_by_date">By date</string> <string name="sort_by_date">По дате</string>
<string name="sort_by_average">By average</string> <string name="sort_by_average">По средней</string>
<string name="sort_by_attendance_percentage">By attendance percentage</string> <string name="sort_by_attendance_percentage">Согласно проценту посещаемости</string>
<string name="sort_by_subject_attendance_balance">By subject attendance balance</string> <string name="sort_by_subject_attendance_balance">Согласно балансу посещаемости уроков</string>
<string-array name="app_theme_entries" tools:ignore="InconsistentArrays"> <string-array name="app_theme_entries" tools:ignore="InconsistentArrays">
<item>Светлая</item> <item>Светлая</item>
<item>Тёмная</item> <item>Тёмная</item>
@ -52,14 +52,14 @@
<item>Средняя из оценок со всего года</item> <item>Средняя из оценок со всего года</item>
</string-array> </string-array>
<string-array name="timetable_show_gaps_entries"> <string-array name="timetable_show_gaps_entries">
<item>Don\'t show</item> <item>Не показывать</item>
<item>Only between lessons</item> <item>Только между уроками</item>
<item>Before and between lessons</item> <item>Перед и между уроками</item>
</string-array> </string-array>
<string-array name="timetable_show_additional_lessons_entries"> <string-array name="timetable_show_additional_lessons_entries">
<item>Don\'t show</item> <item>Не показывать</item>
<item>Show inline</item> <item>Показать в строке</item>
<item>Show below regular lessons</item> <item>Показать ниже обычных уроков</item>
</string-array> </string-array>
<string-array name="dashboard_tile_entries"> <string-array name="dashboard_tile_entries">
<item>Счастливый номер</item> <item>Счастливый номер</item>

View File

@ -13,7 +13,7 @@
<string name="logviewer_title">Просмотр журнала</string> <string name="logviewer_title">Просмотр журнала</string>
<string name="debug_title">Отладка</string> <string name="debug_title">Отладка</string>
<string name="notification_debug_title">Отладка уведомлений</string> <string name="notification_debug_title">Отладка уведомлений</string>
<string name="debug_cookies_clear">Clear webview cookies</string> <string name="debug_cookies_clear">Очистить файлы cookie</string>
<string name="contributors_title">Разработчики</string> <string name="contributors_title">Разработчики</string>
<string name="license_title">Лицензии</string> <string name="license_title">Лицензии</string>
<string name="message_title">Сообщения</string> <string name="message_title">Сообщения</string>
@ -38,14 +38,14 @@
<string name="login_login_pesel_email_hint">Логин, PESEL или электронная почта</string> <string name="login_login_pesel_email_hint">Логин, PESEL или электронная почта</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">Мобильный API</string> <string name="login_type_api">Мобильный 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>
<string name="login_token_hint">Token</string> <string name="login_token_hint">Token</string>
<string name="login_pin_hint">PIN</string> <string name="login_pin_hint">PIN</string>
<string name="login_symbol_hint">Symbol</string> <string name="login_symbol_hint">Symbol</string>
<string name="login_symbol_placeholder">E.g. \"lodz\" or \"powiatjaroslawski\"</string> <string name="login_symbol_placeholder">Например: \"lodz\" или \"powiatjaroslawski\"</string>
<string name="login_sign_in">Войти</string> <string name="login_sign_in">Войти</string>
<string name="login_invalid_password">Пароль слишком короткий</string> <string name="login_invalid_password">Пароль слишком короткий</string>
<string name="login_incorrect_password_default">Данные для входа указаны неверно</string> <string name="login_incorrect_password_default">Данные для входа указаны неверно</string>
@ -56,8 +56,8 @@
<string name="login_invalid_email">Неверный e-mail</string> <string name="login_invalid_email">Неверный e-mail</string>
<string name="login_invalid_login">Используйте назначенный логин вместо e-mail</string> <string name="login_invalid_login">Используйте назначенный логин вместо e-mail</string>
<string name="login_invalid_custom_email">Используйте назначенный логин или email в @%1$s</string> <string name="login_invalid_custom_email">Используйте назначенный логин или email в @%1$s</string>
<string name="login_invalid_domain_suffix">Invalid domain suffix</string> <string name="login_invalid_domain_suffix">Недопустимый суффикс домена</string>
<string name="login_invalid_symbol">Invalid symbol. If you cannot find it, please contact the school</string> <string name="login_invalid_symbol">Неверный символ. Если вы не можете найти символ, пожалуйста, свяжитесь со школой</string>
<string name="login_invalid_symbol_definitely">Don\'t make this up! If you cannot find it, please contact the school</string> <string name="login_invalid_symbol_definitely">Don\'t make this up! If you cannot find it, please contact the school</string>
<string name="login_incorrect_symbol">Ученик не найден. Проверьте symbol и выбранный тип дненика UONET+</string> <string name="login_incorrect_symbol">Ученик не найден. Проверьте symbol и выбранный тип дненика UONET+</string>
<string name="login_duplicate_student">Данный ученик уже авторизован</string> <string name="login_duplicate_student">Данный ученик уже авторизован</string>
@ -73,7 +73,7 @@
<string name="login_contact_discord">Discord</string> <string name="login_contact_discord">Discord</string>
<string name="login_email_intent_title">Отправить письмо</string> <string name="login_email_intent_title">Отправить письмо</string>
<string name="login_recover_warning">Убедитесь, что вы выбрали правильный тип дневника UONET+</string> <string name="login_recover_warning">Убедитесь, что вы выбрали правильный тип дневника UONET+</string>
<string name="login_recover_button">Reset password</string> <string name="login_recover_button">Сбросить пароль</string>
<string name="login_recover_title">Восстановите свой аккаунт</string> <string name="login_recover_title">Восстановите свой аккаунт</string>
<string name="login_recover">Восстановить</string> <string name="login_recover">Восстановить</string>
<string name="login_signed_in">Ученик уже авторизован</string> <string name="login_signed_in">Ученик уже авторизован</string>
@ -81,13 +81,13 @@
<string name="login_other_search_locations">Другие места поиска</string> <string name="login_other_search_locations">Другие места поиска</string>
<string name="login_no_active_student">Не найдено активных учеников</string> <string name="login_no_active_student">Не найдено активных учеников</string>
<string name="login_symbol_enter">Введите другой symbol</string> <string name="login_symbol_enter">Введите другой symbol</string>
<string name="login_support_title">Get help</string> <string name="login_support_title">Помощь</string>
<string name="login_support_school_hint">Full school name with the town (required)</string> <string name="login_support_school_hint">Полное название школы с городом (обязательно)</string>
<string name="login_support_school_placeholder">Np. ZSTiO Jarosław lub SP nr 99 w Łodzi</string> <string name="login_support_school_placeholder">Например: ZSTiO Jarosław или SP nr 99 w Łodzi</string>
<string name="login_support_school_invalid">Enter correct name of the school</string> <string name="login_support_school_invalid">Введите правильное название школы</string>
<string name="login_support_additional_hint">Additional information in Polish (optional)</string> <string name="login_support_additional_hint">Дополнительная информация на польском языке (опционально)</string>
<string name="login_support_additional_placeholder">Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\"</string> <string name="login_support_additional_placeholder">Например: \"Ostatnio zmieniłem szkoł i…\" или \"Jestem rodzicem i nie widz drugiego dziecka…\"</string>
<string name="login_support_submit">Submit</string> <string name="login_support_submit">Отправить</string>
<!--Notifications--> <!--Notifications-->
<string name="notifications_header_title">Включить уведомления</string> <string name="notifications_header_title">Включить уведомления</string>
<string name="notifications_header_description">Включить уведомления, чтобы вы не пропустили сообщение от учителя или новую оценку</string> <string name="notifications_header_description">Включить уведомления, чтобы вы не пропустили сообщение от учителя или новую оценку</string>
@ -98,8 +98,8 @@
<string name="main_log_in">Войти</string> <string name="main_log_in">Войти</string>
<string name="main_session_expired">Сеанс истёк</string> <string name="main_session_expired">Сеанс истёк</string>
<string name="main_session_relogin">Сеанс истёк, авторизуйтесь снова</string> <string name="main_session_relogin">Сеанс истёк, авторизуйтесь снова</string>
<string name="main_expired_credentials_title">Password has expired or been changed</string> <string name="main_expired_credentials_title">Срок действия пароля истек или был изменен</string>
<string name="main_expired_credentials_description">Your account password has expired or been changed. You will need to log in to Wulkanowy again</string> <string name="main_expired_credentials_description">Пароль вашей учетной записи устарел или был изменен. Вам нужно будет войти в Wulkanowy снова</string>
<string name="main_support_title">Поддержка приложения</string> <string name="main_support_title">Поддержка приложения</string>
<string name="main_support_description">Вам нравится это приложение? Поддержите его разработку, включив неинвазивную рекламу, которую можно отключить в любое время</string> <string name="main_support_description">Вам нравится это приложение? Поддержите его разработку, включив неинвазивную рекламу, которую можно отключить в любое время</string>
<string name="main_support_positive">Включить рекламу</string> <string name="main_support_positive">Включить рекламу</string>
@ -113,16 +113,16 @@
<string name="grade_comment">Комментарий</string> <string name="grade_comment">Комментарий</string>
<string name="grade_number_new_items">Количество новых оценок: %1$d</string> <string name="grade_number_new_items">Количество новых оценок: %1$d</string>
<string name="grade_average">Средняя оценка: %1$.2f</string> <string name="grade_average">Средняя оценка: %1$.2f</string>
<string name="grade_average_year">Annual: %1$.2f</string> <string name="grade_average_year">Годовое: %1$.2f</string>
<string name="grade_points_sum">Баллы: %s</string> <string name="grade_points_sum">Баллы: %s</string>
<string name="grade_no_average">Нет средней оценки</string> <string name="grade_no_average">Нет средней оценки</string>
<string name="grade_summary_average_semester">Semester average</string> <string name="grade_summary_average_semester">Средняя семестра</string>
<string name="grade_summary_average_year">Annual average</string> <string name="grade_summary_average_year">Средняя годовой</string>
<string name="grade_summary_points">Сумма баллов</string> <string name="grade_summary_points">Сумма баллов</string>
<string name="grade_summary_final_grade">Итоговая оценка</string> <string name="grade_summary_final_grade">Итоговая оценка</string>
<string name="grade_summary_predicted_grade">Ожидаемая оценка</string> <string name="grade_summary_predicted_grade">Ожидаемая оценка</string>
<string name="grade_summary_descriptive">Descriptive grade</string> <string name="grade_summary_descriptive">Описательная оценка</string>
<string name="grade_summary_calculated_average">Calculated semester average</string> <string name="grade_summary_calculated_average">Рассчитанная средняя семестра</string>
<string name="grade_summary_calculated_average_annual">Calculated annual average</string> <string name="grade_summary_calculated_average_annual">Calculated annual average</string>
<string name="grade_summary_calculated_average_help_dialog_title">Как работает \"Рассчитанная средняя оценка\"?</string> <string name="grade_summary_calculated_average_help_dialog_title">Как работает \"Рассчитанная средняя оценка\"?</string>
<string name="grade_summary_calculated_average_help_dialog_message">Рассчитанная средняя оценка - это среднее арифметическое, рассчитанное на основе средних оценок по предметам. Это позволяет узнать приблизительную итоговую среднюю оценку. Она рассчитывается способом, выбранным пользователем в настройках приложения. Рекомендуется выбрать подходящий вариант, так как каждая школа по разному считает среднюю оценку. Кроме того, если ваша школа выставляет средние оценки по предметам на странице Vulcan, приложение просто загрузит их. Это можно изменить, заставив приложение считать среднюю оценку в настройках.\n\n<b>Средняя из оценок выбранного семестра</b>:\n1. Вычисление средневзвешенного значения по каждому предмету за семестр\n2.Суммирование вычисленных значений\n3. Вычисление среднего арифметического суммированных значений\n\n<b>Средняя из средних оценок семестров</b>:\n1.Расчет средневзвешенного значения для каждого предмета в семестрах. \n2. Вычисление среднего арифметического из средневзвешенных значений для каждого предмета в семестрах.\n3. Суммирование средних арифметических\n4. Вычисление среднего арифматического из суммированных значений\n\n<b>Средняя из оценок со всего года:</b>\n1. Расчет средневзвешенного значения по каждому предмету за год. Итоговое среднее значение за 1 семестр не имеет значения.\n2. Суммирование вычисленных средних\n3. Расчет среднего арифметического суммированных чисел</string> <string name="grade_summary_calculated_average_help_dialog_message">Рассчитанная средняя оценка - это среднее арифметическое, рассчитанное на основе средних оценок по предметам. Это позволяет узнать приблизительную итоговую среднюю оценку. Она рассчитывается способом, выбранным пользователем в настройках приложения. Рекомендуется выбрать подходящий вариант, так как каждая школа по разному считает среднюю оценку. Кроме того, если ваша школа выставляет средние оценки по предметам на странице Vulcan, приложение просто загрузит их. Это можно изменить, заставив приложение считать среднюю оценку в настройках.\n\n<b>Средняя из оценок выбранного семестра</b>:\n1. Вычисление средневзвешенного значения по каждому предмету за семестр\n2.Суммирование вычисленных значений\n3. Вычисление среднего арифметического суммированных значений\n\n<b>Средняя из средних оценок семестров</b>:\n1.Расчет средневзвешенного значения для каждого предмета в семестрах. \n2. Вычисление среднего арифметического из средневзвешенных значений для каждого предмета в семестрах.\n3. Суммирование средних арифметических\n4. Вычисление среднего арифматического из суммированных значений\n\n<b>Средняя из оценок со всего года:</b>\n1. Расчет средневзвешенного значения по каждому предмету за год. Итоговое среднее значение за 1 семестр не имеет значения.\n2. Суммирование вычисленных средних\n3. Расчет среднего арифметического суммированных чисел</string>
@ -167,10 +167,10 @@
<item quantity="other">Новые итоговые оценки</item> <item quantity="other">Новые итоговые оценки</item>
</plurals> </plurals>
<plurals name="grade_new_items_descriptive"> <plurals name="grade_new_items_descriptive">
<item quantity="one">New descriptive grade</item> <item quantity="one">Новая описательная оценка</item>
<item quantity="few">New descriptive grades</item> <item quantity="few">Новые описательные оценки</item>
<item quantity="many">New descriptive grades</item> <item quantity="many">Новые описательные оценки</item>
<item quantity="other">New descriptive grades</item> <item quantity="other">Новые описательные оценки</item>
</plurals> </plurals>
<plurals name="grade_notify_new_items"> <plurals name="grade_notify_new_items">
<item quantity="one">Вы получили %1$d новую оценку</item> <item quantity="one">Вы получили %1$d новую оценку</item>
@ -191,14 +191,14 @@
<item quantity="other">Вы получили %1$d новых итоговые оценки</item> <item quantity="other">Вы получили %1$d новых итоговые оценки</item>
</plurals> </plurals>
<plurals name="grade_notify_new_items_descriptive"> <plurals name="grade_notify_new_items_descriptive">
<item quantity="one">You received %1$d descriptive grade</item> <item quantity="one">Вы получили %1$d новую описательную оценку</item>
<item quantity="few">You received %1$d descriptive grades</item> <item quantity="few">Вы получили %1$d новых описательных оценок</item>
<item quantity="many">You received %1$d descriptive grades</item> <item quantity="many">Вы получили %1$d новых описательных оценок</item>
<item quantity="other">You received %1$d descriptive grades</item> <item quantity="other">Вы получили %1$d новых описательных оценок</item>
</plurals> </plurals>
<!--Timetable--> <!--Timetable-->
<string name="timetable_lesson">Урок</string> <string name="timetable_lesson">Урок</string>
<string name="timetable_additional_lesson">Additional lesson</string> <string name="timetable_additional_lesson">Дополнительный урок</string>
<string name="timetable_room">Аудитория</string> <string name="timetable_room">Аудитория</string>
<string name="timetable_group">Группа</string> <string name="timetable_group">Группа</string>
<string name="timetable_time">Часы</string> <string name="timetable_time">Часы</string>
@ -217,10 +217,10 @@
<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"> <plurals name="timetable_no_lesson">
<item quantity="one">No lesson</item> <item quantity="one">Нет урока</item>
<item quantity="few">No lessons</item> <item quantity="few">Нет урока</item>
<item quantity="many">No lessons</item> <item quantity="many">Нет урока</item>
<item quantity="other">No lessons</item> <item quantity="other">Нет урока</item>
</plurals> </plurals>
<plurals name="timetable_notify_new_items_title"> <plurals name="timetable_notify_new_items_title">
<item quantity="one">Изменение расписания</item> <item quantity="one">Изменение расписания</item>
@ -270,7 +270,7 @@
<string name="additional_lessons_end_time_error">Время окончания должно быть больше, чем время начала</string> <string name="additional_lessons_end_time_error">Время окончания должно быть больше, чем время начала</string>
<!--Attendance--> <!--Attendance-->
<string name="attendance_summary_button">Итоговая посещаемость</string> <string name="attendance_summary_button">Итоговая посещаемость</string>
<string name="attendance_calculator_button">Attendance calculator</string> <string name="attendance_calculator_button">Калькулятор посещаемости</string>
<string name="attendance_calculator_summary_balance_positive"><b>%1$d</b> over target</string> <string name="attendance_calculator_summary_balance_positive"><b>%1$d</b> over target</string>
<string name="attendance_calculator_summary_balance_neutral">right on target</string> <string name="attendance_calculator_summary_balance_neutral">right on target</string>
<string name="attendance_calculator_summary_balance_negative"><b>%1$d</b> under target</string> <string name="attendance_calculator_summary_balance_negative"><b>%1$d</b> under target</string>
@ -347,10 +347,10 @@
<string name="message_forward">Переслать</string> <string name="message_forward">Переслать</string>
<string name="message_select_all">Выбрать все</string> <string name="message_select_all">Выбрать все</string>
<string name="message_unselect_all">Отменить выбор</string> <string name="message_unselect_all">Отменить выбор</string>
<string name="message_restore_from_trash">Restore from trash</string> <string name="message_restore_from_trash">Восстановить из корзины</string>
<string name="message_move_to_trash">Перенести в корзину</string> <string name="message_move_to_trash">Перенести в корзину</string>
<string name="message_delete_forever">Удалить навсегда</string> <string name="message_delete_forever">Удалить навсегда</string>
<string name="message_restore_success">Message restored successfully</string> <string name="message_restore_success">Сообщение успешно восстановлено</string>
<string name="message_delete_success">Сообщение успешно удалено</string> <string name="message_delete_success">Сообщение успешно удалено</string>
<string name="message_mailbox_type_student">ученик</string> <string name="message_mailbox_type_student">ученик</string>
<string name="message_mailbox_type_parent">родитель</string> <string name="message_mailbox_type_parent">родитель</string>
@ -396,10 +396,10 @@
<item quantity="other">%1$d выбрано</item> <item quantity="other">%1$d выбрано</item>
</plurals> </plurals>
<string name="message_messages_deleted">Сообщение удалено</string> <string name="message_messages_deleted">Сообщение удалено</string>
<string name="message_messages_restored">Messages restored</string> <string name="message_messages_restored">Сообщения восстановлены</string>
<string name="message_mailbox_chooser_title">Выбрать почтовый ящик</string> <string name="message_mailbox_chooser_title">Выбрать почтовый ящик</string>
<string name="message_incognito_mode_on">Incognito mode is on</string> <string name="message_incognito_mode_on">Режим инкогнито включен</string>
<string name="message_incognito_description">Thanks to incognito mode sender is not notified when you read the message</string> <string name="message_incognito_description">Благодаря режиму инкогнито отправитель не уведомлен о прочтении сообщения</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>
@ -748,8 +748,8 @@
<string name="pref_view_app_theme">Тема</string> <string name="pref_view_app_theme">Тема</string>
<string name="pref_view_expand_grade">Разворачивание оценок</string> <string name="pref_view_expand_grade">Разворачивание оценок</string>
<string name="pref_view_timetable_show_groups">Показать группы рядом с темами</string> <string name="pref_view_timetable_show_groups">Показать группы рядом с темами</string>
<string name="pref_view_timetable_show_additional_lessons">Show additional lessons</string> <string name="pref_view_timetable_show_additional_lessons">Показать дополнительные уроки</string>
<string name="pref_view_timetable_show_gaps">Show empty tiles where there\'s no lesson</string> <string name="pref_view_timetable_show_gaps">Показать пустые поля, где нет уроков</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>
@ -790,12 +790,12 @@
<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">Режим инкогнито</string>
<string name="pref_other_incognito_mode_summary">Do not inform about reading the message</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>
<string name="pref_ads_consent">Show consent to data processing</string> <string name="pref_ads_consent">Показать согласие на обработку данных</string>
<string name="pref_ads_show_in_app">Показать рекламу в приложении</string> <string name="pref_ads_show_in_app">Показать рекламу в приложении</string>
<string name="pref_ads_support">Посмотреть рекламу для поддержки проекта</string> <string name="pref_ads_support">Посмотреть рекламу для поддержки проекта</string>
<string name="pref_ads_privacy_title">Согласие на обработку данных</string> <string name="pref_ads_privacy_title">Согласие на обработку данных</string>
@ -813,8 +813,8 @@
<string name="pref_dashboard_appearance_header">Главная</string> <string name="pref_dashboard_appearance_header">Главная</string>
<string name="pref_dashboard_appearance_tiles_title">Видимость плиток</string> <string name="pref_dashboard_appearance_tiles_title">Видимость плиток</string>
<string name="pref_attendance_appearance_view">Посещаемость</string> <string name="pref_attendance_appearance_view">Посещаемость</string>
<string name="pref_attendance_calculator_appearance_view">Attendance calculator</string> <string name="pref_attendance_calculator_appearance_view">Калькулятор посещаемости</string>
<string name="pref_attendance_calculator_appearance_settings_title">Settings</string> <string name="pref_attendance_calculator_appearance_settings_title">Настройки</string>
<string name="pref_timetable_appearance_view">Расписание</string> <string name="pref_timetable_appearance_view">Расписание</string>
<string name="pref_grades_advanced_header">Оценки</string> <string name="pref_grades_advanced_header">Оценки</string>
<string name="pref_counted_average_advanced_header">Рассчитанная средняя оценка</string> <string name="pref_counted_average_advanced_header">Рассчитанная средняя оценка</string>
@ -866,31 +866,31 @@
<string name="auth_button">Авторизовать</string> <string name="auth_button">Авторизовать</string>
<string name="auth_success">Авторизация прошла успешно</string> <string name="auth_success">Авторизация прошла успешно</string>
<string name="auth_title">Авторизация</string> <string name="auth_title">Авторизация</string>
<string name="auth_description">Dear Parent,&lt;br /&gt;&lt;br /&gt;To authorize and ensure the security of data, we kindly ask you to enter below PESEL number of student &lt;b&gt;%1$s&lt;/b&gt;. These details are essential for the proper assignment of access and protection of personal data in accordance with applicable regulations.&lt;br /&gt;&lt;br /&gt;After entering the data, it will be verified to ensure that access to the VULCAN system is granted exclusively to authorized individuals. Should you have any doubts or problems, please contact the school diary administrator to clarify the situation.&lt;br /&gt;&lt;br /&gt;We maintain the highest standards of personal data protection and ensure that all information provided is secure. Wulkanowy app does not store or process the PESEL number.&lt;br /&gt;&lt;br /&gt;We remind you that providing full and accurate data is mandatory and necessary for the use of the VULCAN system.</string> <string name="auth_description">Уважаемый родитель,&lt;br /&gt;&lt;br /&gt;для авторизации и обеспечения безопасности данных, просим Вас ввести ниже номер PESEL &lt;b&gt;%1$s&lt;/b&gt;. Эти данные необходимы для надлежащего доступа к и защиты личных данных в соответствии с действующими нормами.&lt;br /&gt;&lt;br /&gt;После ввода данных мы обеспечим проверку, чтобы доступ к системе VULCAN был предоставлен исключительно уполномоченным лицам. Если у Вас возникли какие-либо сомнения или проблемы, пожалуйста, свяжитесь с администратором школьного дневника для уточнения ситуации.&lt;br /&gt;&lt;br /&gt;Мы соблюдаем наивысшие стандарты защиты персональных данных и гарантируем сохранность всей информации. Приложение Wulkanowy не сохраняет и не обрабатывает номер PESEL.&lt;br /&gt;&lt;br /&gt;Напоминаем, что предоставление точных данных является обязательным и необходимым для использования системы VULCAN.</string>
<string name="auth_button_skip">Пропустить сейчас</string> <string name="auth_button_skip">Пропустить сейчас</string>
<!--Captcha--> <!--Captcha-->
<string name="captcha_dialog_title">VULCAN\'s website requires verification</string> <string name="captcha_dialog_title">Требуется верификация веб-сайта VULCAN</string>
<string name="captcha_dialog_description"><b>Why am I seeing this?</b>\nThe register website from which Wulkanowy downloads data displays the same screen as above, so Wulkanowy must also show it to be able to download data from this website. There\'s no way around it</string> <string name="captcha_dialog_description"><b>Why am I seeing this?</b>\nThe register website from which Wulkanowy downloads data displays the same screen as above, so Wulkanowy must also show it to be able to download data from this website. There\'s no way around it</string>
<string name="captcha_verified_message">Verified successfully</string> <string name="captcha_verified_message">Верификация успешна</string>
<!--Errors--> <!--Errors-->
<string name="error_no_internet">Интернет-соединение отсутствует</string> <string name="error_no_internet">Интернет-соединение отсутствует</string>
<string name="error_invalid_device_datetime">Произошла ошибка. Проверьте время на вашем устройстве</string> <string name="error_invalid_device_datetime">Произошла ошибка. Проверьте время на вашем устройстве</string>
<string name="error_account_inactive">This account is inactive. Try logging in again</string> <string name="error_account_inactive">Эта учетная запись неактивна. Попробуйте войти снова</string>
<string name="error_timeout">Не удалось подключиться к дневнику. Возможно, сервера перегружены, повторите попытку позже</string> <string name="error_timeout">Не удалось подключиться к дневнику. Возможно, сервера перегружены, повторите попытку позже</string>
<string name="error_login_failed">Не удалось загрузить данные, повторите попытку позже</string> <string name="error_login_failed">Не удалось загрузить данные, повторите попытку позже</string>
<string name="error_password_invalid">Your password has expired or been changed. Please log in again</string> <string name="error_password_invalid">Ваш пароль устарел или был изменен. Пожалуйста, войдите снова</string>
<string name="error_password_change_required">Необходимо изменить пароль дневника</string> <string name="error_password_change_required">Необходимо изменить пароль дневника</string>
<string name="error_service_unavailable">UONET+ проводит техническое обслуживание, повторите попытку позже</string> <string name="error_service_unavailable">UONET+ проводит техническое обслуживание, повторите попытку позже</string>
<string name="error_unknown_uonet">Неизвестная ошибка дневника UONET+, повторите попытку позже</string> <string name="error_unknown_uonet">Неизвестная ошибка дневника UONET+, повторите попытку позже</string>
<string name="error_unknown_app">Неизвестная ошибка приложения, повторите попытку позже</string> <string name="error_unknown_app">Неизвестная ошибка приложения, повторите попытку позже</string>
<string name="error_cloudflare_captcha">Captcha verification required</string> <string name="error_cloudflare_captcha">Требуется подтверждение капчи</string>
<string name="error_unknown">Произошла непредвиденная ошибка</string> <string name="error_unknown">Произошла непредвиденная ошибка</string>
<string name="error_feature_disabled">Функция отключена вашей школой</string> <string name="error_feature_disabled">Функция отключена вашей школой</string>
<string name="error_feature_not_available">Функция недоступна в режиме Mobile API. Воспользуйтесь другим режимом</string> <string name="error_feature_not_available">Функция недоступна в режиме Mobile API. Воспользуйтесь другим режимом</string>
<string name="error_field_required">Это поле обязательно</string> <string name="error_field_required">Это поле обязательно</string>
<!-- Mute system --> <!-- Mute system -->
<string name="message_mute">Mute</string> <string name="message_mute">Отключить уведомления</string>
<string name="message_unmute">Unmute</string> <string name="message_unmute">Включить уведомления</string>
<string name="message_mute_success">You have muted this user</string> <string name="message_mute_success">Вы отключили уведомления от этого пользователя</string>
<string name="message_unmute_success">You have unmuted this user</string> <string name="message_unmute_success">Вы включили уведомления от этого пользователя снова</string>
</resources> </resources>

View File

@ -28,7 +28,7 @@
<string name="student_info_title">Student info</string> <string name="student_info_title">Student info</string>
<string name="dashboard_title">Dashboard</string> <string name="dashboard_title">Dashboard</string>
<string name="notifications_center_title">Notifications center</string> <string name="notifications_center_title">Notifications center</string>
<string name="menu_order_title">Menu configuartion</string> <string name="menu_order_title">Menu configuration</string>
<!--Subtitles--> <!--Subtitles-->
@ -869,6 +869,9 @@
<string name="captcha_dialog_description"><b>Why am I seeing this?</b>\nThe register website from which Wulkanowy downloads data displays the same screen as above, so Wulkanowy must also show it to be able to download data from this website. There\'s no way around it</string> <string name="captcha_dialog_description"><b>Why am I seeing this?</b>\nThe register website from which Wulkanowy downloads data displays the same screen as above, so Wulkanowy must also show it to be able to download data from this website. There\'s no way around it</string>
<string name="captcha_verified_message">Verified successfully</string> <string name="captcha_verified_message">Verified successfully</string>
<!--Panic mode-->
<string name="panic_mode_title">Emergency access</string>
<!--Errors--> <!--Errors-->
<string name="error_no_internet">No internet connection</string> <string name="error_no_internet">No internet connection</string>

View File

@ -8,6 +8,7 @@ import io.mockk.mockk
fun createWulkanowySdkFactoryMock(sdk: Sdk) = mockk<WulkanowySdkFactory>() fun createWulkanowySdkFactoryMock(sdk: Sdk) = mockk<WulkanowySdkFactory>()
.apply { .apply {
every { create() } returns sdk every { createBase() } returns sdk
coEvery { create() } returns sdk
coEvery { create(any(), any()) } returns sdk coEvery { create(any(), any()) } returns sdk
} }

View File

@ -11,7 +11,6 @@ import io.github.wulkanowy.sdk.pojo.RegisterStudent
import io.mockk.Runs import io.mockk.Runs
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.every
import io.mockk.just import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.spyk import io.mockk.spyk
@ -40,11 +39,13 @@ class WulkanowySdkFactoryTest {
chuckerInterceptor = mockk(), chuckerInterceptor = mockk(),
remoteConfig = mockk(relaxed = true), remoteConfig = mockk(relaxed = true),
webkitCookieManagerProxy = mockk(), webkitCookieManagerProxy = mockk(),
studentDb = studentDao studentDb = studentDao,
wulkanowyRepository = mockk(relaxed = true),
context = mockk(),
) )
) )
every { wulkanowySdkFactory.create() } returns sdk coEvery { wulkanowySdkFactory.create() } returns sdk
} }
@Test @Test

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.modules.main
import io.github.wulkanowy.MainCoroutineRule import io.github.wulkanowy.MainCoroutineRule
import io.github.wulkanowy.data.repositories.PreferencesRepository 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.repositories.WulkanowyRepository
import io.github.wulkanowy.services.sync.SyncManager import io.github.wulkanowy.services.sync.SyncManager
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
@ -31,6 +32,9 @@ class MainPresenterTest {
@MockK @MockK
lateinit var studentRepository: StudentRepository lateinit var studentRepository: StudentRepository
@MockK(relaxed = true)
lateinit var wulkanowyRepository: WulkanowyRepository
@MockK(relaxed = true) @MockK(relaxed = true)
lateinit var prefRepository: PreferencesRepository lateinit var prefRepository: PreferencesRepository
@ -65,7 +69,8 @@ class MainPresenterTest {
analytics = analytics, analytics = analytics,
json = Json, json = Json,
appInfo = appInfo, appInfo = appInfo,
adsHelper = adsHelper adsHelper = adsHelper,
wulkanowyRepository = wulkanowyRepository
) )
presenter.onAttachView(mainView, null) presenter.onAttachView(mainView, null)
} }