forked from github/wulkanowy-mirror
sync fork with v2.5.2 + fix eduOne
This commit is contained in:
@ -27,15 +27,12 @@ android {
|
||||
testApplicationId "io.github.tests.wulkanowy"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 34
|
||||
versionCode 148
|
||||
versionName "2.4.2"
|
||||
versionCode 151
|
||||
versionName "2.5.2"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
resValue "string", "app_name", "Wulkanowy"
|
||||
manifestPlaceholders = [
|
||||
firebase_enabled: project.hasProperty("enableFirebase"),
|
||||
admob_project_id: ""
|
||||
]
|
||||
manifestPlaceholders = [admob_project_id: ""]
|
||||
|
||||
buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "null"
|
||||
buildConfigField "String", "DASHBOARD_TILE_AD_ID", "null"
|
||||
@ -76,7 +73,6 @@ android {
|
||||
resValue "string", "app_name", "Wulkanowy DEV"
|
||||
applicationIdSuffix ".dev"
|
||||
versionNameSuffix "-dev"
|
||||
ext.enableCrashlytics = project.hasProperty("enableFirebase")
|
||||
buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\""
|
||||
buildConfigField "String", "SCHOOLS_BASE_URL", '"https://schools.wulkanowy.net.pl"'
|
||||
}
|
||||
@ -164,8 +160,8 @@ play {
|
||||
defaultToAppBundles = false
|
||||
track = 'production'
|
||||
releaseStatus = ReleaseStatus.IN_PROGRESS
|
||||
userFraction = 0.99d
|
||||
updatePriority = 2
|
||||
userFraction = 0.50d
|
||||
updatePriority = 3
|
||||
enabled.set(false)
|
||||
}
|
||||
|
||||
@ -190,12 +186,12 @@ ext {
|
||||
android_hilt = "1.2.0"
|
||||
room = "2.6.1"
|
||||
chucker = "4.0.0"
|
||||
mockk = "1.13.9"
|
||||
mockk = "1.13.10"
|
||||
coroutines = "1.8.0"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'io.github.wulkanowy:sdk:2.4.2-SNAPSHOT'
|
||||
implementation 'io.github.wulkanowy:sdk:2.5.2'
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
||||
|
||||
@ -252,13 +248,13 @@ dependencies {
|
||||
implementation 'com.fredporciuncula:flow-preferences:1.9.1'
|
||||
implementation 'org.apache.commons:commons-text:1.11.0'
|
||||
|
||||
playImplementation platform('com.google.firebase:firebase-bom:32.7.2')
|
||||
playImplementation platform('com.google.firebase:firebase-bom:32.7.4')
|
||||
playImplementation 'com.google.firebase:firebase-analytics'
|
||||
playImplementation 'com.google.firebase:firebase-messaging'
|
||||
playImplementation 'com.google.firebase:firebase-crashlytics:'
|
||||
playImplementation 'com.google.firebase:firebase-config'
|
||||
|
||||
playImplementation 'com.google.android.gms:play-services-ads:22.6.0'
|
||||
playImplementation 'com.google.android.gms:play-services-ads:23.0.0'
|
||||
playImplementation "com.google.android.play:integrity:1.3.0"
|
||||
playImplementation 'com.google.android.play:app-update-ktx:2.1.0'
|
||||
playImplementation 'com.google.android.play:review-ktx:2.0.1'
|
||||
|
@ -1,7 +1,8 @@
|
||||
#!/bin/bash -
|
||||
|
||||
content=$(cat < "app/src/main/play/release-notes/pl-PL/default.txt") || exit
|
||||
if [[ "${#content}" -gt 500 ]]; then
|
||||
content2=echo "$content" | dos2unix
|
||||
if [[ "${#content2}" -gt 500 ]]; then
|
||||
echo >&2 "Release notes content has reached the limit of 500 characters"
|
||||
exit 1
|
||||
fi
|
||||
|
2533
app/schemas/io.github.wulkanowy.data.db.AppDatabase/61.json
Normal file
2533
app/schemas/io.github.wulkanowy.data.db.AppDatabase/61.json
Normal file
File diff suppressed because it is too large
Load Diff
2547
app/schemas/io.github.wulkanowy.data.db.AppDatabase/62.json
Normal file
2547
app/schemas/io.github.wulkanowy.data.db.AppDatabase/62.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,92 +0,0 @@
|
||||
{
|
||||
"agcgw": {
|
||||
"backurl": "connect-dre.hispace.hicloud.com",
|
||||
"url": "connect-dre.dbankcloud.cn",
|
||||
"websocketbackurl": "connect-ws-dre.hispace.dbankcloud.com",
|
||||
"websocketurl": "connect-ws-dre.hispace.dbankcloud.cn"
|
||||
},
|
||||
"agcgw_all": {
|
||||
"CN": "connect-drcn.dbankcloud.cn",
|
||||
"CN_back": "connect-drcn.hispace.hicloud.com",
|
||||
"DE": "connect-dre.dbankcloud.cn",
|
||||
"DE_back": "connect-dre.hispace.hicloud.com",
|
||||
"RU": "connect-drru.hispace.dbankcloud.ru",
|
||||
"RU_back": "connect-drru.hispace.dbankcloud.cn",
|
||||
"SG": "connect-dra.dbankcloud.cn",
|
||||
"SG_back": "connect-dra.hispace.hicloud.com"
|
||||
},
|
||||
"websocketgw_all": {
|
||||
"CN": "connect-ws-drcn.hispace.dbankcloud.cn",
|
||||
"CN_back": "connect-ws-drcn.hispace.dbankcloud.com",
|
||||
"DE": "connect-ws-dre.hispace.dbankcloud.cn",
|
||||
"DE_back": "connect-ws-dre.hispace.dbankcloud.com",
|
||||
"RU": "connect-ws-drru.hispace.dbankcloud.ru",
|
||||
"RU_back": "connect-ws-drru.hispace.dbankcloud.cn",
|
||||
"SG": "connect-ws-dra.hispace.dbankcloud.cn",
|
||||
"SG_back": "connect-ws-dra.hispace.dbankcloud.com"
|
||||
},
|
||||
"client": {
|
||||
"cp_id": "890048000024105546",
|
||||
"product_id": "736430079244736562",
|
||||
"client_id": "514530959291319360",
|
||||
"client_secret": "C42522DBF17D3D4BBE9D9C1783A54484B7E6844B388B7A67502D36A633A4186B",
|
||||
"project_id": "736430079244736562",
|
||||
"app_id": "106552551",
|
||||
"api_key": "CgB6e3x9BUNiq+r8ebCHNojjjYsMT4pJSjjNDOkm9owtBb6rVI6LjnASoZBRxbjjhObcrV5gANo99fI/eKZDTbWS",
|
||||
"package_name": "io.github.wulkanowy.dev"
|
||||
},
|
||||
"oauth_client": {
|
||||
"client_id": "106552551",
|
||||
"client_type": 1
|
||||
},
|
||||
"app_info": {
|
||||
"app_id": "106552551",
|
||||
"package_name": "io.github.wulkanowy.dev"
|
||||
},
|
||||
"service": {
|
||||
"analytics": {
|
||||
"collector_url": "datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn",
|
||||
"collector_url_ru": "datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com",
|
||||
"collector_url_sg": "datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn",
|
||||
"collector_url_de": "datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn",
|
||||
"collector_url_cn": "datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn",
|
||||
"resource_id": "p1",
|
||||
"channel_id": ""
|
||||
},
|
||||
"search":{
|
||||
"url":"https://search-dre.cloud.huawei.com"
|
||||
},
|
||||
"cloudstorage": {
|
||||
"storage_url_sg_back": "https://agc-storage-dra.cloud.huawei.asia",
|
||||
"storage_url_ru_back": "https://agc-storage-drru.cloud.huawei.ru",
|
||||
"storage_url_ru": "https://agc-storage-drru.cloud.huawei.ru",
|
||||
"storage_url_de_back": "https://agc-storage-dre.cloud.huawei.eu",
|
||||
"storage_url_de": "https://ops-dre.agcstorage.link",
|
||||
"storage_url": "https://agc-storage-drcn.platform.dbankcloud.cn",
|
||||
"storage_url_sg": "https://ops-dra.agcstorage.link",
|
||||
"storage_url_cn_back": "https://agc-storage-drcn.cloud.huawei.com.cn",
|
||||
"storage_url_cn": "https://agc-storage-drcn.platform.dbankcloud.cn"
|
||||
},
|
||||
"ml": {
|
||||
"mlservice_url": "ml-api-dre.ai.dbankcloud.com,ml-api-dre.ai.dbankcloud.cn"
|
||||
}
|
||||
},
|
||||
"region": "DE",
|
||||
"configuration_version": "3.0",
|
||||
"appInfos": [
|
||||
{
|
||||
"package_name": "io.github.wulkanowy.dev",
|
||||
"client": {
|
||||
"app_id": "106552551"
|
||||
},
|
||||
"app_info": {
|
||||
"package_name": "io.github.wulkanowy.dev",
|
||||
"app_id": "106552551"
|
||||
},
|
||||
"oauth_client": {
|
||||
"client_type": 1,
|
||||
"client_id": "106552551"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -155,33 +155,9 @@
|
||||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
|
||||
<!-- workaround for https://github.com/firebase/firebase-android-sdk/issues/473 enabled:false -->
|
||||
<!-- https://firebase.googleblog.com/2017/03/take-control-of-your-firebase-init-on.html -->
|
||||
<provider
|
||||
android:name="com.google.firebase.provider.FirebaseInitProvider"
|
||||
android:authorities="${applicationId}.firebaseinitprovider"
|
||||
android:enabled="${firebase_enabled}"
|
||||
android:exported="false"
|
||||
tools:ignore="MissingClass" />
|
||||
|
||||
<meta-data
|
||||
android:name="install_channel"
|
||||
android:value="${install_channel}" />
|
||||
<meta-data
|
||||
android:name="firebase_analytics_collection_enabled"
|
||||
android:value="${firebase_enabled}" />
|
||||
<meta-data
|
||||
android:name="google_analytics_adid_collection_enabled"
|
||||
android:value="${firebase_enabled}" />
|
||||
<meta-data
|
||||
android:name="firebase_crashlytics_collection_enabled"
|
||||
android:value="${firebase_enabled}" />
|
||||
<meta-data
|
||||
android:name="firebase_messaging_auto_init_enabled"
|
||||
android:value="${firebase_enabled}" />
|
||||
<meta-data
|
||||
android:name="firebase_inapp_messaging_auto_data_collection_enabled"
|
||||
android:value="${firebase_enabled}" />
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_icon"
|
||||
android:resource="@drawable/ic_stat_all" />
|
||||
|
@ -18,17 +18,13 @@ import io.github.wulkanowy.data.api.SchoolsService
|
||||
import io.github.wulkanowy.data.db.AppDatabase
|
||||
import io.github.wulkanowy.data.db.SharedPrefProvider
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.AppInfo
|
||||
import io.github.wulkanowy.utils.RemoteConfigHelper
|
||||
import io.github.wulkanowy.utils.WebkitCookieManagerProxy
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.create
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Singleton
|
||||
|
||||
@ -36,20 +32,6 @@ import javax.inject.Singleton
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal class DataModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideSdk(chuckerInterceptor: ChuckerInterceptor, remoteConfig: RemoteConfigHelper) =
|
||||
Sdk().apply {
|
||||
androidVersion = android.os.Build.VERSION.RELEASE
|
||||
buildTag = android.os.Build.MODEL
|
||||
userAgentTemplate = remoteConfig.userAgentTemplate
|
||||
setSimpleHttpLogger { Timber.d(it) }
|
||||
setAdditionalCookieManager(WebkitCookieManagerProxy())
|
||||
|
||||
// for debug only
|
||||
addInterceptor(chuckerInterceptor, network = true)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideChuckerCollector(
|
||||
|
@ -1,11 +1,17 @@
|
||||
package io.github.wulkanowy.data
|
||||
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterNot
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
@ -14,16 +20,39 @@ import kotlinx.coroutines.flow.takeWhile
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import timber.log.Timber
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
sealed class Resource<T> {
|
||||
|
||||
open class Loading<T> : Resource<T>()
|
||||
sealed interface Resource<out T> {
|
||||
/**
|
||||
* The initial value of a resource flow. Indicates no data that is currently available to be shown,
|
||||
* however with the expectation that the state will transition to another one soon.
|
||||
*/
|
||||
open class Loading<T> : Resource<T>
|
||||
|
||||
/**
|
||||
* A semi-loading state with some data available to be displayed (usually cached data loaded from
|
||||
* the database). Still not the target state and it's expected to transition into another one soon.
|
||||
*/
|
||||
data class Intermediate<T>(val data: T) : Loading<T>()
|
||||
|
||||
data class Success<T>(val data: T) : Resource<T>()
|
||||
/**
|
||||
* The happy-path target state. Data can either be:
|
||||
* - loaded from the database - while it may seem like this case is already handled by the
|
||||
* Intermediate state, the difference here is semantic. Cached data is returned as Intermediate
|
||||
* when there's a API request in progress (or soon expected to be), however when there is no
|
||||
* intention of immediately querying the API, the cached data is returned as a Success.
|
||||
* - fetched from the API.
|
||||
*/
|
||||
data class Success<T>(val data: T) : Resource<T>
|
||||
|
||||
data class Error<T>(val error: Throwable) : Resource<T>()
|
||||
/**
|
||||
* Something bad happened and we were unable to get the requested data. This can be caused by
|
||||
* a database error, a network error, or really just any other error. Upon receiving this state
|
||||
* the UI can either: display a full screen error, or, when it has received any data previously,
|
||||
* display a snack bar informing of the problem.
|
||||
*/
|
||||
data class Error<T>(val error: Throwable) : Resource<T>
|
||||
}
|
||||
|
||||
val <T> Resource<T>.dataOrNull: T?
|
||||
@ -64,6 +93,22 @@ fun <T, U> Resource<T>.mapData(block: (T) -> U) = when (this) {
|
||||
is Resource.Error -> Resource.Error(this.error)
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects another flow into this flow's resource data.
|
||||
*/
|
||||
inline fun <T1, T2, R> Flow<Resource<T1>>.combineWithResourceData(
|
||||
flow: Flow<T2>,
|
||||
crossinline block: suspend (T1, T2) -> R
|
||||
): Flow<Resource<R>> =
|
||||
combine(flow) { resource, inject ->
|
||||
when (resource) {
|
||||
is Resource.Success -> Resource.Success(block(resource.data, inject))
|
||||
is Resource.Intermediate -> Resource.Intermediate(block(resource.data, inject))
|
||||
is Resource.Loading -> Resource.Loading()
|
||||
is Resource.Error -> Resource.Error(resource.error)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Flow<Resource<T>>.logResourceStatus(name: String, showData: Boolean = false) = onEach {
|
||||
val description = when (it) {
|
||||
is Resource.Intermediate -> "intermediate data received" + if (showData) " (data: `${it.data}`)" else ""
|
||||
@ -74,8 +119,29 @@ fun <T> Flow<Resource<T>>.logResourceStatus(name: String, showData: Boolean = fa
|
||||
Timber.i("$name: $description")
|
||||
}
|
||||
|
||||
fun <T, U> Flow<Resource<T>>.mapResourceData(block: (T) -> U) = map {
|
||||
it.mapData(block)
|
||||
inline fun <T, U> Flow<Resource<T>>.mapResourceData(crossinline block: suspend (T) -> U) = map {
|
||||
when (it) {
|
||||
is Resource.Success -> Resource.Success(block(it.data))
|
||||
is Resource.Intermediate -> Resource.Intermediate(block(it.data))
|
||||
is Resource.Loading -> Resource.Loading()
|
||||
is Resource.Error -> Resource.Error(it.error)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun <T, U> Flow<Resource<T>>.flatMapResourceData(
|
||||
inheritIntermediate: Boolean = true, block: suspend (T) -> Flow<Resource<U>>
|
||||
) = flatMapLatest {
|
||||
when (it) {
|
||||
is Resource.Success -> block(it.data)
|
||||
is Resource.Intermediate -> block(it.data).map { newRes ->
|
||||
if (inheritIntermediate && newRes is Resource.Success) Resource.Intermediate(newRes.data)
|
||||
else newRes
|
||||
}
|
||||
|
||||
is Resource.Loading -> flowOf(Resource.Loading())
|
||||
is Resource.Error -> flowOf(Resource.Error(it.error))
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Flow<Resource<T>>.onResourceData(block: suspend (T) -> Unit) = onEach {
|
||||
@ -105,13 +171,13 @@ fun <T> Flow<Resource<T>>.onResourceSuccess(block: suspend (T) -> Unit) = onEach
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Flow<Resource<T>>.onResourceError(block: (Throwable) -> Unit) = onEach {
|
||||
fun <T> Flow<Resource<T>>.onResourceError(block: suspend (Throwable) -> Unit) = onEach {
|
||||
if (it is Resource.Error) {
|
||||
block(it.error)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Flow<Resource<T>>.onResourceNotLoading(block: () -> Unit) = onEach {
|
||||
fun <T> Flow<Resource<T>>.onResourceNotLoading(block: suspend () -> Unit) = onEach {
|
||||
if (it !is Resource.Loading) {
|
||||
block()
|
||||
}
|
||||
@ -121,70 +187,99 @@ suspend fun <T> Flow<Resource<T>>.toFirstResult() = filter { it !is Resource.Loa
|
||||
|
||||
suspend fun <T> Flow<Resource<T>>.waitForResult() = takeWhile { it is Resource.Loading }.collect()
|
||||
|
||||
inline fun <ResultType, RequestType> networkBoundResource(
|
||||
mutex: Mutex = Mutex(),
|
||||
showSavedOnLoading: Boolean = true,
|
||||
crossinline isResultEmpty: (ResultType) -> Boolean,
|
||||
crossinline query: () -> Flow<ResultType>,
|
||||
crossinline fetch: suspend (ResultType) -> RequestType,
|
||||
crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit,
|
||||
crossinline onFetchFailed: (Throwable) -> Unit = { },
|
||||
crossinline shouldFetch: (ResultType) -> Boolean = { true },
|
||||
crossinline filterResult: (ResultType) -> ResultType = { it }
|
||||
) = flow {
|
||||
emit(Resource.Loading())
|
||||
// Can cause excessive amounts of `Resource.Intermediate` to be emitted. Unless that is desired,
|
||||
// use `debounceIntermediates` to alleviate this behavior.
|
||||
inline fun <reified T> combineResourceFlows(flows: Iterable<Flow<Resource<T>>>): Flow<Resource<List<T>>> =
|
||||
combine(flows) { items ->
|
||||
var isIntermediate = false
|
||||
val data = mutableListOf<T>()
|
||||
for (item in items) {
|
||||
when (item) {
|
||||
is Resource.Success -> data.add(item.data)
|
||||
is Resource.Intermediate -> {
|
||||
isIntermediate = true
|
||||
data.add(item.data)
|
||||
}
|
||||
|
||||
val data = query().first()
|
||||
emitAll(if (shouldFetch(data)) {
|
||||
val filteredResult = filterResult(data)
|
||||
|
||||
if (showSavedOnLoading && !isResultEmpty(filteredResult)) {
|
||||
emit(Resource.Intermediate(filteredResult))
|
||||
is Resource.Loading -> return@combine Resource.Loading()
|
||||
is Resource.Error -> continue
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
val newData = fetch(data)
|
||||
mutex.withLock { saveFetchResult(query().first(), newData) }
|
||||
query().map { Resource.Success(filterResult(it)) }
|
||||
} catch (throwable: Throwable) {
|
||||
onFetchFailed(throwable)
|
||||
flowOf(Resource.Error(throwable))
|
||||
if (data.isEmpty()) {
|
||||
// All items have to be errors for this to happen, so just return the first one.
|
||||
// mapData is functionally useless and exists only to satisfy the type checker
|
||||
items.first().mapData { listOf(it) }
|
||||
} else if (isIntermediate) {
|
||||
Resource.Intermediate(data)
|
||||
} else {
|
||||
Resource.Success(data)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
fun <T> Flow<Resource<T>>.debounceIntermediates(timeout: Duration = 5.seconds) = flow {
|
||||
var wasIntermediate = false
|
||||
|
||||
emitAll(this@debounceIntermediates.debounce {
|
||||
if (it is Resource.Intermediate) {
|
||||
if (!wasIntermediate) {
|
||||
wasIntermediate = true
|
||||
Duration.ZERO
|
||||
} else {
|
||||
timeout
|
||||
}
|
||||
} else {
|
||||
wasIntermediate = false
|
||||
Duration.ZERO
|
||||
}
|
||||
} else {
|
||||
query().map { Resource.Success(filterResult(it)) }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
inline fun <OutputType, ApiType> networkBoundResource(
|
||||
mutex: Mutex = Mutex(),
|
||||
crossinline isResultEmpty: (OutputType) -> Boolean,
|
||||
crossinline query: () -> Flow<OutputType>,
|
||||
crossinline fetch: suspend () -> ApiType,
|
||||
crossinline saveFetchResult: suspend (old: OutputType, new: ApiType) -> Unit,
|
||||
crossinline shouldFetch: (OutputType) -> Boolean = { true },
|
||||
crossinline filterResult: (OutputType) -> OutputType = { it }
|
||||
) = networkBoundResource(
|
||||
mutex = mutex,
|
||||
isResultEmpty = isResultEmpty,
|
||||
query = query,
|
||||
fetch = fetch,
|
||||
saveFetchResult = saveFetchResult,
|
||||
shouldFetch = shouldFetch,
|
||||
mapResult = filterResult
|
||||
)
|
||||
|
||||
@JvmName("networkBoundResourceWithMap")
|
||||
inline fun <ResultType, RequestType, T> networkBoundResource(
|
||||
inline fun <DatabaseType, ApiType, OutputType> networkBoundResource(
|
||||
mutex: Mutex = Mutex(),
|
||||
showSavedOnLoading: Boolean = true,
|
||||
crossinline isResultEmpty: (T) -> Boolean,
|
||||
crossinline query: () -> Flow<ResultType>,
|
||||
crossinline fetch: suspend (ResultType) -> RequestType,
|
||||
crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit,
|
||||
crossinline onFetchFailed: (Throwable) -> Unit = { },
|
||||
crossinline shouldFetch: (ResultType) -> Boolean = { true },
|
||||
crossinline mapResult: (ResultType) -> T,
|
||||
crossinline isResultEmpty: (OutputType) -> Boolean,
|
||||
crossinline query: () -> Flow<DatabaseType>,
|
||||
crossinline fetch: suspend () -> ApiType,
|
||||
crossinline saveFetchResult: suspend (old: DatabaseType, new: ApiType) -> Unit,
|
||||
crossinline shouldFetch: (DatabaseType) -> Boolean = { true },
|
||||
crossinline mapResult: (DatabaseType) -> OutputType,
|
||||
) = flow {
|
||||
emit(Resource.Loading())
|
||||
|
||||
val data = query().first()
|
||||
emitAll(if (shouldFetch(data)) {
|
||||
val mappedResult = mapResult(data)
|
||||
if (shouldFetch(data)) {
|
||||
emit(Resource.Intermediate(data))
|
||||
|
||||
if (showSavedOnLoading && !isResultEmpty(mappedResult)) {
|
||||
emit(Resource.Intermediate(mappedResult))
|
||||
}
|
||||
try {
|
||||
val newData = fetch(data)
|
||||
val newData = fetch()
|
||||
mutex.withLock { saveFetchResult(query().first(), newData) }
|
||||
query().map { Resource.Success(mapResult(it)) }
|
||||
} catch (throwable: Throwable) {
|
||||
onFetchFailed(throwable)
|
||||
flowOf(Resource.Error(throwable))
|
||||
emit(Resource.Error(throwable))
|
||||
return@flow
|
||||
}
|
||||
} else {
|
||||
query().map { Resource.Success(mapResult(it)) }
|
||||
})
|
||||
}
|
||||
|
||||
emitAll(query().map { Resource.Success(it) })
|
||||
}
|
||||
.mapResourceData { mapResult(it) }
|
||||
.filterNot { it is Resource.Intermediate && isResultEmpty(it.data) }
|
||||
|
@ -0,0 +1,65 @@
|
||||
package io.github.wulkanowy.data
|
||||
|
||||
import com.chuckerteam.chucker.api.ChuckerInterceptor
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.RemoteConfigHelper
|
||||
import io.github.wulkanowy.utils.WebkitCookieManagerProxy
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class WulkanowySdkFactory @Inject constructor(
|
||||
private val chuckerInterceptor: ChuckerInterceptor,
|
||||
private val remoteConfig: RemoteConfigHelper,
|
||||
private val webkitCookieManagerProxy: WebkitCookieManagerProxy
|
||||
) {
|
||||
|
||||
private val sdk = Sdk().apply {
|
||||
androidVersion = android.os.Build.VERSION.RELEASE
|
||||
buildTag = android.os.Build.MODEL
|
||||
userAgentTemplate = remoteConfig.userAgentTemplate
|
||||
setSimpleHttpLogger { Timber.d(it) }
|
||||
setAdditionalCookieManager(webkitCookieManagerProxy)
|
||||
|
||||
// for debug only
|
||||
addInterceptor(chuckerInterceptor, network = true)
|
||||
}
|
||||
|
||||
fun create() = sdk
|
||||
|
||||
fun create(student: Student, semester: Semester? = null): Sdk {
|
||||
return create().apply {
|
||||
email = student.email
|
||||
password = student.password
|
||||
symbol = student.symbol
|
||||
schoolSymbol = student.schoolSymbol
|
||||
studentId = student.studentId
|
||||
classId = student.classId
|
||||
isEduOne = student.isEduOne
|
||||
emptyCookieJarInterceptor = true
|
||||
|
||||
if (Sdk.Mode.valueOf(student.loginMode) == Sdk.Mode.HEBE) {
|
||||
mobileBaseUrl = student.mobileBaseUrl
|
||||
} else {
|
||||
scrapperBaseUrl = student.scrapperBaseUrl
|
||||
domainSuffix = student.scrapperDomainSuffix
|
||||
loginType = Sdk.ScrapperLoginType.valueOf(student.loginType)
|
||||
}
|
||||
|
||||
mode = Sdk.Mode.valueOf(student.loginMode)
|
||||
mobileBaseUrl = student.mobileBaseUrl
|
||||
keyId = student.certificateKey
|
||||
privatePem = student.privateKey
|
||||
|
||||
if (semester != null) {
|
||||
diaryId = semester.diaryId
|
||||
kindergartenDiaryId = semester.kindergartenDiaryId
|
||||
schoolYear = semester.schoolYear
|
||||
unitId = semester.unitId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -173,6 +173,8 @@ import javax.inject.Singleton
|
||||
AutoMigration(from = 57, to = 58, spec = Migration58::class),
|
||||
AutoMigration(from = 58, to = 59),
|
||||
AutoMigration(from = 59, to = 60),
|
||||
AutoMigration(from = 60, to = 61),
|
||||
AutoMigration(from = 61, to = 62),
|
||||
],
|
||||
version = AppDatabase.VERSION_SCHEMA,
|
||||
exportSchema = true
|
||||
@ -181,7 +183,7 @@ import javax.inject.Singleton
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
|
||||
companion object {
|
||||
const val VERSION_SCHEMA = 60
|
||||
const val VERSION_SCHEMA = 62
|
||||
|
||||
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
|
||||
Migration2(),
|
||||
@ -308,6 +310,6 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
abstract val adminMessagesDao: AdminMessageDao
|
||||
|
||||
abstract val mutedMessageSendersDao: MutedMessageSendersDao
|
||||
|
||||
|
||||
abstract val gradeDescriptiveDao: GradeDescriptiveDao
|
||||
}
|
||||
|
@ -2,24 +2,14 @@ package io.github.wulkanowy.data.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import io.github.wulkanowy.data.db.entities.AdminMessage
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
@Dao
|
||||
abstract class AdminMessageDao : BaseDao<AdminMessage> {
|
||||
interface AdminMessageDao : BaseDao<AdminMessage> {
|
||||
|
||||
@Query("SELECT * FROM AdminMessages")
|
||||
abstract fun loadAll(): Flow<List<AdminMessage>>
|
||||
|
||||
@Transaction
|
||||
open suspend fun removeOldAndSaveNew(
|
||||
oldMessages: List<AdminMessage>,
|
||||
newMessages: List<AdminMessage>
|
||||
) {
|
||||
deleteAll(oldMessages)
|
||||
insertAll(newMessages)
|
||||
}
|
||||
fun loadAll(): Flow<List<AdminMessage>>
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package io.github.wulkanowy.data.db.dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
|
||||
interface BaseDao<T> {
|
||||
@ -15,4 +16,10 @@ interface BaseDao<T> {
|
||||
|
||||
@Delete
|
||||
suspend fun deleteAll(items: List<T>)
|
||||
|
||||
@Transaction
|
||||
suspend fun removeOldAndSaveNew(oldItems: List<T>, newItems: List<T>) {
|
||||
deleteAll(oldItems)
|
||||
insertAll(newItems)
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.db.entities.StudentIsAuthorized
|
||||
import io.github.wulkanowy.data.db.entities.StudentName
|
||||
import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
|
||||
import javax.inject.Singleton
|
||||
@ -23,6 +24,9 @@ abstract class StudentDao {
|
||||
@Delete
|
||||
abstract suspend fun delete(student: Student)
|
||||
|
||||
@Update(entity = Student::class)
|
||||
abstract suspend fun update(studentIsAuthorized: StudentIsAuthorized)
|
||||
|
||||
@Update(entity = Student::class)
|
||||
abstract suspend fun update(studentNickAndAvatar: StudentNickAndAvatar)
|
||||
|
||||
|
@ -16,7 +16,9 @@ data class SchoolAnnouncement(
|
||||
|
||||
val subject: String,
|
||||
|
||||
val content: String
|
||||
val content: String,
|
||||
|
||||
val author: String? = null,
|
||||
) : Serializable {
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
|
@ -78,6 +78,13 @@ data class Student(
|
||||
|
||||
@ColumnInfo(name = "registration_date")
|
||||
val registrationDate: Instant,
|
||||
|
||||
@ColumnInfo(name = "is_authorized", defaultValue = "0")
|
||||
val isAuthorized: Boolean,
|
||||
|
||||
@ColumnInfo(name = "is_edu_one", defaultValue = "0")
|
||||
val isEduOne: Boolean,
|
||||
|
||||
) : Serializable {
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
|
@ -0,0 +1,16 @@
|
||||
package io.github.wulkanowy.data.db.entities
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import java.io.Serializable
|
||||
|
||||
@Entity
|
||||
data class StudentIsAuthorized(
|
||||
@ColumnInfo(name = "is_authorized", defaultValue = "0")
|
||||
val isAuthorized: Boolean,
|
||||
) : Serializable {
|
||||
|
||||
@PrimaryKey
|
||||
var id: Long = 0
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package io.github.wulkanowy.data.enums
|
||||
|
||||
enum class AttendanceCalculatorSortingMode(private val value: String) {
|
||||
ALPHABETIC("alphabetic"),
|
||||
ATTENDANCE("attendance_percentage"),
|
||||
LESSON_BALANCE("lesson_balance");
|
||||
|
||||
companion object {
|
||||
fun getByValue(value: String) =
|
||||
AttendanceCalculatorSortingMode.values()
|
||||
.find { it.value == value } ?: ALPHABETIC
|
||||
}
|
||||
}
|
@ -3,5 +3,10 @@ package io.github.wulkanowy.data.enums
|
||||
enum class MessageFolder(val id: Int = 1) {
|
||||
RECEIVED(1),
|
||||
SENT(2),
|
||||
TRASHED(3)
|
||||
TRASHED(3),
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun byId(id: Int) = entries.first { it.id == id }
|
||||
}
|
||||
}
|
||||
|
@ -3,12 +3,26 @@ package io.github.wulkanowy.data.mappers
|
||||
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.sdk.pojo.DirectorInformation as SdkDirectorInformation
|
||||
import io.github.wulkanowy.sdk.pojo.LastAnnouncement as SdkLastAnnouncement
|
||||
|
||||
@JvmName("mapDirectorInformationToEntities")
|
||||
fun List<SdkDirectorInformation>.mapToEntities(student: Student) = map {
|
||||
SchoolAnnouncement(
|
||||
userLoginId = student.userLoginId,
|
||||
date = it.date,
|
||||
subject = it.subject,
|
||||
content = it.content,
|
||||
author = null,
|
||||
)
|
||||
}
|
||||
|
||||
@JvmName("mapLastAnnouncementsToEntities")
|
||||
fun List<SdkLastAnnouncement>.mapToEntities(student: Student) = map {
|
||||
SchoolAnnouncement(
|
||||
userLoginId = student.userLoginId,
|
||||
date = it.date,
|
||||
subject = it.subject,
|
||||
content = it.content,
|
||||
author = it.author,
|
||||
)
|
||||
}
|
||||
|
@ -34,17 +34,19 @@ fun SdkRegisterUser.mapToPojo(password: String?) = RegisterUser(
|
||||
error = it.error,
|
||||
students = it.subjects
|
||||
.filterIsInstance<SdkRegisterStudent>()
|
||||
.map { registerSubject ->
|
||||
.map { registerStudent ->
|
||||
RegisterStudent(
|
||||
studentId = registerSubject.studentId,
|
||||
studentName = registerSubject.studentName,
|
||||
studentSecondName = registerSubject.studentSecondName,
|
||||
studentSurname = registerSubject.studentSurname,
|
||||
className = registerSubject.className,
|
||||
classId = registerSubject.classId,
|
||||
isParent = registerSubject.isParent,
|
||||
semesters = registerSubject.semesters
|
||||
.mapToEntities(registerSubject.studentId),
|
||||
studentId = registerStudent.studentId,
|
||||
studentName = registerStudent.studentName,
|
||||
studentSecondName = registerStudent.studentSecondName,
|
||||
studentSurname = registerStudent.studentSurname,
|
||||
className = registerStudent.className,
|
||||
classId = registerStudent.classId,
|
||||
isParent = registerStudent.isParent,
|
||||
isAuthorized = registerStudent.isAuthorized,
|
||||
isEduOne = registerStudent.isEduOne,
|
||||
semesters = registerStudent.semesters
|
||||
.mapToEntities(registerStudent.studentId),
|
||||
)
|
||||
},
|
||||
)
|
||||
@ -84,6 +86,8 @@ fun RegisterStudent.mapToStudentWithSemesters(
|
||||
password = user.password.orEmpty(),
|
||||
isCurrent = false,
|
||||
registrationDate = Instant.now(),
|
||||
isAuthorized = this.isAuthorized,
|
||||
isEduOne = this.isEduOne,
|
||||
).apply {
|
||||
avatarColor = colors.random()
|
||||
},
|
||||
|
@ -0,0 +1,14 @@
|
||||
package io.github.wulkanowy.data.pojos
|
||||
|
||||
data class AttendanceData(
|
||||
val subjectName: String,
|
||||
val lessonBalance: Int,
|
||||
val presences: Int,
|
||||
val absences: Int,
|
||||
) {
|
||||
val total: Int
|
||||
get() = presences + absences
|
||||
|
||||
val presencePercentage: Double
|
||||
get() = if (total == 0) 0.0 else (presences.toDouble() / total) * 100
|
||||
}
|
@ -45,4 +45,6 @@ data class RegisterStudent(
|
||||
val classId: Int,
|
||||
val isParent: Boolean,
|
||||
val semesters: List<Semester>,
|
||||
val isAuthorized: Boolean,
|
||||
val isEduOne: Boolean
|
||||
) : java.io.Serializable
|
||||
|
@ -6,6 +6,7 @@ import io.github.wulkanowy.data.db.dao.AdminMessageDao
|
||||
import io.github.wulkanowy.data.db.entities.AdminMessage
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filterNot
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@ -28,6 +29,6 @@ class AdminMessageRepository @Inject constructor(
|
||||
saveFetchResult = { oldItems, newItems ->
|
||||
adminMessageDao.removeOldAndSaveNew(oldItems, newItems)
|
||||
},
|
||||
showSavedOnLoading = false,
|
||||
)
|
||||
.filterNot { it is Resource.Intermediate }
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.AttendanceDao
|
||||
import io.github.wulkanowy.data.db.dao.TimetableDao
|
||||
import io.github.wulkanowy.data.db.entities.Attendance
|
||||
@ -7,20 +8,15 @@ import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.sdk.pojo.Absent
|
||||
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.monday
|
||||
import io.github.wulkanowy.utils.sunday
|
||||
import io.github.wulkanowy.utils.switchSemester
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
@ -31,10 +27,11 @@ import javax.inject.Singleton
|
||||
class AttendanceRepository @Inject constructor(
|
||||
private val attendanceDb: AttendanceDao,
|
||||
private val timetableDb: TimetableDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
private val preferencesRepository: PreferencesRepository
|
||||
) {
|
||||
|
||||
private val saveFetchResultMutex = Mutex()
|
||||
|
||||
private val cacheKey = "attendance"
|
||||
@ -81,27 +78,23 @@ class AttendanceRepository @Inject constructor(
|
||||
.selectedHiddenSettingTiles
|
||||
.contains(DashboardItem.HiddenSettingTile.NOTES)
|
||||
|
||||
val lessons = withContext(Dispatchers.IO) {
|
||||
timetableDb.load(
|
||||
semester.diaryId, semester.studentId, start.monday, end.sunday
|
||||
)
|
||||
}
|
||||
val lessons = timetableDb.load(
|
||||
semester.diaryId, semester.studentId, start.monday, end.sunday
|
||||
)
|
||||
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getAttendance(start.monday, end.sunday)
|
||||
.filter { attendance ->
|
||||
!badAttendanceHidden ||
|
||||
(attendance.presence || attendance.excused)
|
||||
}.mapToEntities(semester, lessons)
|
||||
.filter { !badAttendanceHidden || (it.presence || it.excused) }
|
||||
.mapToEntities(semester, lessons)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
attendanceDb.deleteAll(old uniqueSubtract new)
|
||||
val attendanceToAdd = (new uniqueSubtract old).map { newAttendance ->
|
||||
newAttendance.apply { if (notify) isNotified = false }
|
||||
}
|
||||
attendanceDb.insertAll(attendanceToAdd)
|
||||
|
||||
attendanceDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = attendanceToAdd,
|
||||
)
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
|
||||
},
|
||||
filterResult = { it.filter { item -> item.date in start..end } }
|
||||
@ -128,8 +121,10 @@ class AttendanceRepository @Inject constructor(
|
||||
}
|
||||
|
||||
suspend fun excuseForAbsence(
|
||||
student: Student, semester: Semester,
|
||||
absenceList: List<Attendance>, reason: String? = null
|
||||
student: Student,
|
||||
semester: Semester,
|
||||
absenceList: List<Attendance>,
|
||||
reason: String? = null
|
||||
) {
|
||||
val items = absenceList.map { attendance ->
|
||||
Absent(
|
||||
@ -137,8 +132,7 @@ class AttendanceRepository @Inject constructor(
|
||||
timeId = attendance.timeId
|
||||
)
|
||||
}
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.excuseForAbsence(items, reason)
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import androidx.room.withTransaction
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.AppDatabase
|
||||
import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.switchSemester
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import javax.inject.Inject
|
||||
@ -18,8 +18,9 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class AttendanceSummaryRepository @Inject constructor(
|
||||
private val attendanceDb: AttendanceSummaryDao,
|
||||
private val sdk: Sdk,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
private val appDatabase: AppDatabase,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
) {
|
||||
|
||||
private val saveFetchResultMutex = Mutex()
|
||||
@ -40,14 +41,15 @@ class AttendanceSummaryRepository @Inject constructor(
|
||||
},
|
||||
query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, subjectId) },
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getAttendanceSummary(subjectId)
|
||||
.mapToEntities(semester, subjectId)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
attendanceDb.deleteAll(old uniqueSubtract new)
|
||||
attendanceDb.insertAll(new uniqueSubtract old)
|
||||
appDatabase.withTransaction {
|
||||
attendanceDb.deleteAll(old uniqueSubtract new)
|
||||
attendanceDb.insertAll(new uniqueSubtract old)
|
||||
}
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
|
||||
}
|
||||
)
|
||||
|
@ -1,12 +1,16 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.CompletedLessonsDao
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.*
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.monday
|
||||
import io.github.wulkanowy.utils.sunday
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import java.time.LocalDate
|
||||
import javax.inject.Inject
|
||||
@ -15,7 +19,7 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class CompletedLessonsRepository @Inject constructor(
|
||||
private val completedLessonsDb: CompletedLessonsDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
) {
|
||||
|
||||
@ -47,14 +51,15 @@ class CompletedLessonsRepository @Inject constructor(
|
||||
)
|
||||
},
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getCompletedLessons(start.monday, end.sunday)
|
||||
.mapToEntities(semester)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
completedLessonsDb.deleteAll(old uniqueSubtract new)
|
||||
completedLessonsDb.insertAll(new uniqueSubtract old)
|
||||
completedLessonsDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = new uniqueSubtract old,
|
||||
)
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
|
||||
},
|
||||
filterResult = { it.filter { item -> item.date in start..end } }
|
||||
|
@ -1,16 +1,14 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.ConferenceDao
|
||||
import io.github.wulkanowy.data.db.entities.Conference
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.switchSemester
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
@ -21,7 +19,7 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class ConferenceRepository @Inject constructor(
|
||||
private val conferenceDb: ConferenceDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
) {
|
||||
|
||||
@ -46,19 +44,18 @@ class ConferenceRepository @Inject constructor(
|
||||
conferenceDb.loadAll(semester.diaryId, student.studentId, startDate)
|
||||
},
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getConferences()
|
||||
.mapToEntities(semester)
|
||||
.filter { it.date >= startDate }
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
val conferencesToSave = (new uniqueSubtract old).onEach {
|
||||
if (notify) it.isNotified = false
|
||||
}
|
||||
|
||||
conferenceDb.deleteAll(old uniqueSubtract new)
|
||||
conferenceDb.insertAll(conferencesToSave)
|
||||
conferenceDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = (new uniqueSubtract old).onEach {
|
||||
if (notify) it.isNotified = false
|
||||
},
|
||||
)
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
|
||||
}
|
||||
)
|
||||
|
@ -1,18 +1,16 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.ExamDao
|
||||
import io.github.wulkanowy.data.db.entities.Exam
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.endExamsDay
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.startExamsDay
|
||||
import io.github.wulkanowy.utils.switchSemester
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
@ -23,7 +21,7 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class ExamRepository @Inject constructor(
|
||||
private val examDb: ExamDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
) {
|
||||
|
||||
@ -56,18 +54,17 @@ class ExamRepository @Inject constructor(
|
||||
)
|
||||
},
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getExams(start.startExamsDay, start.endExamsDay)
|
||||
.mapToEntities(semester)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
val examsToSave = (new uniqueSubtract old).onEach {
|
||||
if (notify) it.isNotified = false
|
||||
}
|
||||
|
||||
examDb.deleteAll(old uniqueSubtract new)
|
||||
examDb.insertAll(examsToSave)
|
||||
examDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = (new uniqueSubtract old).onEach {
|
||||
if (notify) it.isNotified = false
|
||||
},
|
||||
)
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
|
||||
},
|
||||
filterResult = { it.filter { item -> item.date in start..end } }
|
||||
|
@ -1,5 +1,6 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.GradeDao
|
||||
import io.github.wulkanowy.data.db.dao.GradeDescriptiveDao
|
||||
import io.github.wulkanowy.data.db.dao.GradeSummaryDao
|
||||
@ -10,12 +11,8 @@ import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.switchSemester
|
||||
import io.github.wulkanowy.utils.toLocalDate
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@ -32,10 +29,11 @@ class GradeRepository @Inject constructor(
|
||||
private val gradeDb: GradeDao,
|
||||
private val gradeSummaryDb: GradeSummaryDao,
|
||||
private val gradeDescriptiveDb: GradeDescriptiveDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
private val preferencesRepository: PreferencesRepository
|
||||
) {
|
||||
|
||||
private val saveFetchResultMutex = Mutex()
|
||||
|
||||
private fun loadGrades(semesterId: Int, studentId: Int): Flow<List<Grade>> {
|
||||
@ -62,7 +60,7 @@ class GradeRepository @Inject constructor(
|
||||
details.isEmpty() || (summaries.isEmpty() && descriptive.isEmpty()) || forceRefresh || isExpired
|
||||
},
|
||||
query = {
|
||||
val detailsFlow = loadGrades(semester.semesterId, semester.studentId)
|
||||
val detailsFlow = gradeDb.loadAll(semester.semesterId, semester.studentId)
|
||||
val summaryFlow = gradeSummaryDb.loadAll(semester.semesterId, semester.studentId)
|
||||
val descriptiveFlow =
|
||||
gradeDescriptiveDb.loadAll(semester.semesterId, semester.studentId)
|
||||
@ -74,8 +72,7 @@ class GradeRepository @Inject constructor(
|
||||
fetch = {
|
||||
val hiddenGrades = preferencesRepository.hiddenGrades
|
||||
|
||||
val (details, summary, descriptive) = sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
val (details, summary, descriptive) = wulkanowySdkFactory.create(student, semester)
|
||||
.getGrades(semester.semesterId)
|
||||
|
||||
val censoredDetails = details.filterNot { it.entry in hiddenGrades }
|
||||
@ -100,10 +97,12 @@ class GradeRepository @Inject constructor(
|
||||
new: List<GradeDescriptive>,
|
||||
notify: Boolean
|
||||
) {
|
||||
gradeDescriptiveDb.deleteAll(old uniqueSubtract new)
|
||||
gradeDescriptiveDb.insertAll((new uniqueSubtract old).onEach {
|
||||
if (notify) it.isNotified = false
|
||||
})
|
||||
gradeDescriptiveDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = (new uniqueSubtract old).onEach {
|
||||
if (notify) it.isNotified = false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun refreshGradeDetails(
|
||||
@ -114,13 +113,16 @@ class GradeRepository @Inject constructor(
|
||||
) {
|
||||
val notifyBreakDate = oldGrades.maxByOrNull { it.date }?.date
|
||||
?: student.registrationDate.toLocalDate()
|
||||
gradeDb.deleteAll(oldGrades uniqueSubtract newDetails)
|
||||
gradeDb.insertAll((newDetails uniqueSubtract oldGrades).onEach {
|
||||
if (it.date >= notifyBreakDate) it.apply {
|
||||
isRead = false
|
||||
if (notify) isNotified = false
|
||||
}
|
||||
})
|
||||
|
||||
gradeDb.removeOldAndSaveNew(
|
||||
oldItems = oldGrades uniqueSubtract newDetails,
|
||||
newItems = (newDetails uniqueSubtract oldGrades).onEach {
|
||||
if (it.date >= notifyBreakDate) it.apply {
|
||||
isRead = false
|
||||
if (notify) isNotified = false
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun refreshGradeSummaries(
|
||||
@ -128,31 +130,43 @@ class GradeRepository @Inject constructor(
|
||||
newSummary: List<GradeSummary>,
|
||||
notify: Boolean
|
||||
) {
|
||||
gradeSummaryDb.deleteAll(oldSummaries uniqueSubtract newSummary)
|
||||
gradeSummaryDb.insertAll((newSummary uniqueSubtract oldSummaries).onEach { summary ->
|
||||
val oldSummary = oldSummaries.find { old -> old.subject == summary.subject }
|
||||
summary.isPredictedGradeNotified = when {
|
||||
summary.predictedGrade.isEmpty() -> true
|
||||
notify && oldSummary?.predictedGrade != summary.predictedGrade -> false
|
||||
else -> true
|
||||
}
|
||||
summary.isFinalGradeNotified = when {
|
||||
summary.finalGrade.isEmpty() -> true
|
||||
notify && oldSummary?.finalGrade != summary.finalGrade -> false
|
||||
else -> true
|
||||
}
|
||||
gradeSummaryDb.removeOldAndSaveNew(
|
||||
oldItems = oldSummaries uniqueSubtract newSummary,
|
||||
newItems = (newSummary uniqueSubtract oldSummaries).onEach { summary ->
|
||||
getGradeSummaryWithUpdatedNotificationState(
|
||||
summary = summary,
|
||||
oldSummary = oldSummaries.find { it.subject == summary.subject },
|
||||
notify = notify,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
summary.predictedGradeLastChange = when {
|
||||
oldSummary == null -> Instant.now()
|
||||
summary.predictedGrade != oldSummary.predictedGrade -> Instant.now()
|
||||
else -> oldSummary.predictedGradeLastChange
|
||||
}
|
||||
summary.finalGradeLastChange = when {
|
||||
oldSummary == null -> Instant.now()
|
||||
summary.finalGrade != oldSummary.finalGrade -> Instant.now()
|
||||
else -> oldSummary.finalGradeLastChange
|
||||
}
|
||||
})
|
||||
private fun getGradeSummaryWithUpdatedNotificationState(
|
||||
summary: GradeSummary,
|
||||
oldSummary: GradeSummary?,
|
||||
notify: Boolean,
|
||||
) {
|
||||
summary.isPredictedGradeNotified = when {
|
||||
summary.predictedGrade.isEmpty() -> true
|
||||
notify && oldSummary?.predictedGrade != summary.predictedGrade -> false
|
||||
else -> true
|
||||
}
|
||||
summary.isFinalGradeNotified = when {
|
||||
summary.finalGrade.isEmpty() -> true
|
||||
notify && oldSummary?.finalGrade != summary.finalGrade -> false
|
||||
else -> true
|
||||
}
|
||||
summary.predictedGradeLastChange = when {
|
||||
oldSummary == null -> Instant.now()
|
||||
summary.predictedGrade != oldSummary.predictedGrade -> Instant.now()
|
||||
else -> oldSummary.predictedGradeLastChange
|
||||
}
|
||||
summary.finalGradeLastChange = when {
|
||||
oldSummary == null -> Instant.now()
|
||||
summary.finalGrade != oldSummary.finalGrade -> Instant.now()
|
||||
else -> oldSummary.finalGradeLastChange
|
||||
}
|
||||
}
|
||||
|
||||
fun getUnreadGrades(semester: Semester): Flow<List<Grade>> {
|
||||
|
@ -1,5 +1,6 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.GradePartialStatisticsDao
|
||||
import io.github.wulkanowy.data.db.dao.GradePointsStatisticsDao
|
||||
import io.github.wulkanowy.data.db.dao.GradeSemesterStatisticsDao
|
||||
@ -12,14 +13,11 @@ import io.github.wulkanowy.data.mappers.mapPointsToStatisticsItems
|
||||
import io.github.wulkanowy.data.mappers.mapSemesterToStatisticItems
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.switchSemester
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@ -28,7 +26,7 @@ class GradeStatisticsRepository @Inject constructor(
|
||||
private val gradePartialStatisticsDb: GradePartialStatisticsDao,
|
||||
private val gradePointsStatisticsDb: GradePointsStatisticsDao,
|
||||
private val gradeSemesterStatisticsDb: GradeSemesterStatisticsDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
) {
|
||||
|
||||
@ -56,14 +54,15 @@ class GradeStatisticsRepository @Inject constructor(
|
||||
},
|
||||
query = { gradePartialStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getGradesPartialStatistics(semester.semesterId)
|
||||
.mapToEntities(semester)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
gradePartialStatisticsDb.deleteAll(old uniqueSubtract new)
|
||||
gradePartialStatisticsDb.insertAll(new uniqueSubtract old)
|
||||
gradePartialStatisticsDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = new uniqueSubtract old,
|
||||
)
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(partialCacheKey, semester))
|
||||
},
|
||||
mapResult = { items ->
|
||||
@ -80,6 +79,7 @@ class GradeStatisticsRepository @Inject constructor(
|
||||
)
|
||||
listOf(summaryItem) + items
|
||||
}
|
||||
|
||||
else -> items.filter { it.subject == subjectName }
|
||||
}.mapPartialToStatisticItems()
|
||||
}
|
||||
@ -101,14 +101,15 @@ class GradeStatisticsRepository @Inject constructor(
|
||||
},
|
||||
query = { gradeSemesterStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getGradesSemesterStatistics(semester.semesterId)
|
||||
.mapToEntities(semester)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
gradeSemesterStatisticsDb.deleteAll(old uniqueSubtract new)
|
||||
gradeSemesterStatisticsDb.insertAll(new uniqueSubtract old)
|
||||
gradeSemesterStatisticsDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = new uniqueSubtract old,
|
||||
)
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(semesterCacheKey, semester))
|
||||
},
|
||||
mapResult = { items ->
|
||||
@ -138,6 +139,7 @@ class GradeStatisticsRepository @Inject constructor(
|
||||
}
|
||||
listOf(summaryItem) + itemsWithAverage
|
||||
}
|
||||
|
||||
else -> itemsWithAverage.filter { it.subject == subjectName }
|
||||
}.mapSemesterToStatisticItems()
|
||||
}
|
||||
@ -157,14 +159,15 @@ class GradeStatisticsRepository @Inject constructor(
|
||||
},
|
||||
query = { gradePointsStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getGradesPointsStatistics(semester.semesterId)
|
||||
.mapToEntities(semester)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
gradePointsStatisticsDb.deleteAll(old uniqueSubtract new)
|
||||
gradePointsStatisticsDb.insertAll(new uniqueSubtract old)
|
||||
gradePointsStatisticsDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = new uniqueSubtract old,
|
||||
)
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(pointsCacheKey, semester))
|
||||
},
|
||||
mapResult = { items ->
|
||||
|
@ -1,18 +1,16 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.HomeworkDao
|
||||
import io.github.wulkanowy.data.db.entities.Homework
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.monday
|
||||
import io.github.wulkanowy.utils.sunday
|
||||
import io.github.wulkanowy.utils.switchSemester
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import java.time.LocalDate
|
||||
@ -22,7 +20,7 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class HomeworkRepository @Inject constructor(
|
||||
private val homeworkDb: HomeworkDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
) {
|
||||
|
||||
@ -55,20 +53,19 @@ class HomeworkRepository @Inject constructor(
|
||||
)
|
||||
},
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getHomework(start.monday, end.sunday)
|
||||
.mapToEntities(semester)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
val homeWorkToSave = (new uniqueSubtract old).onEach {
|
||||
if (notify) it.isNotified = false
|
||||
}
|
||||
val filteredOld = old.filterNot { it.isAddedByUser }
|
||||
|
||||
homeworkDb.deleteAll(filteredOld uniqueSubtract new)
|
||||
homeworkDb.insertAll(homeWorkToSave)
|
||||
|
||||
homeworkDb.removeOldAndSaveNew(
|
||||
oldItems = filteredOld uniqueSubtract new,
|
||||
newItems = (new uniqueSubtract old).onEach {
|
||||
if (notify) it.isNotified = false
|
||||
},
|
||||
)
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
|
||||
}
|
||||
)
|
||||
|
@ -1,12 +1,13 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.LuckyNumberDao
|
||||
import io.github.wulkanowy.data.db.entities.LuckyNumber
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntity
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.ui.modules.luckynumberwidget.LuckyNumberWidgetProvider
|
||||
import io.github.wulkanowy.utils.AppWidgetUpdater
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
@ -18,7 +19,8 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class LuckyNumberRepository @Inject constructor(
|
||||
private val luckyNumberDb: LuckyNumberDao,
|
||||
private val sdk: Sdk
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val appWidgetUpdater: AppWidgetUpdater,
|
||||
) {
|
||||
|
||||
private val saveFetchResultMutex = Mutex()
|
||||
@ -27,23 +29,28 @@ class LuckyNumberRepository @Inject constructor(
|
||||
student: Student,
|
||||
forceRefresh: Boolean,
|
||||
notify: Boolean = false,
|
||||
isFromAppWidget: Boolean = false
|
||||
) = networkBoundResource(
|
||||
mutex = saveFetchResultMutex,
|
||||
isResultEmpty = { it == null },
|
||||
shouldFetch = { it == null || forceRefresh },
|
||||
query = { luckyNumberDb.load(student.studentId, now()) },
|
||||
fetch = {
|
||||
sdk.init(student).getLuckyNumber(student.schoolShortName)?.mapToEntity(student)
|
||||
wulkanowySdkFactory.create(student)
|
||||
.getLuckyNumber(student.schoolShortName)
|
||||
?.mapToEntity(student)
|
||||
},
|
||||
saveFetchResult = { oldLuckyNumber, newLuckyNumber ->
|
||||
newLuckyNumber ?: return@networkBoundResource
|
||||
|
||||
if (newLuckyNumber != oldLuckyNumber) {
|
||||
val updatedLuckNumberList =
|
||||
listOf(newLuckyNumber.apply { if (notify) isNotified = false })
|
||||
|
||||
oldLuckyNumber?.let { luckyNumberDb.deleteAll(listOfNotNull(it)) }
|
||||
luckyNumberDb.insertAll(updatedLuckNumberList)
|
||||
luckyNumberDb.removeOldAndSaveNew(
|
||||
oldItems = listOfNotNull(oldLuckyNumber),
|
||||
newItems = listOf(newLuckyNumber.apply { if (notify) isNotified = false }),
|
||||
)
|
||||
if (!isFromAppWidget) {
|
||||
appWidgetUpdater.updateAllAppWidgetsByProvider(LuckyNumberWidgetProvider::class)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -4,6 +4,7 @@ import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.Resource
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.SharedPrefProvider
|
||||
import io.github.wulkanowy.data.db.dao.MailboxDao
|
||||
import io.github.wulkanowy.data.db.dao.MessageAttachmentDao
|
||||
@ -18,6 +19,7 @@ import io.github.wulkanowy.data.db.entities.Recipient
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.enums.MessageFolder
|
||||
import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED
|
||||
import io.github.wulkanowy.data.enums.MessageFolder.SENT
|
||||
import io.github.wulkanowy.data.enums.MessageFolder.TRASHED
|
||||
import io.github.wulkanowy.data.mappers.mapFromEntities
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
@ -25,16 +27,14 @@ import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.data.onResourceError
|
||||
import io.github.wulkanowy.data.onResourceSuccess
|
||||
import io.github.wulkanowy.data.pojos.MessageDraft
|
||||
import io.github.wulkanowy.data.toFirstResult
|
||||
import io.github.wulkanowy.data.waitForResult
|
||||
import io.github.wulkanowy.domain.messages.GetMailboxByStudentUseCase
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.sdk.pojo.Folder
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
@ -47,7 +47,7 @@ class MessageRepository @Inject constructor(
|
||||
private val messagesDb: MessagesDao,
|
||||
private val mutedMessageSendersDao: MutedMessageSendersDao,
|
||||
private val messageAttachmentDao: MessageAttachmentDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
@ApplicationContext private val context: Context,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
private val sharedPrefProvider: SharedPrefProvider,
|
||||
@ -81,19 +81,26 @@ class MessageRepository @Inject constructor(
|
||||
} else messagesDb.loadMessagesWithMutedAuthor(mailbox.globalKey, folder.id)
|
||||
},
|
||||
fetch = {
|
||||
sdk.init(student).getMessages(
|
||||
folder = Folder.valueOf(folder.name),
|
||||
mailboxKey = mailbox?.globalKey,
|
||||
).mapToEntities(student, mailbox, mailboxDao.loadAll(student.email))
|
||||
wulkanowySdkFactory.create(student)
|
||||
.getMessages(
|
||||
folder = Folder.valueOf(folder.name),
|
||||
mailboxKey = mailbox?.globalKey,
|
||||
)
|
||||
.mapToEntities(
|
||||
student = student,
|
||||
mailbox = mailbox,
|
||||
allMailboxes = mailboxDao.loadAll(student.email)
|
||||
)
|
||||
},
|
||||
saveFetchResult = { oldWithAuthors, new ->
|
||||
val old = oldWithAuthors.map { it.message }
|
||||
messagesDb.deleteAll(old uniqueSubtract new)
|
||||
messagesDb.insertAll((new uniqueSubtract old).onEach {
|
||||
val muted = isMuted(it.correspondents)
|
||||
it.isNotified = !notify || muted
|
||||
})
|
||||
|
||||
messagesDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = (new uniqueSubtract old).onEach {
|
||||
val muted = isMuted(it.correspondents)
|
||||
it.isNotified = !notify || muted
|
||||
},
|
||||
)
|
||||
refreshHelper.updateLastRefreshTimestamp(
|
||||
getRefreshKey(messagesCacheKey, mailbox, folder)
|
||||
)
|
||||
@ -113,10 +120,11 @@ class MessageRepository @Inject constructor(
|
||||
},
|
||||
query = { messagesDb.loadMessageWithAttachment(message.messageGlobalKey) },
|
||||
fetch = {
|
||||
sdk.init(student).getMessageDetails(
|
||||
messageKey = it!!.message.messageGlobalKey,
|
||||
markAsRead = message.unread && markAsRead,
|
||||
)
|
||||
wulkanowySdkFactory.create(student)
|
||||
.getMessageDetails(
|
||||
messageKey = message.messageGlobalKey,
|
||||
markAsRead = message.unread && markAsRead,
|
||||
)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
checkNotNull(old) { "Fetched message no longer exist!" }
|
||||
@ -155,22 +163,36 @@ class MessageRepository @Inject constructor(
|
||||
subject: String,
|
||||
content: String,
|
||||
recipients: List<Recipient>,
|
||||
mailboxId: String,
|
||||
mailbox: Mailbox,
|
||||
) {
|
||||
sdk.init(student).sendMessage(
|
||||
subject = subject,
|
||||
content = content,
|
||||
recipients = recipients.mapFromEntities(),
|
||||
mailboxId = mailboxId,
|
||||
)
|
||||
wulkanowySdkFactory.create(student)
|
||||
.sendMessage(
|
||||
subject = subject,
|
||||
content = content,
|
||||
recipients = recipients.mapFromEntities(),
|
||||
mailboxId = mailbox.globalKey,
|
||||
)
|
||||
refreshFolders(student, mailbox, listOf(SENT))
|
||||
}
|
||||
|
||||
suspend fun deleteMessages(student: Student, mailbox: Mailbox?, messages: List<Message>) {
|
||||
suspend fun restoreMessages(student: Student, mailbox: Mailbox?, messages: List<Message>) {
|
||||
wulkanowySdkFactory.create(student)
|
||||
.restoreMessages(messages = messages.map { it.messageGlobalKey })
|
||||
|
||||
refreshFolders(student, mailbox)
|
||||
}
|
||||
|
||||
suspend fun deleteMessage(student: Student, message: Message) {
|
||||
deleteMessages(student, listOf(message))
|
||||
}
|
||||
|
||||
suspend fun deleteMessages(student: Student, messages: List<Message>) {
|
||||
val firstMessage = messages.first()
|
||||
sdk.init(student).deleteMessages(
|
||||
messages = messages.map { it.messageGlobalKey },
|
||||
removeForever = firstMessage.folderId == TRASHED.id,
|
||||
)
|
||||
wulkanowySdkFactory.create(student)
|
||||
.deleteMessages(
|
||||
messages = messages.map { it.messageGlobalKey },
|
||||
removeForever = firstMessage.folderId == TRASHED.id,
|
||||
)
|
||||
|
||||
if (firstMessage.folderId != TRASHED.id) {
|
||||
val deletedMessages = messages.map {
|
||||
@ -184,18 +206,24 @@ class MessageRepository @Inject constructor(
|
||||
}
|
||||
|
||||
messagesDb.updateAll(deletedMessages)
|
||||
} else messagesDb.deleteAll(messages)
|
||||
|
||||
getMessages(
|
||||
student = student,
|
||||
mailbox = mailbox,
|
||||
folder = TRASHED,
|
||||
forceRefresh = true,
|
||||
).first()
|
||||
} else {
|
||||
messagesDb.deleteAll(messages)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteMessage(student: Student, mailbox: Mailbox?, message: Message) {
|
||||
deleteMessages(student, mailbox, listOf(message))
|
||||
private suspend fun refreshFolders(
|
||||
student: Student,
|
||||
mailbox: Mailbox?,
|
||||
folders: List<MessageFolder> = MessageFolder.entries
|
||||
) {
|
||||
folders.forEach {
|
||||
getMessages(
|
||||
student = student,
|
||||
mailbox = mailbox,
|
||||
folder = it,
|
||||
forceRefresh = true,
|
||||
).toFirstResult()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getMailboxes(student: Student, forceRefresh: Boolean) = networkBoundResource(
|
||||
@ -209,7 +237,9 @@ class MessageRepository @Inject constructor(
|
||||
},
|
||||
query = { mailboxDao.loadAll(student.email, student.symbol, student.schoolSymbol) },
|
||||
fetch = {
|
||||
sdk.init(student).getMailboxes().mapToEntities(student)
|
||||
wulkanowySdkFactory.create(student)
|
||||
.getMailboxes()
|
||||
.mapToEntities(student)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
mailboxDao.deleteAll(old uniqueSubtract new)
|
||||
@ -240,7 +270,7 @@ class MessageRepository @Inject constructor(
|
||||
value?.let { json.encodeToString(it) }
|
||||
)
|
||||
|
||||
suspend fun isMuted(author: String): Boolean {
|
||||
private suspend fun isMuted(author: String): Boolean {
|
||||
return mutedMessageSendersDao.checkMute(author)
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.MobileDeviceDao
|
||||
import io.github.wulkanowy.data.db.entities.MobileDevice
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
@ -8,11 +9,8 @@ import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.mappers.mapToMobileDeviceToken
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.data.pojos.MobileDeviceToken
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.switchSemester
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import javax.inject.Inject
|
||||
@ -21,7 +19,7 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class MobileDeviceRepository @Inject constructor(
|
||||
private val mobileDb: MobileDeviceDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
) {
|
||||
|
||||
@ -42,30 +40,28 @@ class MobileDeviceRepository @Inject constructor(
|
||||
},
|
||||
query = { mobileDb.loadAll(student.userLoginId) },
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getRegisteredDevices()
|
||||
.mapToEntities(student)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
mobileDb.deleteAll(old uniqueSubtract new)
|
||||
mobileDb.insertAll(new uniqueSubtract old)
|
||||
|
||||
mobileDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = new uniqueSubtract old,
|
||||
)
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
|
||||
}
|
||||
)
|
||||
|
||||
suspend fun unregisterDevice(student: Student, semester: Semester, device: MobileDevice) {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.unregisterDevice(device.deviceId)
|
||||
|
||||
mobileDb.deleteAll(listOf(device))
|
||||
}
|
||||
|
||||
suspend fun getToken(student: Student, semester: Semester): MobileDeviceToken {
|
||||
return sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
return wulkanowySdkFactory.create(student, semester)
|
||||
.getToken()
|
||||
.mapToMobileDeviceToken()
|
||||
}
|
||||
|
@ -1,14 +1,17 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.NoteDao
|
||||
import io.github.wulkanowy.data.db.entities.Note
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
|
||||
import io.github.wulkanowy.utils.*
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.toLocalDate
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import javax.inject.Inject
|
||||
@ -17,10 +20,11 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class NoteRepository @Inject constructor(
|
||||
private val noteDb: NoteDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
private val preferencesRepository: PreferencesRepository
|
||||
) {
|
||||
|
||||
private val saveFetchResultMutex = Mutex()
|
||||
|
||||
private val cacheKey = "note"
|
||||
@ -55,21 +59,22 @@ class NoteRepository @Inject constructor(
|
||||
.selectedHiddenSettingTiles
|
||||
.contains(DashboardItem.HiddenSettingTile.NOTES)
|
||||
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getNotes()
|
||||
.filter { !notesHidden }
|
||||
.mapToEntities(semester)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
noteDb.deleteAll(old uniqueSubtract new)
|
||||
noteDb.insertAll((new uniqueSubtract old).onEach {
|
||||
val notesToAdd = (new uniqueSubtract old).onEach {
|
||||
if (it.date >= student.registrationDate.toLocalDate()) it.apply {
|
||||
isRead = false
|
||||
if (notify) isNotified = false
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
noteDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = notesToAdd,
|
||||
)
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
|
||||
}
|
||||
)
|
||||
|
@ -12,6 +12,7 @@ import com.fredporciuncula.flow.preferences.Serializer
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.enums.AppTheme
|
||||
import io.github.wulkanowy.data.enums.AttendanceCalculatorSortingMode
|
||||
import io.github.wulkanowy.data.enums.GradeColorTheme
|
||||
import io.github.wulkanowy.data.enums.GradeExpandMode
|
||||
import io.github.wulkanowy.data.enums.GradeSortingMode
|
||||
@ -43,6 +44,27 @@ class PreferencesRepository @Inject constructor(
|
||||
R.bool.pref_default_attendance_present
|
||||
)
|
||||
|
||||
val targetAttendanceFlow: Flow<Int>
|
||||
get() = flowSharedPref.getInt(
|
||||
context.getString(R.string.pref_key_attendance_target),
|
||||
context.resources.getInteger(R.integer.pref_default_attendance_target)
|
||||
).asFlow()
|
||||
|
||||
val attendanceCalculatorSortingModeFlow: Flow<AttendanceCalculatorSortingMode>
|
||||
get() = flowSharedPref.getString(
|
||||
context.getString(R.string.pref_key_attendance_calculator_sorting_mode),
|
||||
context.resources.getString(R.string.pref_default_attendance_calculator_sorting_mode)
|
||||
).asFlow().map(AttendanceCalculatorSortingMode::getByValue)
|
||||
|
||||
/**
|
||||
* Subjects are empty when they don't have any attendances (total = 0, attendances = 0, absences = 0).
|
||||
*/
|
||||
val attendanceCalculatorShowEmptySubjects: Flow<Boolean>
|
||||
get() = flowSharedPref.getBoolean(
|
||||
context.getString(R.string.pref_key_attendance_calculator_show_empty_subjects),
|
||||
context.resources.getBoolean(R.bool.pref_default_attendance_calculator_show_empty_subjects)
|
||||
).asFlow()
|
||||
|
||||
private val gradeAverageModePref: Preference<GradeAverageMode>
|
||||
get() = getObjectFlow(
|
||||
R.string.pref_key_grade_average_mode,
|
||||
|
@ -1,12 +1,15 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.RecipientDao
|
||||
import io.github.wulkanowy.data.db.entities.*
|
||||
import io.github.wulkanowy.data.db.entities.Mailbox
|
||||
import io.github.wulkanowy.data.db.entities.MailboxType
|
||||
import io.github.wulkanowy.data.db.entities.Message
|
||||
import io.github.wulkanowy.data.db.entities.Recipient
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@ -14,19 +17,22 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class RecipientRepository @Inject constructor(
|
||||
private val recipientDb: RecipientDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
) {
|
||||
|
||||
private val cacheKey = "recipient"
|
||||
|
||||
suspend fun refreshRecipients(student: Student, mailbox: Mailbox, type: MailboxType) {
|
||||
val new = sdk.init(student).getRecipients(mailbox.globalKey)
|
||||
val new = wulkanowySdkFactory.create(student)
|
||||
.getRecipients(mailbox.globalKey)
|
||||
.mapToEntities(mailbox.globalKey)
|
||||
val old = recipientDb.loadAll(type, mailbox.globalKey)
|
||||
|
||||
recipientDb.deleteAll(old uniqueSubtract new)
|
||||
recipientDb.insertAll(new uniqueSubtract old)
|
||||
recipientDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = new uniqueSubtract old,
|
||||
)
|
||||
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
|
||||
}
|
||||
@ -54,7 +60,7 @@ class RecipientRepository @Inject constructor(
|
||||
): List<Recipient> {
|
||||
mailbox ?: return emptyList()
|
||||
|
||||
return sdk.init(student)
|
||||
return wulkanowySdkFactory.create(student)
|
||||
.getMessageReplayDetails(message.messageGlobalKey)
|
||||
.sender
|
||||
.let(::listOf)
|
||||
|
@ -1,17 +1,23 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class RecoverRepository @Inject constructor(private val sdk: Sdk) {
|
||||
class RecoverRepository @Inject constructor(
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory
|
||||
) {
|
||||
|
||||
suspend fun getReCaptchaSiteKey(host: String, symbol: String): Pair<String, String> {
|
||||
return sdk.getPasswordResetCaptchaCode(host, symbol)
|
||||
}
|
||||
suspend fun getReCaptchaSiteKey(host: String, symbol: String): Pair<String, String> =
|
||||
wulkanowySdkFactory.create()
|
||||
.getPasswordResetCaptchaCode(host, symbol)
|
||||
|
||||
suspend fun sendRecoverRequest(
|
||||
url: String, symbol: String, email: String, reCaptchaResponse: String
|
||||
): String = sdk.sendPasswordResetRequest(url, symbol, email, reCaptchaResponse)
|
||||
url: String,
|
||||
symbol: String,
|
||||
email: String,
|
||||
reCaptchaResponse: String
|
||||
): String = wulkanowySdkFactory.create()
|
||||
.sendPasswordResetRequest(url, symbol, email, reCaptchaResponse)
|
||||
}
|
||||
|
@ -1,14 +1,13 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.SchoolAnnouncementDao
|
||||
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
@ -18,7 +17,7 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class SchoolAnnouncementRepository @Inject constructor(
|
||||
private val schoolAnnouncementDb: SchoolAnnouncementDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
) {
|
||||
|
||||
@ -41,17 +40,18 @@ class SchoolAnnouncementRepository @Inject constructor(
|
||||
schoolAnnouncementDb.loadAll(student.userLoginId)
|
||||
},
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.getDirectorInformation()
|
||||
.mapToEntities(student)
|
||||
val sdk = wulkanowySdkFactory.create(student)
|
||||
val lastAnnouncements = sdk.getLastAnnouncements().mapToEntities(student)
|
||||
val directorInformation = sdk.getDirectorInformation().mapToEntities(student)
|
||||
lastAnnouncements + directorInformation
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
val schoolAnnouncementsToSave = (new uniqueSubtract old).onEach {
|
||||
if (notify) it.isNotified = false
|
||||
}
|
||||
|
||||
schoolAnnouncementDb.deleteAll(old uniqueSubtract new)
|
||||
schoolAnnouncementDb.insertAll(schoolAnnouncementsToSave)
|
||||
schoolAnnouncementDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = (new uniqueSubtract old).onEach {
|
||||
if (notify) it.isNotified = false
|
||||
},
|
||||
)
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
|
||||
}
|
||||
)
|
||||
|
@ -1,15 +1,13 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.SchoolDao
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntity
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.switchSemester
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@ -17,7 +15,7 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class SchoolRepository @Inject constructor(
|
||||
private val schoolDb: SchoolDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
) {
|
||||
|
||||
@ -40,17 +38,16 @@ class SchoolRepository @Inject constructor(
|
||||
},
|
||||
query = { schoolDb.load(semester.studentId, semester.classId) },
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getSchool()
|
||||
.mapToEntity(semester)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
if (old != null && new != old) {
|
||||
with(schoolDb) {
|
||||
deleteAll(listOf(old))
|
||||
insertAll(listOf(new))
|
||||
}
|
||||
schoolDb.removeOldAndSaveNew(
|
||||
oldItems = listOf(old),
|
||||
newItems = listOf(new)
|
||||
)
|
||||
} else if (old == null) {
|
||||
schoolDb.insertAll(listOf(new))
|
||||
}
|
||||
|
@ -1,17 +1,15 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.api.SchoolsService
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
|
||||
import io.github.wulkanowy.data.pojos.IntegrityRequest
|
||||
import io.github.wulkanowy.data.pojos.LoginEvent
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.ui.modules.login.LoginData
|
||||
import io.github.wulkanowy.utils.IntegrityHelper
|
||||
import io.github.wulkanowy.utils.getCurrentOrLast
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.switchSemester
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
@ -23,7 +21,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||
class SchoolsRepository @Inject constructor(
|
||||
private val integrityHelper: IntegrityHelper,
|
||||
private val schoolsService: SchoolsService,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
) {
|
||||
|
||||
suspend fun logSchoolLogin(loginData: LoginData, students: List<StudentWithSemesters>) {
|
||||
@ -40,10 +38,9 @@ class SchoolsRepository @Inject constructor(
|
||||
private suspend fun logLogin(loginData: LoginData, student: Student, semester: Semester) {
|
||||
val requestId = UUID.randomUUID().toString()
|
||||
val token = integrityHelper.getIntegrityToken(requestId) ?: return
|
||||
val updatedStudent = student.copy(password = loginData.password)
|
||||
|
||||
val schoolInfo = sdk
|
||||
.init(student.copy(password = loginData.password))
|
||||
.switchSemester(semester)
|
||||
val schoolInfo = wulkanowySdkFactory.create(updatedStudent, semester)
|
||||
.getSchool()
|
||||
|
||||
schoolsService.logLoginEvent(
|
||||
|
@ -1,11 +1,15 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.SemesterDao
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.*
|
||||
import io.github.wulkanowy.utils.DispatchersProvider
|
||||
import io.github.wulkanowy.utils.getCurrentOrLast
|
||||
import io.github.wulkanowy.utils.isCurrent
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
@ -14,8 +18,8 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class SemesterRepository @Inject constructor(
|
||||
private val semesterDb: SemesterDao,
|
||||
private val sdk: Sdk,
|
||||
private val dispatchers: DispatchersProvider
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val dispatchers: DispatchersProvider,
|
||||
) {
|
||||
|
||||
suspend fun getSemesters(
|
||||
@ -45,6 +49,7 @@ class SemesterRepository @Inject constructor(
|
||||
0 == it.diaryId && 0 == it.kindergartenDiaryId
|
||||
} == true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
@ -55,12 +60,17 @@ class SemesterRepository @Inject constructor(
|
||||
}
|
||||
|
||||
private suspend fun refreshSemesters(student: Student) {
|
||||
val new = sdk.init(student).getSemesters().mapToEntities(student.studentId)
|
||||
val new = wulkanowySdkFactory.create(student)
|
||||
.getSemesters()
|
||||
.mapToEntities(student.studentId)
|
||||
|
||||
if (new.isEmpty()) return Timber.i("Empty semester list!")
|
||||
|
||||
val old = semesterDb.loadAll(student.studentId, student.classId)
|
||||
semesterDb.deleteAll(old.uniqueSubtract(new))
|
||||
semesterDb.insertSemesters(new.uniqueSubtract(old))
|
||||
semesterDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = new uniqueSubtract old,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getCurrentSemester(student: Student, forceRefresh: Boolean = false) =
|
||||
|
@ -1,13 +1,11 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.StudentInfoDao
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntity
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.switchSemester
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@ -15,7 +13,7 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class StudentInfoRepository @Inject constructor(
|
||||
private val studentInfoDao: StudentInfoDao,
|
||||
private val sdk: Sdk
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
) {
|
||||
|
||||
private val saveFetchResultMutex = Mutex()
|
||||
@ -30,16 +28,16 @@ class StudentInfoRepository @Inject constructor(
|
||||
shouldFetch = { it == null || forceRefresh },
|
||||
query = { studentInfoDao.loadStudentInfo(student.studentId) },
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
.getStudentInfo().mapToEntity(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getStudentInfo()
|
||||
.mapToEntity(semester)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
if (old != null && new != old) {
|
||||
with(studentInfoDao) {
|
||||
deleteAll(listOf(old))
|
||||
insertAll(listOf(new))
|
||||
}
|
||||
studentInfoDao.removeOldAndSaveNew(
|
||||
oldItems = listOf(old),
|
||||
newItems = listOf(new),
|
||||
)
|
||||
} else if (old == null) {
|
||||
studentInfoDao.insertAll(listOf(new))
|
||||
}
|
||||
|
@ -1,11 +1,13 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import androidx.room.withTransaction
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.AppDatabase
|
||||
import io.github.wulkanowy.data.db.dao.SemesterDao
|
||||
import io.github.wulkanowy.data.db.dao.StudentDao
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.db.entities.StudentIsAuthorized
|
||||
import io.github.wulkanowy.data.db.entities.StudentName
|
||||
import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
|
||||
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
|
||||
@ -14,9 +16,8 @@ import io.github.wulkanowy.data.mappers.mapToPojo
|
||||
import io.github.wulkanowy.data.pojos.RegisterUser
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.DispatchersProvider
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.getCurrentOrLast
|
||||
import io.github.wulkanowy.utils.security.Scrambler
|
||||
import io.github.wulkanowy.utils.switchSemester
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@ -26,7 +27,7 @@ class StudentRepository @Inject constructor(
|
||||
private val dispatchers: DispatchersProvider,
|
||||
private val studentDb: StudentDao,
|
||||
private val semesterDb: SemesterDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val appDatabase: AppDatabase,
|
||||
private val scrambler: Scrambler,
|
||||
) {
|
||||
@ -37,7 +38,7 @@ class StudentRepository @Inject constructor(
|
||||
pin: String,
|
||||
symbol: String,
|
||||
token: String
|
||||
): RegisterUser = sdk
|
||||
): RegisterUser = wulkanowySdkFactory.create()
|
||||
.getStudentsFromHebe(token, pin, symbol, "")
|
||||
.mapToPojo(null)
|
||||
|
||||
@ -47,7 +48,7 @@ class StudentRepository @Inject constructor(
|
||||
scrapperBaseUrl: String,
|
||||
domainSuffix: String,
|
||||
symbol: String
|
||||
): RegisterUser = sdk
|
||||
): RegisterUser = wulkanowySdkFactory.create()
|
||||
.getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, domainSuffix, symbol)
|
||||
.mapToPojo(password)
|
||||
|
||||
@ -56,7 +57,7 @@ class StudentRepository @Inject constructor(
|
||||
password: String,
|
||||
scrapperBaseUrl: String,
|
||||
symbol: String
|
||||
): RegisterUser = sdk
|
||||
): RegisterUser = wulkanowySdkFactory.create()
|
||||
.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol)
|
||||
.mapToPojo(password)
|
||||
|
||||
@ -100,6 +101,25 @@ class StudentRepository @Inject constructor(
|
||||
return student
|
||||
}
|
||||
|
||||
suspend fun checkCurrentStudentAuthorizationStatus() {
|
||||
val student = getCurrentStudent()
|
||||
|
||||
if (!student.isAuthorized) {
|
||||
val currentSemester = semesterDb.loadAll(
|
||||
studentId = student.studentId,
|
||||
classId = student.classId,
|
||||
).getCurrentOrLast()
|
||||
val initializedSdk = wulkanowySdkFactory.create(student, currentSemester)
|
||||
val isAuthorized = initializedSdk.getCurrentStudent()?.isAuthorized ?: false
|
||||
|
||||
if (isAuthorized) {
|
||||
studentDb.update(StudentIsAuthorized(isAuthorized = true).apply {
|
||||
id = student.id
|
||||
})
|
||||
} else throw NoAuthorizationException()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getCurrentStudent(decryptPass: Boolean = true): Student {
|
||||
val student = studentDb.loadCurrent() ?: throw NoCurrentStudentException()
|
||||
|
||||
@ -149,13 +169,11 @@ class StudentRepository @Inject constructor(
|
||||
.distinctBy { it.student.studentName }.size == 1
|
||||
|
||||
suspend fun authorizePermission(student: Student, semester: Semester, pesel: String) =
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.authorizePermission(pesel)
|
||||
|
||||
suspend fun refreshStudentName(student: Student, semester: Semester) {
|
||||
val newCurrentApiStudent = sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
val newCurrentApiStudent = wulkanowySdkFactory.create(student, semester)
|
||||
.getCurrentStudent() ?: return
|
||||
|
||||
val studentName = StudentName(
|
||||
@ -176,3 +194,6 @@ class StudentRepository @Inject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NoAuthorizationException : Exception()
|
||||
|
||||
|
@ -1,15 +1,13 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.SubjectDao
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.switchSemester
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import javax.inject.Inject
|
||||
@ -18,7 +16,7 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class SubjectRepository @Inject constructor(
|
||||
private val subjectDao: SubjectDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
) {
|
||||
|
||||
@ -39,15 +37,15 @@ class SubjectRepository @Inject constructor(
|
||||
},
|
||||
query = { subjectDao.loadAll(semester.diaryId, semester.studentId) },
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getSubjects()
|
||||
.mapToEntities(semester)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
subjectDao.deleteAll(old uniqueSubtract new)
|
||||
subjectDao.insertAll(new uniqueSubtract old)
|
||||
|
||||
subjectDao.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = new uniqueSubtract old
|
||||
)
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
|
||||
}
|
||||
)
|
||||
|
@ -1,15 +1,13 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.TeacherDao
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.switchSemester
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import javax.inject.Inject
|
||||
@ -18,7 +16,7 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class TeacherRepository @Inject constructor(
|
||||
private val teacherDb: TeacherDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
) {
|
||||
|
||||
@ -39,15 +37,15 @@ class TeacherRepository @Inject constructor(
|
||||
},
|
||||
query = { teacherDb.loadAll(semester.studentId, semester.classId) },
|
||||
fetch = {
|
||||
sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
wulkanowySdkFactory.create(student, semester)
|
||||
.getTeachers()
|
||||
.mapToEntities(semester)
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
teacherDb.deleteAll(old uniqueSubtract new)
|
||||
teacherDb.insertAll(new uniqueSubtract old)
|
||||
|
||||
teacherDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = new uniqueSubtract old,
|
||||
)
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
|
||||
}
|
||||
)
|
||||
|
@ -1,5 +1,6 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.data.db.dao.TimetableAdditionalDao
|
||||
import io.github.wulkanowy.data.db.dao.TimetableDao
|
||||
import io.github.wulkanowy.data.db.dao.TimetableHeaderDao
|
||||
@ -11,14 +12,13 @@ import io.github.wulkanowy.data.db.entities.TimetableHeader
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.networkBoundResource
|
||||
import io.github.wulkanowy.data.pojos.TimetableFull
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper
|
||||
import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider
|
||||
import io.github.wulkanowy.utils.AppWidgetUpdater
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.getRefreshKey
|
||||
import io.github.wulkanowy.utils.init
|
||||
import io.github.wulkanowy.utils.monday
|
||||
import io.github.wulkanowy.utils.sunday
|
||||
import io.github.wulkanowy.utils.switchSemester
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
@ -28,14 +28,16 @@ import java.time.LocalDate
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
||||
@Singleton
|
||||
class TimetableRepository @Inject constructor(
|
||||
private val timetableDb: TimetableDao,
|
||||
private val timetableAdditionalDb: TimetableAdditionalDao,
|
||||
private val timetableHeaderDb: TimetableHeaderDao,
|
||||
private val sdk: Sdk,
|
||||
private val wulkanowySdkFactory: WulkanowySdkFactory,
|
||||
private val schedulerHelper: TimetableNotificationSchedulerHelper,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
private val appWidgetUpdater: AppWidgetUpdater,
|
||||
) {
|
||||
|
||||
private val saveFetchResultMutex = Mutex()
|
||||
@ -54,7 +56,8 @@ class TimetableRepository @Inject constructor(
|
||||
forceRefresh: Boolean,
|
||||
refreshAdditional: Boolean = false,
|
||||
notify: Boolean = false,
|
||||
timetableType: TimetableType = TimetableType.NORMAL
|
||||
timetableType: TimetableType = TimetableType.NORMAL,
|
||||
isFromAppWidget: Boolean = false
|
||||
) = networkBoundResource(
|
||||
mutex = saveFetchResultMutex,
|
||||
isResultEmpty = {
|
||||
@ -74,8 +77,7 @@ class TimetableRepository @Inject constructor(
|
||||
},
|
||||
query = { getFullTimetableFromDatabase(student, semester, start, end) },
|
||||
fetch = {
|
||||
val timetableFull = sdk.init(student)
|
||||
.switchSemester(semester)
|
||||
val timetableFull = wulkanowySdkFactory.create(student, semester)
|
||||
.getTimetable(start.monday, end.sunday)
|
||||
|
||||
timetableFull.mapToEntities(semester)
|
||||
@ -86,6 +88,9 @@ class TimetableRepository @Inject constructor(
|
||||
refreshDayHeaders(timetableOld.headers, timetableNew.headers)
|
||||
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
|
||||
if (!isFromAppWidget) {
|
||||
appWidgetUpdater.updateAllAppWidgetsByProvider(TimetableWidgetProvider::class)
|
||||
}
|
||||
},
|
||||
filterResult = { (timetable, additional, headers) ->
|
||||
TimetableFull(
|
||||
@ -154,8 +159,10 @@ class TimetableRepository @Inject constructor(
|
||||
new.apply { if (notify) isNotified = false }
|
||||
}
|
||||
|
||||
timetableDb.deleteAll(lessonsToRemove)
|
||||
timetableDb.insertAll(lessonsToAdd)
|
||||
timetableDb.removeOldAndSaveNew(
|
||||
oldItems = lessonsToRemove,
|
||||
newItems = lessonsToAdd,
|
||||
)
|
||||
|
||||
schedulerHelper.cancelScheduled(lessonsToRemove, student)
|
||||
schedulerHelper.scheduleNotifications(lessonsToAdd, student)
|
||||
@ -166,13 +173,17 @@ class TimetableRepository @Inject constructor(
|
||||
new: List<TimetableAdditional>
|
||||
) {
|
||||
val oldFiltered = old.filter { !it.isAddedByUser }
|
||||
timetableAdditionalDb.deleteAll(oldFiltered uniqueSubtract new)
|
||||
timetableAdditionalDb.insertAll(new uniqueSubtract old)
|
||||
timetableAdditionalDb.removeOldAndSaveNew(
|
||||
oldItems = oldFiltered uniqueSubtract new,
|
||||
newItems = new uniqueSubtract old,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun refreshDayHeaders(old: List<TimetableHeader>, new: List<TimetableHeader>) {
|
||||
timetableHeaderDb.deleteAll(old uniqueSubtract new)
|
||||
timetableHeaderDb.insertAll(new uniqueSubtract old)
|
||||
timetableHeaderDb.removeOldAndSaveNew(
|
||||
oldItems = old uniqueSubtract new,
|
||||
newItems = new uniqueSubtract old,
|
||||
)
|
||||
}
|
||||
|
||||
fun getLastRefreshTimestamp(semester: Semester, start: LocalDate, end: LocalDate): Instant {
|
||||
|
@ -0,0 +1,106 @@
|
||||
package io.github.wulkanowy.domain.attendance
|
||||
|
||||
import io.github.wulkanowy.data.*
|
||||
import io.github.wulkanowy.data.db.entities.AttendanceSummary
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.db.entities.Subject
|
||||
import io.github.wulkanowy.data.enums.AttendanceCalculatorSortingMode
|
||||
import io.github.wulkanowy.data.enums.AttendanceCalculatorSortingMode.*
|
||||
import io.github.wulkanowy.data.pojos.AttendanceData
|
||||
import io.github.wulkanowy.data.repositories.AttendanceSummaryRepository
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.data.repositories.SubjectRepository
|
||||
import io.github.wulkanowy.utils.allAbsences
|
||||
import io.github.wulkanowy.utils.allPresences
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
|
||||
class GetAttendanceCalculatorDataUseCase @Inject constructor(
|
||||
private val subjectRepository: SubjectRepository,
|
||||
private val attendanceSummaryRepository: AttendanceSummaryRepository,
|
||||
private val preferencesRepository: PreferencesRepository,
|
||||
) {
|
||||
|
||||
operator fun invoke(
|
||||
student: Student,
|
||||
semester: Semester,
|
||||
forceRefresh: Boolean,
|
||||
): Flow<Resource<List<AttendanceData>>> =
|
||||
subjectRepository.getSubjects(student, semester, forceRefresh)
|
||||
.mapResourceData { subjects -> subjects.sortedBy(Subject::name) }
|
||||
.combineWithResourceData(preferencesRepository.targetAttendanceFlow, ::Pair)
|
||||
.flatMapResourceData { (subjects, targetFreq) ->
|
||||
combineResourceFlows(subjects.map { subject ->
|
||||
attendanceSummaryRepository.getAttendanceSummary(
|
||||
student = student,
|
||||
semester = semester,
|
||||
subjectId = subject.realId,
|
||||
forceRefresh = forceRefresh
|
||||
).mapResourceData { summaries ->
|
||||
summaries.toAttendanceData(subject.name, targetFreq)
|
||||
}
|
||||
})
|
||||
// Every individual combined flow causes separate network requests to update data.
|
||||
// When there is N child flows, they can cause up to N-1 items to be emitted. Since all
|
||||
// requests are usually completed in less than 5s, there is no need to emit multiple
|
||||
// intermediates that will be visible for barely any time.
|
||||
.debounceIntermediates()
|
||||
}
|
||||
.combineWithResourceData(preferencesRepository.attendanceCalculatorShowEmptySubjects) { attendanceDataList, showEmptySubjects ->
|
||||
attendanceDataList.filter { it.total != 0 || showEmptySubjects }
|
||||
}
|
||||
.combineWithResourceData(preferencesRepository.attendanceCalculatorSortingModeFlow, List<AttendanceData>::sortedBy)
|
||||
}
|
||||
|
||||
private fun List<AttendanceSummary>.toAttendanceData(subjectName: String, targetFreq: Int): AttendanceData {
|
||||
val presences = sumOf { it.allPresences }
|
||||
val absences = sumOf { it.allAbsences }
|
||||
return AttendanceData(
|
||||
subjectName = subjectName,
|
||||
lessonBalance = calcLessonBalance(
|
||||
targetFreq.toDouble() / 100, presences, absences
|
||||
),
|
||||
presences = presences,
|
||||
absences = absences,
|
||||
)
|
||||
}
|
||||
|
||||
private fun calcLessonBalance(targetFreq: Double, presences: Int, absences: Int): Int {
|
||||
val total = presences + absences
|
||||
// The `+ 1` is to avoid false positives in close cases. Eg.:
|
||||
// target frequency 99%, 1 presence. Without the `+ 1` this would be reported shown as
|
||||
// a positive balance of +1, however that is not actually true as skipping one class
|
||||
// would make it so that the balance would actually be negative (-98). The `+ 1`
|
||||
// fixes this and makes sure that in situations like these, it's not reporting incorrect
|
||||
// balances
|
||||
return when {
|
||||
presences / (total + 1f) >= targetFreq -> calcMissingAbsences(
|
||||
targetFreq, absences, presences
|
||||
)
|
||||
presences / (total + 0f) < targetFreq -> -calcMissingPresences(
|
||||
targetFreq, absences, presences
|
||||
)
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun calcMissingPresences(targetFreq: Double, absences: Int, presences: Int) =
|
||||
calcMinRequiredPresencesFor(targetFreq, absences) - presences
|
||||
|
||||
private fun calcMinRequiredPresencesFor(targetFreq: Double, absences: Int) =
|
||||
ceil((targetFreq / (1 - targetFreq)) * absences).toInt()
|
||||
|
||||
private fun calcMissingAbsences(targetFreq: Double, absences: Int, presences: Int) =
|
||||
calcMinRequiredAbsencesFor(targetFreq, presences) - absences
|
||||
|
||||
private fun calcMinRequiredAbsencesFor(targetFreq: Double, presences: Int) =
|
||||
floor((presences * (1 - targetFreq)) / targetFreq).toInt()
|
||||
|
||||
private fun List<AttendanceData>.sortedBy(mode: AttendanceCalculatorSortingMode) = when (mode) {
|
||||
ALPHABETIC -> sortedBy(AttendanceData::subjectName)
|
||||
ATTENDANCE -> sortedByDescending(AttendanceData::presencePercentage)
|
||||
LESSON_BALANCE -> sortedBy(AttendanceData::lessonBalance)
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package io.github.wulkanowy.ui.base
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
@ -17,6 +18,8 @@ import io.github.wulkanowy.utils.FragmentLifecycleLogger
|
||||
import io.github.wulkanowy.utils.getThemeAttrColor
|
||||
import io.github.wulkanowy.utils.lifecycleAwareVariable
|
||||
import io.github.wulkanowy.utils.openInternetBrowser
|
||||
import timber.log.Timber
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
|
||||
@ -36,16 +39,26 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
|
||||
|
||||
abstract var presenter: T
|
||||
|
||||
private var lastDialogOpenTime = mutableMapOf<String, Instant>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
inject()
|
||||
themeManager.applyActivityTheme(this)
|
||||
super.onCreate(savedInstanceState)
|
||||
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleLogger, true)
|
||||
applyCustomTaskDescription()
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
setTaskDescription(
|
||||
ActivityManager.TaskDescription(null, null, getThemeAttrColor(R.attr.colorSurface))
|
||||
)
|
||||
@Suppress("DEPRECATION")
|
||||
private fun applyCustomTaskDescription() {
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) return
|
||||
try {
|
||||
val newColor = getThemeAttrColor(R.attr.colorSurface)
|
||||
val taskDescription = ActivityManager.TaskDescription(null, null, newColor)
|
||||
setTaskDescription(taskDescription)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun showError(text: String, error: Throwable) {
|
||||
@ -70,6 +83,8 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
|
||||
}
|
||||
|
||||
override fun showExpiredCredentialsDialog() {
|
||||
if (!shouldShowDialog(DIALOG_ERROR_BAD_CREDENTIALS)) return
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.main_expired_credentials_title)
|
||||
.setMessage(R.string.main_expired_credentials_description)
|
||||
@ -83,6 +98,8 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
|
||||
}
|
||||
|
||||
override fun showDecryptionFailedDialog() {
|
||||
if (!shouldShowDialog(DIALOG_ERROR_DECRYPTION_FAILED)) return
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.main_session_expired)
|
||||
.setMessage(R.string.main_session_relogin)
|
||||
@ -119,4 +136,21 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
|
||||
protected open fun inject() {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
private fun shouldShowDialog(name: String): Boolean {
|
||||
val lastOpenTime = lastDialogOpenTime[name]
|
||||
val now = Instant.now()
|
||||
|
||||
if (lastOpenTime != null && now.isBefore(lastOpenTime.plusSeconds(1))) {
|
||||
Timber.i("Dialog $name was shown less than a second ago. Skip")
|
||||
return false
|
||||
}
|
||||
lastDialogOpenTime[name] = Instant.now()
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DIALOG_ERROR_BAD_CREDENTIALS = "dialog_error_bad_credentials"
|
||||
private const val DIALOG_ERROR_DECRYPTION_FAILED = "dialog_error_decryption_failed"
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ package io.github.wulkanowy.ui.base
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
|
||||
import io.github.wulkanowy.sdk.scrapper.exception.AuthorizationRequiredException
|
||||
import io.github.wulkanowy.data.repositories.NoAuthorizationException
|
||||
import io.github.wulkanowy.sdk.scrapper.exception.CloudflareVerificationException
|
||||
import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException
|
||||
import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException
|
||||
@ -40,7 +40,7 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co
|
||||
is ScramblerException -> onDecryptionFailed()
|
||||
is BadCredentialsException -> onExpiredCredentials()
|
||||
is NoCurrentStudentException -> onNoCurrentStudent()
|
||||
is AuthorizationRequiredException -> onAuthorizationRequired()
|
||||
is NoAuthorizationException -> onAuthorizationRequired()
|
||||
is CloudflareVerificationException -> onCaptchaVerificationRequired(error.originalUrl)
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package io.github.wulkanowy.ui.modules.attendance
|
||||
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Typeface
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@ -33,17 +34,17 @@ class AttendanceAdapter @Inject constructor() :
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
|
||||
val context = holder.binding.root.context
|
||||
val item = items[position]
|
||||
|
||||
with(holder.binding) {
|
||||
attendanceItemNumber.text = item.number.toString()
|
||||
attendanceItemSubject.text = item.subject.ifBlank {
|
||||
root.context.getString(R.string.all_no_data)
|
||||
}
|
||||
attendanceItemSubject.text = item.subject
|
||||
.ifBlank { context.getString(R.string.all_no_data) }
|
||||
attendanceItemDescription.setText(item.descriptionRes)
|
||||
|
||||
attendanceItemDescription.setTextColor(
|
||||
root.context.getThemeAttrColor(
|
||||
context.getThemeAttrColor(
|
||||
when {
|
||||
item.absence && !item.excused -> R.attr.colorAttendanceAbsence
|
||||
item.lateness && !item.excused -> R.attr.colorAttendanceLateness
|
||||
@ -61,13 +62,15 @@ class AttendanceAdapter @Inject constructor() :
|
||||
attendanceItemAlert.isVisible =
|
||||
item.let { (it.absence && !it.excused) || (it.lateness && !it.excused) }
|
||||
|
||||
attendanceItemAlert.setColorFilter(root.context.getThemeAttrColor(
|
||||
when{
|
||||
item.absence && !item.excused -> R.attr.colorAttendanceAbsence
|
||||
item.lateness && !item.excused -> R.attr.colorAttendanceLateness
|
||||
else -> android.R.attr.colorPrimary
|
||||
}
|
||||
))
|
||||
attendanceItemAlert.imageTintList = ColorStateList.valueOf(
|
||||
context.getThemeAttrColor(
|
||||
when {
|
||||
item.absence && !item.excused -> R.attr.colorAttendanceAbsence
|
||||
item.lateness && !item.excused -> R.attr.colorAttendanceLateness
|
||||
else -> android.R.attr.colorPrimary
|
||||
}
|
||||
)
|
||||
)
|
||||
attendanceItemNumber.visibility = View.GONE
|
||||
attendanceItemExcuseInfo.visibility = View.GONE
|
||||
attendanceItemExcuseCheckbox.visibility = View.GONE
|
||||
|
@ -14,6 +14,7 @@ import io.github.wulkanowy.data.db.entities.Attendance
|
||||
import io.github.wulkanowy.databinding.DialogExcuseBinding
|
||||
import io.github.wulkanowy.databinding.FragmentAttendanceBinding
|
||||
import io.github.wulkanowy.ui.base.BaseFragment
|
||||
import io.github.wulkanowy.ui.modules.attendance.calculator.AttendanceCalculatorFragment
|
||||
import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment
|
||||
import io.github.wulkanowy.ui.modules.main.MainActivity
|
||||
import io.github.wulkanowy.ui.modules.main.MainView
|
||||
@ -134,6 +135,7 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return if (item.itemId == R.id.attendanceMenuSummary) presenter.onSummarySwitchSelected()
|
||||
else if (item.itemId == R.id.attendanceMenuCalculator) presenter.onCalculatorSwitchSelected()
|
||||
else false
|
||||
}
|
||||
|
||||
@ -253,6 +255,10 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
|
||||
(activity as? MainActivity)?.pushView(AttendanceSummaryFragment.newInstance())
|
||||
}
|
||||
|
||||
override fun openCalculatorView() {
|
||||
(activity as? MainActivity)?.pushView(AttendanceCalculatorFragment.newInstance())
|
||||
}
|
||||
|
||||
override fun startActionMode() {
|
||||
actionMode = (activity as MainActivity?)?.startSupportActionMode(actionModeCallback)
|
||||
}
|
||||
|
@ -1,17 +1,36 @@
|
||||
package io.github.wulkanowy.ui.modules.attendance
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import io.github.wulkanowy.data.*
|
||||
import io.github.wulkanowy.data.Resource
|
||||
import io.github.wulkanowy.data.db.entities.Attendance
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.flatResourceFlow
|
||||
import io.github.wulkanowy.data.logResourceStatus
|
||||
import io.github.wulkanowy.data.mapResourceData
|
||||
import io.github.wulkanowy.data.onResourceData
|
||||
import io.github.wulkanowy.data.onResourceError
|
||||
import io.github.wulkanowy.data.onResourceIntermediate
|
||||
import io.github.wulkanowy.data.onResourceLoading
|
||||
import io.github.wulkanowy.data.onResourceNotLoading
|
||||
import io.github.wulkanowy.data.onResourceSuccess
|
||||
import io.github.wulkanowy.data.repositories.AttendanceRepository
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.data.repositories.SemesterRepository
|
||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||
import io.github.wulkanowy.data.resourceFlow
|
||||
import io.github.wulkanowy.ui.base.BasePresenter
|
||||
import io.github.wulkanowy.ui.base.ErrorHandler
|
||||
import io.github.wulkanowy.utils.*
|
||||
import io.github.wulkanowy.utils.AnalyticsHelper
|
||||
import io.github.wulkanowy.utils.capitalise
|
||||
import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday
|
||||
import io.github.wulkanowy.utils.isExcusableOrNotExcused
|
||||
import io.github.wulkanowy.utils.isHolidays
|
||||
import io.github.wulkanowy.utils.monday
|
||||
import io.github.wulkanowy.utils.nextSchoolDay
|
||||
import io.github.wulkanowy.utils.previousOrSameSchoolDay
|
||||
import io.github.wulkanowy.utils.previousSchoolDay
|
||||
import io.github.wulkanowy.utils.sunday
|
||||
import io.github.wulkanowy.utils.toFormattedString
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import timber.log.Timber
|
||||
@ -29,6 +48,7 @@ class AttendancePresenter @Inject constructor(
|
||||
private val prefRepository: PreferencesRepository,
|
||||
private val analytics: AnalyticsHelper
|
||||
) : BasePresenter<AttendanceView>(errorHandler, studentRepository) {
|
||||
|
||||
private var initialDate: LocalDate? = null
|
||||
private var isWeekendHasLessons: Boolean = false
|
||||
|
||||
@ -195,6 +215,11 @@ class AttendancePresenter @Inject constructor(
|
||||
return true
|
||||
}
|
||||
|
||||
fun onCalculatorSwitchSelected(): Boolean {
|
||||
view?.openCalculatorView()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun loadData(forceRefresh: Boolean = false) {
|
||||
Timber.i("Loading attendance data started")
|
||||
|
||||
|
@ -56,6 +56,8 @@ interface AttendanceView : BaseView {
|
||||
|
||||
fun openSummaryView()
|
||||
|
||||
fun openCalculatorView()
|
||||
|
||||
fun startSendMessageIntent(date: LocalDate, numbers: String, reason: String)
|
||||
|
||||
fun startActionMode()
|
||||
|
@ -0,0 +1,67 @@
|
||||
package io.github.wulkanowy.ui.modules.attendance.calculator
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.pojos.AttendanceData
|
||||
import io.github.wulkanowy.databinding.ItemAttendanceCalculatorHeaderBinding
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class AttendanceCalculatorAdapter @Inject constructor() :
|
||||
RecyclerView.Adapter<AttendanceCalculatorAdapter.ViewHolder>() {
|
||||
|
||||
var items = emptyList<AttendanceData>()
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
|
||||
ItemAttendanceCalculatorHeaderBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
)
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(parent: ViewHolder, position: Int) {
|
||||
val context = parent.binding.root.context
|
||||
val item = items[position]
|
||||
|
||||
with(parent.binding) {
|
||||
attendanceCalculatorPercentage.text = "${item.presencePercentage.roundToInt()}"
|
||||
|
||||
attendanceCalculatorSummaryBalance.text = when {
|
||||
item.lessonBalance > 0 -> {
|
||||
context.getString(
|
||||
R.string.attendance_calculator_summary_balance_positive,
|
||||
item.lessonBalance
|
||||
)
|
||||
}
|
||||
|
||||
item.lessonBalance < 0 -> {
|
||||
context.getString(
|
||||
R.string.attendance_calculator_summary_balance_negative,
|
||||
abs(item.lessonBalance)
|
||||
)
|
||||
}
|
||||
|
||||
else -> context.getString(R.string.attendance_calculator_summary_balance_neutral)
|
||||
}
|
||||
attendanceCalculatorWarning.isVisible = item.lessonBalance < 0
|
||||
attendanceCalculatorTitle.text = item.subjectName
|
||||
attendanceCalculatorSummaryValues.text = if (item.total == 0) {
|
||||
context.getString(R.string.attendance_calculator_summary_values_empty)
|
||||
} else {
|
||||
context.getString(
|
||||
R.string.attendance_calculator_summary_values,
|
||||
item.presences,
|
||||
item.total
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolder(val binding: ItemAttendanceCalculatorHeaderBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
package io.github.wulkanowy.ui.modules.attendance.calculator
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.pojos.AttendanceData
|
||||
import io.github.wulkanowy.databinding.FragmentAttendanceCalculatorBinding
|
||||
import io.github.wulkanowy.ui.base.BaseFragment
|
||||
import io.github.wulkanowy.ui.modules.main.MainView
|
||||
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
|
||||
import io.github.wulkanowy.utils.getThemeAttrColor
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AttendanceCalculatorFragment :
|
||||
BaseFragment<FragmentAttendanceCalculatorBinding>(R.layout.fragment_attendance_calculator),
|
||||
AttendanceCalculatorView, MainView.TitledView {
|
||||
|
||||
@Inject
|
||||
lateinit var presenter: AttendanceCalculatorPresenter
|
||||
|
||||
@Inject
|
||||
lateinit var attendanceCalculatorAdapter: AttendanceCalculatorAdapter
|
||||
|
||||
override val titleStringId get() = R.string.attendance_title
|
||||
|
||||
companion object {
|
||||
fun newInstance() = AttendanceCalculatorFragment()
|
||||
}
|
||||
|
||||
override val isViewEmpty get() = attendanceCalculatorAdapter.items.isEmpty()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding = FragmentAttendanceCalculatorBinding.bind(view)
|
||||
messageContainer = binding.attendanceCalculatorRecycler
|
||||
presenter.onAttachView(this)
|
||||
}
|
||||
|
||||
override fun initView() {
|
||||
with(binding.attendanceCalculatorRecycler) {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
adapter = attendanceCalculatorAdapter
|
||||
addItemDecoration(DividerItemDecoration(context))
|
||||
}
|
||||
|
||||
with(binding) {
|
||||
attendanceCalculatorSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
|
||||
attendanceCalculatorSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
|
||||
attendanceCalculatorSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
|
||||
attendanceCalculatorErrorRetry.setOnClickListener { presenter.onRetry() }
|
||||
attendanceCalculatorErrorDetails.setOnClickListener { presenter.onDetailsClick() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateData(data: List<AttendanceData>) {
|
||||
with(attendanceCalculatorAdapter) {
|
||||
items = data
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearView() {
|
||||
with(attendanceCalculatorAdapter) {
|
||||
items = emptyList()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override fun showEmpty(show: Boolean) {
|
||||
binding.attendanceCalculatorEmpty.isVisible = show
|
||||
}
|
||||
|
||||
override fun showErrorView(show: Boolean) {
|
||||
binding.attendanceCalculatorError.isVisible = show
|
||||
}
|
||||
|
||||
override fun setErrorDetails(message: String) {
|
||||
binding.attendanceCalculatorErrorMessage.text = message
|
||||
}
|
||||
|
||||
override fun showProgress(show: Boolean) {
|
||||
binding.attendanceCalculatorProgress.isVisible = show
|
||||
}
|
||||
|
||||
override fun enableSwipe(enable: Boolean) {
|
||||
binding.attendanceCalculatorSwipe.isEnabled = enable
|
||||
}
|
||||
|
||||
override fun showContent(show: Boolean) {
|
||||
binding.attendanceCalculatorRecycler.isVisible = show
|
||||
}
|
||||
|
||||
override fun showRefresh(show: Boolean) {
|
||||
binding.attendanceCalculatorSwipe.isRefreshing = show
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
presenter.onDetachView()
|
||||
super.onDestroyView()
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
package io.github.wulkanowy.ui.modules.attendance.calculator
|
||||
|
||||
import io.github.wulkanowy.data.*
|
||||
import io.github.wulkanowy.data.repositories.SemesterRepository
|
||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||
import io.github.wulkanowy.domain.attendance.GetAttendanceCalculatorDataUseCase
|
||||
import io.github.wulkanowy.ui.base.BasePresenter
|
||||
import io.github.wulkanowy.ui.base.ErrorHandler
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class AttendanceCalculatorPresenter @Inject constructor(
|
||||
errorHandler: ErrorHandler,
|
||||
studentRepository: StudentRepository,
|
||||
private val semesterRepository: SemesterRepository,
|
||||
private val getAttendanceCalculatorData: GetAttendanceCalculatorDataUseCase,
|
||||
) : BasePresenter<AttendanceCalculatorView>(errorHandler, studentRepository) {
|
||||
|
||||
private lateinit var lastError: Throwable
|
||||
|
||||
override fun onAttachView(view: AttendanceCalculatorView) {
|
||||
super.onAttachView(view)
|
||||
view.initView()
|
||||
Timber.i("Attendance calculator view was initialized")
|
||||
errorHandler.showErrorMessage = ::showErrorViewOnError
|
||||
loadData()
|
||||
}
|
||||
|
||||
fun onSwipeRefresh() {
|
||||
Timber.i("Force refreshing the attendance calculator")
|
||||
loadData(forceRefresh = true)
|
||||
}
|
||||
|
||||
fun onRetry() {
|
||||
view?.run {
|
||||
showErrorView(false)
|
||||
showProgress(true)
|
||||
}
|
||||
loadData()
|
||||
}
|
||||
|
||||
fun onDetailsClick() {
|
||||
view?.showErrorDetailsDialog(lastError)
|
||||
}
|
||||
|
||||
private fun loadData(forceRefresh: Boolean = false) {
|
||||
flatResourceFlow {
|
||||
val student = studentRepository.getCurrentStudent()
|
||||
val semester = semesterRepository.getCurrentSemester(student)
|
||||
getAttendanceCalculatorData(student, semester, forceRefresh)
|
||||
}
|
||||
.logResourceStatus("load attendance calculator")
|
||||
.onResourceData {
|
||||
view?.run {
|
||||
showProgress(false)
|
||||
showErrorView(false)
|
||||
showContent(it.isNotEmpty())
|
||||
showEmpty(it.isEmpty())
|
||||
updateData(it)
|
||||
}
|
||||
}
|
||||
.onResourceIntermediate { view?.showRefresh(true) }
|
||||
.onResourceNotLoading {
|
||||
view?.run {
|
||||
enableSwipe(true)
|
||||
showRefresh(false)
|
||||
showProgress(false)
|
||||
}
|
||||
}
|
||||
.onResourceError(errorHandler::dispatch)
|
||||
.launch()
|
||||
}
|
||||
|
||||
private fun showErrorViewOnError(message: String, error: Throwable) {
|
||||
view?.run {
|
||||
if (isViewEmpty) {
|
||||
lastError = error
|
||||
setErrorDetails(message)
|
||||
showErrorView(true)
|
||||
showEmpty(false)
|
||||
} else showError(message, error)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package io.github.wulkanowy.ui.modules.attendance.calculator
|
||||
|
||||
import io.github.wulkanowy.data.pojos.AttendanceData
|
||||
import io.github.wulkanowy.ui.base.BaseView
|
||||
|
||||
interface AttendanceCalculatorView : BaseView {
|
||||
|
||||
val isViewEmpty: Boolean
|
||||
|
||||
fun initView()
|
||||
|
||||
fun showRefresh(show: Boolean)
|
||||
|
||||
fun showContent(show: Boolean)
|
||||
|
||||
fun showProgress(show: Boolean)
|
||||
|
||||
fun enableSwipe(enable: Boolean)
|
||||
|
||||
fun showEmpty(show: Boolean)
|
||||
|
||||
fun showErrorView(show: Boolean)
|
||||
|
||||
fun setErrorDetails(message: String)
|
||||
|
||||
fun updateData(data: List<AttendanceData>)
|
||||
|
||||
fun clearView()
|
||||
}
|
@ -78,4 +78,9 @@ class AuthDialog : BaseDialogFragment<DialogAuthBinding>(), AuthView {
|
||||
override fun showDescriptionWithName(name: String) {
|
||||
binding.authDescription.text = getString(R.string.auth_description, name).parseAsHtml()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
presenter.onDetachView()
|
||||
super.onDestroyView()
|
||||
}
|
||||
}
|
||||
|
@ -10,9 +10,10 @@ import android.webkit.WebViewClient
|
||||
import androidx.core.os.bundleOf
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.WulkanowySdkFactory
|
||||
import io.github.wulkanowy.databinding.DialogCaptchaBinding
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.ui.base.BaseDialogFragment
|
||||
import io.github.wulkanowy.utils.WebkitCookieManagerProxy
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -20,7 +21,10 @@ import javax.inject.Inject
|
||||
class CaptchaDialog : BaseDialogFragment<DialogCaptchaBinding>() {
|
||||
|
||||
@Inject
|
||||
lateinit var sdk: Sdk
|
||||
lateinit var wulkanowySdkFactory: WulkanowySdkFactory
|
||||
|
||||
@Inject
|
||||
lateinit var webkitCookieManagerProxy: WebkitCookieManagerProxy
|
||||
|
||||
private var webView: WebView? = null
|
||||
|
||||
@ -55,7 +59,7 @@ class CaptchaDialog : BaseDialogFragment<DialogCaptchaBinding>() {
|
||||
webView = this
|
||||
with(settings) {
|
||||
javaScriptEnabled = true
|
||||
userAgentString = sdk.userAgent
|
||||
userAgentString = wulkanowySdkFactory.create().userAgent
|
||||
}
|
||||
|
||||
webViewClient = object : WebViewClient() {
|
||||
@ -80,6 +84,7 @@ class CaptchaDialog : BaseDialogFragment<DialogCaptchaBinding>() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
webkitCookieManagerProxy.webkitCookieManager?.flush()
|
||||
webView?.destroy()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
@ -159,7 +159,7 @@ class GradeAverageProvider @Inject constructor(
|
||||
?.updateModifiers(student, config).orEmpty()
|
||||
|
||||
(updatedSecondSemesterGrades + updatedFirstSemesterGrades).calcAverage(
|
||||
config.isOptionalArithmeticAverage
|
||||
isOptionalArithmeticAverage = config.isOptionalArithmeticAverage,
|
||||
)
|
||||
} else {
|
||||
secondSemesterSubject.average
|
||||
@ -173,13 +173,21 @@ class GradeAverageProvider @Inject constructor(
|
||||
config: AverageCalcParams,
|
||||
): Double {
|
||||
return if (!isAnyVulcanAverage || config.forceAverageCalc) {
|
||||
val divider = if (secondSemesterSubject.grades.any { it.weightValue > .0 }) 2 else 1
|
||||
val isSecondSemesterHasWeightGrade = secondSemesterSubject.grades
|
||||
.any { it.weightValue > .0 }
|
||||
val isSecondSemesterHasArithmeticGrade = secondSemesterSubject.grades
|
||||
.all { it.weightValue == .0 } && config.isOptionalArithmeticAverage
|
||||
val isSecondSemesterHaveAverage =
|
||||
isSecondSemesterHasWeightGrade || isSecondSemesterHasArithmeticGrade
|
||||
|
||||
val divider = if (isSecondSemesterHaveAverage) 2 else 1
|
||||
val secondSemesterAverage = secondSemesterSubject.grades
|
||||
.updateModifiers(student, config)
|
||||
.calcAverage(config.isOptionalArithmeticAverage)
|
||||
.calcAverage(isOptionalArithmeticAverage = config.isOptionalArithmeticAverage)
|
||||
val firstSemesterAverage = firstSemesterSubject?.grades
|
||||
?.updateModifiers(student, config)
|
||||
?.calcAverage(config.isOptionalArithmeticAverage) ?: secondSemesterAverage
|
||||
?.calcAverage(isOptionalArithmeticAverage = config.isOptionalArithmeticAverage)
|
||||
?: secondSemesterAverage
|
||||
|
||||
(secondSemesterAverage + firstSemesterAverage) / divider
|
||||
} else {
|
||||
@ -225,7 +233,7 @@ class GradeAverageProvider @Inject constructor(
|
||||
subject = summary.subject,
|
||||
average = if (!isAnyAverage || params.forceAverageCalc) {
|
||||
grades.updateModifiers(student, params)
|
||||
.calcAverage(params.isOptionalArithmeticAverage)
|
||||
.calcAverage(isOptionalArithmeticAverage = params.isOptionalArithmeticAverage)
|
||||
} else summary.average,
|
||||
points = summary.pointsSum,
|
||||
summary = summary,
|
||||
@ -286,8 +294,13 @@ class GradeAverageProvider @Inject constructor(
|
||||
proposedPoints = "",
|
||||
finalPoints = "",
|
||||
pointsSum = "",
|
||||
average = if (calcAverage) details.updateModifiers(student, params)
|
||||
.calcAverage(params.isOptionalArithmeticAverage) else .0
|
||||
average = when {
|
||||
calcAverage -> details
|
||||
.updateModifiers(student, params)
|
||||
.calcAverage(isOptionalArithmeticAverage = params.isOptionalArithmeticAverage)
|
||||
|
||||
else -> .0
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -145,7 +145,11 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() {
|
||||
}
|
||||
|
||||
if (currentStudent != null) {
|
||||
luckyNumberRepository.getLuckyNumber(currentStudent, forceRefresh = false)
|
||||
luckyNumberRepository.getLuckyNumber(
|
||||
student = currentStudent,
|
||||
forceRefresh = false,
|
||||
isFromAppWidget = true
|
||||
)
|
||||
.toFirstResult()
|
||||
.dataOrThrow
|
||||
} else null
|
||||
|
@ -73,6 +73,7 @@ class MainPresenter @Inject constructor(
|
||||
syncManager.startPeriodicSyncWorker()
|
||||
|
||||
checkAppSupport()
|
||||
checkCurrentStudentAuthorizationStatus()
|
||||
|
||||
analytics.logEvent("app_open", "destination" to initDestination.toString())
|
||||
Timber.i("Main view was initialized with $initDestination")
|
||||
@ -191,4 +192,13 @@ class MainPresenter @Inject constructor(
|
||||
|
||||
view?.showStudentAvatar(currentStudent)
|
||||
}
|
||||
|
||||
private fun checkCurrentStudentAuthorizationStatus() {
|
||||
presenterScope.launch {
|
||||
runCatching { studentRepository.checkCurrentStudentAuthorizationStatus() }
|
||||
.onFailure { errorHandler.dispatch(it) }
|
||||
|
||||
Timber.i("Current student authorization status checked")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -47,7 +47,6 @@ class MailboxChooserDialog : BaseDialogFragment<DialogMailboxChooserBinding>(),
|
||||
}
|
||||
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
presenter.onAttachView(
|
||||
|
@ -44,8 +44,12 @@ class MessagePreviewFragment :
|
||||
|
||||
private var menuForwardButton: MenuItem? = null
|
||||
|
||||
private var menuRestoreButton: MenuItem? = null
|
||||
|
||||
private var menuDeleteButton: MenuItem? = null
|
||||
|
||||
private var menuDeleteForeverButton: MenuItem? = null
|
||||
|
||||
private var menuShareButton: MenuItem? = null
|
||||
|
||||
private var menuPrintButton: MenuItem? = null
|
||||
@ -64,6 +68,9 @@ class MessagePreviewFragment :
|
||||
override val unmuteMessageSuccessString: String
|
||||
get() = getString(R.string.message_unmute_success)
|
||||
|
||||
override val restoreMessageSuccessString: String
|
||||
get() = getString(R.string.message_restore_success)
|
||||
|
||||
override val messageNoSubjectString: String
|
||||
get() = getString(R.string.message_no_subject)
|
||||
|
||||
@ -75,10 +82,10 @@ class MessagePreviewFragment :
|
||||
get() = getString(R.string.message_not_exists)
|
||||
|
||||
companion object {
|
||||
const val MESSAGE_ID_KEY = "message_id"
|
||||
private const val MESSAGE_ARG_KEY = "message"
|
||||
|
||||
fun newInstance(message: Message) = MessagePreviewFragment().apply {
|
||||
arguments = bundleOf(MESSAGE_ID_KEY to message)
|
||||
arguments = bundleOf(MESSAGE_ARG_KEY to message)
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,7 +101,7 @@ class MessagePreviewFragment :
|
||||
messageContainer = binding.messagePreviewContainer
|
||||
presenter.onAttachView(
|
||||
view = this,
|
||||
message = (savedInstanceState ?: arguments)?.serializable(MESSAGE_ID_KEY),
|
||||
message = requireArguments().serializable(MESSAGE_ARG_KEY),
|
||||
)
|
||||
}
|
||||
|
||||
@ -111,7 +118,9 @@ class MessagePreviewFragment :
|
||||
inflater.inflate(R.menu.action_menu_message_preview, menu)
|
||||
menuReplyButton = menu.findItem(R.id.messagePreviewMenuReply)
|
||||
menuForwardButton = menu.findItem(R.id.messagePreviewMenuForward)
|
||||
menuRestoreButton = menu.findItem(R.id.messagePreviewMenuRestore)
|
||||
menuDeleteButton = menu.findItem(R.id.messagePreviewMenuDelete)
|
||||
menuDeleteForeverButton = menu.findItem(R.id.messagePreviewMenuDeleteForever)
|
||||
menuShareButton = menu.findItem(R.id.messagePreviewMenuShare)
|
||||
menuPrintButton = menu.findItem(R.id.messagePreviewMenuPrint)
|
||||
menuMuteButton = menu.findItem(R.id.messagePreviewMenuMute)
|
||||
@ -124,7 +133,9 @@ class MessagePreviewFragment :
|
||||
return when (item.itemId) {
|
||||
R.id.messagePreviewMenuReply -> presenter.onReply()
|
||||
R.id.messagePreviewMenuForward -> presenter.onForward()
|
||||
R.id.messagePreviewMenuRestore -> presenter.onMessageRestore()
|
||||
R.id.messagePreviewMenuDelete -> presenter.onMessageDelete()
|
||||
R.id.messagePreviewMenuDeleteForever -> presenter.onMessageDelete()
|
||||
R.id.messagePreviewMenuShare -> presenter.onShare()
|
||||
R.id.messagePreviewMenuPrint -> presenter.onPrint()
|
||||
R.id.messagePreviewMenuMute -> presenter.onMute()
|
||||
@ -152,23 +163,17 @@ class MessagePreviewFragment :
|
||||
binding.messagePreviewRecycler.visibility = if (show) VISIBLE else GONE
|
||||
}
|
||||
|
||||
override fun showOptions(show: Boolean, isReplayable: Boolean) {
|
||||
menuReplyButton?.isVisible = isReplayable
|
||||
override fun showOptions(show: Boolean, isReplayable: Boolean, isRestorable: Boolean) {
|
||||
menuReplyButton?.isVisible = show && isReplayable
|
||||
menuForwardButton?.isVisible = show
|
||||
menuDeleteButton?.isVisible = show
|
||||
menuRestoreButton?.isVisible = show && isRestorable
|
||||
menuDeleteButton?.isVisible = show && !isRestorable
|
||||
menuDeleteForeverButton?.isVisible = show && isRestorable
|
||||
menuShareButton?.isVisible = show
|
||||
menuPrintButton?.isVisible = show
|
||||
menuMuteButton?.isVisible = show && isReplayable
|
||||
}
|
||||
|
||||
override fun setDeletedOptionsLabels() {
|
||||
menuDeleteButton?.setTitle(R.string.message_delete_forever)
|
||||
}
|
||||
|
||||
override fun setNotDeletedOptionsLabels() {
|
||||
menuDeleteButton?.setTitle(R.string.message_move_to_trash)
|
||||
}
|
||||
|
||||
override fun showErrorView(show: Boolean) {
|
||||
binding.messagePreviewError.visibility = if (show) VISIBLE else GONE
|
||||
}
|
||||
@ -228,11 +233,6 @@ class MessagePreviewFragment :
|
||||
(activity as MainActivity).popView()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putSerializable(MESSAGE_ID_KEY, presenter.messageWithAttachments)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
presenter.onDetachView()
|
||||
super.onDestroyView()
|
||||
|
@ -3,10 +3,15 @@ package io.github.wulkanowy.ui.modules.message.preview
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.core.text.parseAsHtml
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.*
|
||||
import io.github.wulkanowy.data.db.entities.Message
|
||||
import io.github.wulkanowy.data.db.entities.MessageWithAttachment
|
||||
import io.github.wulkanowy.data.enums.MessageFolder
|
||||
import io.github.wulkanowy.data.flatResourceFlow
|
||||
import io.github.wulkanowy.data.logResourceStatus
|
||||
import io.github.wulkanowy.data.onResourceData
|
||||
import io.github.wulkanowy.data.onResourceError
|
||||
import io.github.wulkanowy.data.onResourceNotLoading
|
||||
import io.github.wulkanowy.data.onResourceSuccess
|
||||
import io.github.wulkanowy.data.repositories.MessageRepository
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||
@ -14,9 +19,11 @@ import io.github.wulkanowy.ui.base.BasePresenter
|
||||
import io.github.wulkanowy.ui.base.ErrorHandler
|
||||
import io.github.wulkanowy.utils.AnalyticsHelper
|
||||
import io.github.wulkanowy.utils.toFormattedString
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class MessagePreviewPresenter @Inject constructor(
|
||||
errorHandler: ErrorHandler,
|
||||
@ -26,17 +33,17 @@ class MessagePreviewPresenter @Inject constructor(
|
||||
private val analytics: AnalyticsHelper
|
||||
) : BasePresenter<MessagePreviewView>(errorHandler, studentRepository) {
|
||||
|
||||
var messageWithAttachments: MessageWithAttachment? = null
|
||||
private var messageWithAttachments: MessageWithAttachment? = null
|
||||
|
||||
private lateinit var lastError: Throwable
|
||||
|
||||
private var retryCallback: () -> Unit = {}
|
||||
|
||||
fun onAttachView(view: MessagePreviewView, message: Message?) {
|
||||
fun onAttachView(view: MessagePreviewView, message: Message) {
|
||||
super.onAttachView(view)
|
||||
view.initView()
|
||||
errorHandler.showErrorMessage = ::showErrorViewOnError
|
||||
loadData(requireNotNull(message))
|
||||
loadData(message)
|
||||
}
|
||||
|
||||
private fun onMessageLoadRetry(message: Message) {
|
||||
@ -74,6 +81,7 @@ class MessagePreviewPresenter @Inject constructor(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
delay(1.seconds)
|
||||
view?.run {
|
||||
showMessage(messageNotExists)
|
||||
popView()
|
||||
@ -172,13 +180,51 @@ class MessagePreviewPresenter @Inject constructor(
|
||||
return true
|
||||
}
|
||||
|
||||
private fun restoreMessage() {
|
||||
val message = messageWithAttachments?.message ?: return
|
||||
|
||||
view?.run {
|
||||
showContent(false)
|
||||
showProgress(true)
|
||||
showOptions(
|
||||
show = false,
|
||||
isReplayable = false,
|
||||
isRestorable = false,
|
||||
)
|
||||
showErrorView(false)
|
||||
}
|
||||
Timber.i("Restore message ${message.messageGlobalKey}")
|
||||
presenterScope.launch {
|
||||
runCatching {
|
||||
val student = studentRepository.getCurrentStudent(decryptPass = true)
|
||||
val mailbox = messageRepository.getMailboxByStudent(student)
|
||||
messageRepository.restoreMessages(student, mailbox, listOfNotNull(message))
|
||||
}
|
||||
.onFailure {
|
||||
retryCallback = { onMessageRestore() }
|
||||
errorHandler.dispatch(it)
|
||||
}
|
||||
.onSuccess {
|
||||
view?.run {
|
||||
showMessage(restoreMessageSuccessString)
|
||||
popView()
|
||||
}
|
||||
}
|
||||
view?.showProgress(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteMessage() {
|
||||
messageWithAttachments?.message ?: return
|
||||
|
||||
view?.run {
|
||||
showContent(false)
|
||||
showProgress(true)
|
||||
showOptions(show = false, isReplayable = false)
|
||||
showOptions(
|
||||
show = false,
|
||||
isReplayable = false,
|
||||
isRestorable = false,
|
||||
)
|
||||
showErrorView(false)
|
||||
}
|
||||
|
||||
@ -187,8 +233,7 @@ class MessagePreviewPresenter @Inject constructor(
|
||||
presenterScope.launch {
|
||||
runCatching {
|
||||
val student = studentRepository.getCurrentStudent(decryptPass = true)
|
||||
val mailbox = messageRepository.getMailboxByStudent(student)
|
||||
messageRepository.deleteMessage(student, mailbox, messageWithAttachments?.message!!)
|
||||
messageRepository.deleteMessage(student, messageWithAttachments?.message!!)
|
||||
}.onFailure {
|
||||
retryCallback = { onMessageDelete() }
|
||||
errorHandler.dispatch(it)
|
||||
@ -213,6 +258,11 @@ class MessagePreviewPresenter @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun onMessageRestore(): Boolean {
|
||||
restoreMessage()
|
||||
return true
|
||||
}
|
||||
|
||||
fun onMessageDelete(): Boolean {
|
||||
deleteMessage()
|
||||
return true
|
||||
@ -222,15 +272,9 @@ class MessagePreviewPresenter @Inject constructor(
|
||||
view?.apply {
|
||||
showOptions(
|
||||
show = messageWithAttachments?.message != null,
|
||||
isReplayable = messageWithAttachments?.message?.folderId != MessageFolder.SENT.id,
|
||||
isReplayable = messageWithAttachments?.message?.folderId == MessageFolder.RECEIVED.id,
|
||||
isRestorable = messageWithAttachments?.message?.folderId == MessageFolder.TRASHED.id,
|
||||
)
|
||||
messageWithAttachments?.message?.let {
|
||||
when (it.folderId == MessageFolder.TRASHED.id) {
|
||||
true -> setDeletedOptionsLabels()
|
||||
false -> setNotDeletedOptionsLabels()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,8 @@ interface MessagePreviewView : BaseView {
|
||||
|
||||
val unmuteMessageSuccessString: String
|
||||
|
||||
val restoreMessageSuccessString: String
|
||||
|
||||
val messageNoSubjectString: String
|
||||
|
||||
val printHTML: String
|
||||
@ -35,11 +37,7 @@ interface MessagePreviewView : BaseView {
|
||||
|
||||
fun setErrorRetryCallback(callback: () -> Unit)
|
||||
|
||||
fun showOptions(show: Boolean, isReplayable: Boolean)
|
||||
|
||||
fun setDeletedOptionsLabels()
|
||||
|
||||
fun setNotDeletedOptionsLabels()
|
||||
fun showOptions(show: Boolean, isReplayable: Boolean, isRestorable: Boolean)
|
||||
|
||||
fun openMessageReply(message: Message?)
|
||||
|
||||
|
@ -203,7 +203,7 @@ class SendMessagePresenter @Inject constructor(
|
||||
subject = subject,
|
||||
content = content,
|
||||
recipients = recipients,
|
||||
mailboxId = mailbox.globalKey,
|
||||
mailbox = mailbox,
|
||||
)
|
||||
}.logResourceStatus("sending message").onEach {
|
||||
when (it) {
|
||||
|
@ -5,7 +5,9 @@ import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.View.*
|
||||
import android.view.View.GONE
|
||||
import android.view.View.INVISIBLE
|
||||
import android.view.View.VISIBLE
|
||||
import android.widget.CompoundButton
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.view.ActionMode
|
||||
@ -64,10 +66,12 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
if (presenter.folder == MessageFolder.TRASHED) {
|
||||
val menuItem = menu.findItem(R.id.messageTabContextMenuDelete)
|
||||
menuItem.setTitle(R.string.message_delete_forever)
|
||||
}
|
||||
val isTrashFolder = presenter.folder == MessageFolder.TRASHED
|
||||
|
||||
menu.findItem(R.id.messageTabContextMenuDelete).setVisible(!isTrashFolder)
|
||||
menu.findItem(R.id.messageTabContextMenuDeleteForever).setVisible(isTrashFolder)
|
||||
menu.findItem(R.id.messageTabContextMenuRestore).setVisible(isTrashFolder)
|
||||
|
||||
return presenter.onPrepareActionMode()
|
||||
}
|
||||
|
||||
@ -79,6 +83,8 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
|
||||
override fun onActionItemClicked(mode: ActionMode, menu: MenuItem): Boolean {
|
||||
when (menu.itemId) {
|
||||
R.id.messageTabContextMenuDelete -> presenter.onActionModeSelectDelete()
|
||||
R.id.messageTabContextMenuRestore -> presenter.onActionModeSelectRestore()
|
||||
R.id.messageTabContextMenuDeleteForever -> presenter.onActionModeSelectDelete()
|
||||
R.id.messageTabContextMenuSelectAll -> presenter.onActionModeSelectCheckAll()
|
||||
}
|
||||
return true
|
||||
|
@ -121,8 +121,27 @@ class MessageTabPresenter @Inject constructor(
|
||||
return true
|
||||
}
|
||||
|
||||
fun onActionModeSelectRestore() {
|
||||
Timber.i("Restore ${messagesToDelete.size} messages")
|
||||
val messageList = messagesToDelete.toList()
|
||||
|
||||
presenterScope.launch {
|
||||
view?.run {
|
||||
showProgress(true)
|
||||
showContent(false)
|
||||
showActionMode(false)
|
||||
}
|
||||
runCatching {
|
||||
val student = studentRepository.getCurrentStudent(true)
|
||||
messageRepository.restoreMessages(student, selectedMailbox, messageList)
|
||||
}
|
||||
.onFailure(errorHandler::dispatch)
|
||||
.onSuccess { view?.showMessage(R.string.message_messages_restored) }
|
||||
}
|
||||
}
|
||||
|
||||
fun onActionModeSelectDelete() {
|
||||
Timber.i("Delete ${messagesToDelete.size} messages)")
|
||||
Timber.i("Delete ${messagesToDelete.size} messages")
|
||||
val messageList = messagesToDelete.toList()
|
||||
|
||||
presenterScope.launch {
|
||||
@ -134,7 +153,7 @@ class MessageTabPresenter @Inject constructor(
|
||||
|
||||
runCatching {
|
||||
val student = studentRepository.getCurrentStudent(true)
|
||||
messageRepository.deleteMessages(student, selectedMailbox, messageList)
|
||||
messageRepository.deleteMessages(student, messageList)
|
||||
}
|
||||
.onFailure(errorHandler::dispatch)
|
||||
.onSuccess { view?.showMessage(R.string.message_messages_deleted) }
|
||||
|
@ -2,6 +2,7 @@ package io.github.wulkanowy.ui.modules.schoolannouncement
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
|
||||
import io.github.wulkanowy.databinding.ItemSchoolAnnouncementBinding
|
||||
@ -29,6 +30,10 @@ class SchoolAnnouncementAdapter @Inject constructor() :
|
||||
schoolAnnouncementItemDate.text = item.date.toFormattedString()
|
||||
schoolAnnouncementItemType.text = item.subject
|
||||
schoolAnnouncementItemContent.text = item.content.parseUonetHtml()
|
||||
with(schoolAnnouncementItemAuthor) {
|
||||
text = item.author
|
||||
isVisible = !item.author.isNullOrBlank()
|
||||
}
|
||||
|
||||
root.setOnClickListener { onItemClickListener(item) }
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.SeekBarPreference
|
||||
import com.yariksoffice.lingver.Lingver
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.github.wulkanowy.R
|
||||
@ -36,6 +37,15 @@ class AppearanceFragment : PreferenceFragmentCompat(),
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.scheme_preferences_appearance, rootKey)
|
||||
val attendanceTargetPref =
|
||||
findPreference<SeekBarPreference>(requireContext().getString(R.string.pref_key_attendance_target))!!
|
||||
attendanceTargetPref.setOnPreferenceChangeListener { _, newValueObj ->
|
||||
val newValue = (((newValueObj as Int).toDouble() + 2.5) / 5).toInt() * 5
|
||||
attendanceTargetPref.value =
|
||||
newValue.coerceIn(attendanceTargetPref.min, attendanceTargetPref.max)
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
|
@ -104,7 +104,14 @@ class TimetableWidgetFactory(
|
||||
private suspend fun getLessons(
|
||||
student: Student, semester: Semester, date: LocalDate
|
||||
): List<Timetable> {
|
||||
val timetable = timetableRepository.getTimetable(student, semester, date, date, false)
|
||||
val timetable = timetableRepository.getTimetable(
|
||||
student = student,
|
||||
semester = semester,
|
||||
start = date,
|
||||
end = date,
|
||||
forceRefresh = false,
|
||||
isFromAppWidget = true
|
||||
)
|
||||
val lessons = timetable.toFirstResult().dataOrThrow.lessons
|
||||
return lessons.sortedBy { it.number }
|
||||
}
|
||||
|
@ -0,0 +1,34 @@
|
||||
package io.github.wulkanowy.utils
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class AppWidgetUpdater @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val appWidgetManager: AppWidgetManager
|
||||
) {
|
||||
|
||||
fun updateAllAppWidgetsByProvider(providerClass: KClass<out BroadcastReceiver>) {
|
||||
try {
|
||||
val ids = appWidgetManager.getAppWidgetIds(ComponentName(context, providerClass.java))
|
||||
if (ids.isEmpty()) return
|
||||
|
||||
val intent = Intent(context, providerClass.java)
|
||||
.apply {
|
||||
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
|
||||
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
|
||||
}
|
||||
|
||||
context.sendBroadcast(intent)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to update all widgets for provider $providerClass")
|
||||
}
|
||||
}
|
||||
}
|
@ -10,19 +10,19 @@ import io.github.wulkanowy.sdk.scrapper.attendance.AttendanceCategory
|
||||
* (https://www.vulcan.edu.pl/vulcang_files/user/AABW/AABW-PDF/uonetplus/uonetplus_Frekwencja-liczby-obecnych-nieobecnych.pdf)
|
||||
*/
|
||||
|
||||
private inline val AttendanceSummary.allPresences: Double
|
||||
get() = presence.toDouble() + absenceForSchoolReasons + lateness + latenessExcused
|
||||
inline val AttendanceSummary.allPresences: Int
|
||||
get() = presence + absenceForSchoolReasons + lateness + latenessExcused
|
||||
|
||||
private inline val AttendanceSummary.allAbsences: Double
|
||||
get() = absence.toDouble() + absenceExcused
|
||||
inline val AttendanceSummary.allAbsences: Int
|
||||
get() = absence + absenceExcused
|
||||
|
||||
inline val Attendance.isExcusableOrNotExcused: Boolean
|
||||
get() = (excusable || ((absence || lateness) && !excused)) && excuseStatus == null
|
||||
|
||||
fun AttendanceSummary.calculatePercentage() = calculatePercentage(allPresences, allAbsences)
|
||||
fun AttendanceSummary.calculatePercentage() = calculatePercentage(allPresences.toDouble(), allAbsences.toDouble())
|
||||
|
||||
fun List<AttendanceSummary>.calculatePercentage(): Double {
|
||||
return calculatePercentage(sumOf { it.allPresences }, sumOf { it.allAbsences })
|
||||
return calculatePercentage(sumOf { it.allPresences.toDouble() }, sumOf { it.allAbsences.toDouble() })
|
||||
}
|
||||
|
||||
private fun calculatePercentage(presence: Double, absence: Double): Double {
|
||||
|
@ -4,30 +4,31 @@ import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.core.os.BundleCompat
|
||||
import java.io.Serializable
|
||||
|
||||
// Even though API was introduced in 33, we use 34 as 33 is bugged in some scenarios.
|
||||
|
||||
inline fun <reified T : Serializable> Bundle.serializable(key: String): T = when {
|
||||
Build.VERSION.SDK_INT >= 33 -> getSerializable(key, T::class.java)!!
|
||||
Build.VERSION.SDK_INT >= 34 -> getSerializable(key, T::class.java)!!
|
||||
else -> @Suppress("DEPRECATION") getSerializable(key) as T
|
||||
}
|
||||
|
||||
inline fun <reified T : Serializable> Bundle.nullableSerializable(key: String): T? = when {
|
||||
Build.VERSION.SDK_INT >= 33 -> getSerializable(key, T::class.java)
|
||||
Build.VERSION.SDK_INT >= 34 -> getSerializable(key, T::class.java)
|
||||
else -> @Suppress("DEPRECATION") getSerializable(key) as T?
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
inline fun <reified T : Parcelable> Bundle.parcelableArray(key: String): Array<T>? = when {
|
||||
Build.VERSION.SDK_INT >= 33 -> getParcelableArray(key, T::class.java)
|
||||
else -> @Suppress("DEPRECATION") getParcelableArray(key) as Array<T>?
|
||||
}
|
||||
inline fun <reified T : Parcelable> Bundle.parcelableArray(key: String): Array<T>? =
|
||||
BundleCompat.getParcelableArray(this, key, T::class.java) as Array<T>?
|
||||
|
||||
inline fun <reified T : Serializable> Intent.serializable(key: String): T = when {
|
||||
Build.VERSION.SDK_INT >= 33 -> getSerializableExtra(key, T::class.java)!!
|
||||
Build.VERSION.SDK_INT >= 34 -> getSerializableExtra(key, T::class.java)!!
|
||||
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as T
|
||||
}
|
||||
|
||||
inline fun <reified T : Serializable> Intent.nullableSerializable(key: String): T? = when {
|
||||
Build.VERSION.SDK_INT >= 33 -> getSerializableExtra(key, T::class.java)
|
||||
Build.VERSION.SDK_INT >= 34 -> getSerializableExtra(key, T::class.java)
|
||||
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as T?
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException
|
||||
import io.github.wulkanowy.sdk.scrapper.exception.ScrapperException
|
||||
import io.github.wulkanowy.sdk.scrapper.exception.ServiceUnavailableException
|
||||
import io.github.wulkanowy.sdk.scrapper.exception.VulcanException
|
||||
import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException
|
||||
import io.github.wulkanowy.sdk.scrapper.login.NotLoggedInException
|
||||
import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException
|
||||
import okhttp3.internal.http2.StreamResetException
|
||||
@ -34,6 +35,7 @@ fun Resources.getErrorString(error: Throwable): String = when (error) {
|
||||
is ServiceUnavailableException -> R.string.error_service_unavailable
|
||||
is FeatureDisabledException -> R.string.error_feature_disabled
|
||||
is FeatureNotAvailableException -> R.string.error_feature_not_available
|
||||
is BadCredentialsException -> R.string.error_password_invalid
|
||||
is AccountInactiveException -> R.string.error_account_inactive
|
||||
is VulcanException -> R.string.error_unknown_uonet
|
||||
is ScrapperException -> R.string.error_unknown_app
|
||||
|
@ -1,42 +0,0 @@
|
||||
package io.github.wulkanowy.utils
|
||||
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import timber.log.Timber
|
||||
|
||||
fun Sdk.init(student: Student): Sdk {
|
||||
email = student.email
|
||||
password = student.password
|
||||
symbol = student.symbol
|
||||
schoolSymbol = student.schoolSymbol
|
||||
studentId = student.studentId
|
||||
classId = student.classId
|
||||
emptyCookieJarInterceptor = true
|
||||
|
||||
if (Sdk.Mode.valueOf(student.loginMode) == Sdk.Mode.HEBE) {
|
||||
mobileBaseUrl = student.mobileBaseUrl
|
||||
} else {
|
||||
scrapperBaseUrl = student.scrapperBaseUrl
|
||||
domainSuffix = student.scrapperDomainSuffix
|
||||
loginType = Sdk.ScrapperLoginType.valueOf(student.loginType)
|
||||
}
|
||||
|
||||
mode = Sdk.Mode.valueOf(student.loginMode)
|
||||
mobileBaseUrl = student.mobileBaseUrl
|
||||
keyId = student.certificateKey
|
||||
privatePem = student.privateKey
|
||||
|
||||
Timber.d("Sdk in ${student.loginMode} mode reinitialized")
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
fun Sdk.switchSemester(semester: Semester): Sdk {
|
||||
return switchDiary(
|
||||
diaryId = semester.diaryId,
|
||||
kindergartenDiaryId = semester.kindergartenDiaryId,
|
||||
schoolYear = semester.schoolYear,
|
||||
unitId = semester.unitId,
|
||||
)
|
||||
}
|
@ -5,17 +5,21 @@ import java.net.CookiePolicy
|
||||
import java.net.CookieStore
|
||||
import java.net.HttpCookie
|
||||
import java.net.URI
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import android.webkit.CookieManager as WebkitCookieManager
|
||||
import java.net.CookieManager as JavaCookieManager
|
||||
|
||||
class WebkitCookieManagerProxy : JavaCookieManager(null, CookiePolicy.ACCEPT_ALL) {
|
||||
@Singleton
|
||||
class WebkitCookieManagerProxy @Inject constructor() :
|
||||
JavaCookieManager(null, CookiePolicy.ACCEPT_ALL) {
|
||||
|
||||
private val webkitCookieManager: WebkitCookieManager? = getWebkitCookieManager()
|
||||
val webkitCookieManager: WebkitCookieManager? = getCookieManager()
|
||||
|
||||
/**
|
||||
* @see [https://stackoverflow.com/a/70354583/6695449]
|
||||
*/
|
||||
private fun getWebkitCookieManager(): WebkitCookieManager? {
|
||||
private fun getCookieManager(): WebkitCookieManager? {
|
||||
return try {
|
||||
WebkitCookieManager.getInstance()
|
||||
} catch (e: AndroidRuntimeException) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
Wersja 2.4.2
|
||||
Wersja 2.5.2
|
||||
|
||||
- naprawiliśmy crash przy przełączaniu uczniów, motywów i języków
|
||||
- naprawiliśmy crash przy dodawaniu dodatkowych lekcji
|
||||
- naprawiliśmy obsługę błędów widżetach
|
||||
— naprawiliśmy omyłkowe wyświetlanie ekranu z wymaganą autoryzacją numerem PESEL
|
||||
— naprawiliśmy kilka problemów ze stabilnością
|
||||
— poprawiliśmy wyświetlanie kolorów we frekwencji
|
||||
|
||||
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases
|
||||
|
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M19,7H9C7.9,7 7,7.9 7,9v10c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2V9C21,7.9 20.1,7 19,7zM19,9v2H9V9H19zM13,15v-2h2v2H13zM15,17v2h-2v-2H15zM11,15H9v-2h2V15zM17,13h2v2h-2V13zM9,17h2v2H9V17zM17,19v-2h2v2H17zM6,17H5c-1.1,0 -2,-0.9 -2,-2V5c0,-1.1 0.9,-2 2,-2h10c1.1,0 2,0.9 2,2v1h-2V5H5v10h1V17z"/>
|
||||
</vector>
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M14.12,10.47L12,12.59l-2.13,-2.12 -1.41,1.41L10.59,14l-2.12,2.12 1.41,1.41L12,15.41l2.12,2.12 1.41,-1.41L13.41,14l2.12,-2.12zM15.5,4l-1,-1h-5l-1,1H5v2h14V4zM6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM8,9h8v10H8V9z" />
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_menu_message_restore.xml
Normal file
9
app/src/main/res/drawable/ic_menu_message_restore.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4zM6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L18,7L6,7v12zM8,14L8,9h8v10L8,19v-5zM10,18h4v-4h2l-4,-4 -4,4h2z" />
|
||||
</vector>
|
@ -7,15 +7,18 @@
|
||||
tools:context=".ui.modules.captcha.CaptchaDialog">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/captcha_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingVertical="10dp"
|
||||
android:text="@string/captcha_dialog_title"
|
||||
app:layout_constraintBottom_toBottomOf="@id/captcha_close"
|
||||
app:layout_constraintEnd_toStartOf="@id/captcha_refresh"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/captcha_refresh"
|
||||
@ -41,11 +44,29 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/captcha_toolbar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="captcha_title,captcha_close,captcha_refresh" />
|
||||
|
||||
<WebView
|
||||
android:id="@+id/captcha_webview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/captcha_description"
|
||||
app:layout_constraintDimensionRatio="1"
|
||||
app:layout_constraintTop_toBottomOf="@id/captcha_toolbar" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/captcha_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="20dp"
|
||||
android:paddingVertical="10dp"
|
||||
android:text="@string/captcha_dialog_description"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/captcha_close" />
|
||||
app:layout_constraintTop_toBottomOf="@id/captcha_webview" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
103
app/src/main/res/layout/fragment_attendance_calculator.xml
Normal file
103
app/src/main/res/layout/fragment_attendance_calculator.xml
Normal file
@ -0,0 +1,103 @@
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.modules.attendance.calculator.AttendanceCalculatorFragment">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/attendanceCalculatorSwipe"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/attendanceCalculatorRecycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:listitem="@layout/item_attendance_calculator_header" />
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/attendanceCalculatorProgress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/attendanceCalculatorEmpty"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:visibility="invisible"
|
||||
tools:ignore="UseCompoundDrawables">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="100dp"
|
||||
app:srcCompat="@drawable/ic_main_attendance"
|
||||
app:tint="?colorOnBackground"
|
||||
tools:ignore="contentDescription" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/attendance_no_items"
|
||||
android:textSize="20sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/attendanceCalculatorError"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:visibility="invisible"
|
||||
tools:ignore="UseCompoundDrawables"
|
||||
tools:visibility="invisible">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="100dp"
|
||||
app:srcCompat="@drawable/ic_error"
|
||||
app:tint="?colorOnBackground"
|
||||
tools:ignore="contentDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/attendanceCalculatorErrorMessage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:gravity="center"
|
||||
android:padding="8dp"
|
||||
android:text="@string/error_unknown"
|
||||
android:textSize="20sp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/attendanceCalculatorErrorDetails"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:text="@string/all_details" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/attendanceCalculatorErrorRetry"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/all_retry" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
111
app/src/main/res/layout/item_attendance_calculator_header.xml
Normal file
111
app/src/main/res/layout/item_attendance_calculator_header.xml
Normal file
@ -0,0 +1,111 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingBottom="6dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/attendanceCalculatorPercentage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="right|center_vertical"
|
||||
android:minWidth="32dp"
|
||||
android:minHeight="36dp"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="50" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/attendanceCalculatorPercentagePercentSign"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="%"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/attendanceCalculatorPercentage"
|
||||
app:layout_constraintStart_toEndOf="@+id/attendanceCalculatorPercentage" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/attendanceCalculatorSummaryValues"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="left"
|
||||
android:maxLines="1"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:textSize="13sp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/attendanceCalculatorSummaryDot"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="@+id/attendanceCalculatorTitle"
|
||||
app:layout_constraintTop_toBottomOf="@+id/attendanceCalculatorTitle"
|
||||
tools:text="11/123 obecności" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/attendanceCalculatorSummaryDot"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
android:gravity="center"
|
||||
android:text="·"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:textSize="13sp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/attendanceCalculatorSummaryBalance"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toEndOf="@+id/attendanceCalculatorSummaryValues"
|
||||
app:layout_constraintTop_toBottomOf="@+id/attendanceCalculatorTitle" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/attendanceCalculatorSummaryBalance"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="left"
|
||||
android:maxLines="1"
|
||||
android:minWidth="24dp"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:textSize="13sp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/attendanceCalculatorWarning"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toEndOf="@+id/attendanceCalculatorSummaryDot"
|
||||
app:layout_constraintTop_toBottomOf="@+id/attendanceCalculatorTitle"
|
||||
tools:text="12 powyżej celu" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/attendanceCalculatorTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toStartOf="@+id/attendanceCalculatorWarning"
|
||||
app:layout_constraintStart_toEndOf="@+id/attendanceCalculatorPercentagePercentSign"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Informatyka" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/attendanceCalculatorWarning"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription=""
|
||||
android:layout_marginStart="12dp"
|
||||
android:gravity="center_vertical|right"
|
||||
android:minWidth="24dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_all_round_mark"
|
||||
app:tint="?colorPrimary"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -11,27 +11,41 @@
|
||||
android:id="@+id/schoolAnnouncementItemDate"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="15dp"
|
||||
android:layout_marginHorizontal="15dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:textSize="15sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/schoolAnnouncementItemAuthor"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="@tools:sample/date/ddmmyy" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/schoolAnnouncementItemAuthor"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="15dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:gravity="end"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:textSize="15sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/schoolAnnouncementItemDate"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="@tools:sample/full_names" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/schoolAnnouncementItemType"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="5dp"
|
||||
android:layout_marginHorizontal="15dp"
|
||||
android:layout_marginVertical="5dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintEnd_toEndOf="@id/schoolAnnouncementItemDate"
|
||||
app:layout_constraintStart_toStartOf="@id/schoolAnnouncementItemDate"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/schoolAnnouncementItemDate"
|
||||
app:layout_goneMarginEnd="0dp"
|
||||
tools:text="@tools:sample/lorem" />
|
||||
@ -40,6 +54,7 @@
|
||||
android:id="@+id/schoolAnnouncementItemContent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="15dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="15dp"
|
||||
android:ellipsize="end"
|
||||
@ -47,8 +62,8 @@
|
||||
android:maxLines="2"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@+id/schoolAnnouncementItemType"
|
||||
app:layout_constraintStart_toStartOf="@id/schoolAnnouncementItemDate"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/schoolAnnouncementItemType"
|
||||
tools:text="@tools:sample/lorem/random" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
@ -24,6 +24,7 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0"
|
||||
tools:text="5" />
|
||||
|
||||
<TextView
|
||||
@ -38,7 +39,7 @@
|
||||
android:textSize="15sp"
|
||||
app:layout_constraintEnd_toStartOf="@id/timetableItemTimeBarrier"
|
||||
app:layout_constraintStart_toEndOf="@+id/timetableItemTimeStart"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/timetableItemTimeStart"
|
||||
tools:text="@tools:sample/lorem" />
|
||||
|
||||
<TextView
|
||||
@ -52,6 +53,8 @@
|
||||
app:layout_constraintBottom_toTopOf="@id/timetableItemTimeFinish"
|
||||
app:layout_constraintStart_toEndOf="@id/timetableItemNumber"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="11:11" />
|
||||
|
||||
<TextView
|
||||
@ -129,8 +132,8 @@
|
||||
android:textColor="?colorTimetableChange"
|
||||
android:textSize="13sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/timetableItemTeacher"
|
||||
app:layout_constraintTop_toTopOf="@+id/timetableItemTimeFinish"
|
||||
app:layout_constraintStart_toEndOf="@id/timetableItemTimeFinish"
|
||||
app:layout_constraintTop_toTopOf="@id/timetableItemTimeFinish"
|
||||
tools:text="Lekcja odwołana: uczniowie zwolnieni do domu"
|
||||
tools:visibility="gone" />
|
||||
|
||||
@ -179,7 +182,7 @@
|
||||
android:visibility="gone"
|
||||
app:backgroundTint="?colorPrimary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/timetableItemTimeStart"
|
||||
tools:text="jeszcze 15 min"
|
||||
tools:visibility="visible" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
140
app/src/main/res/layout/pref_target_attendance.xml
Normal file
140
app/src/main/res/layout/pref_target_attendance.xml
Normal file
@ -0,0 +1,140 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Taken from https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-camerax-release/preference/preference/res/layout/preference_widget_seekbar_material.xml.
|
||||
|
||||
~ Copyright (C) 2018 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<!-- Layout used by SeekBarPreference for the seekbar widget style. -->
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?android:attr/listPreferredItemHeightSmall"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingRight="?android:attr/listPreferredItemPaddingRight"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:baselineAligned="false">
|
||||
<include layout="@layout/image_frame"/>
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false">
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
<TextView
|
||||
android:id="@android:id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
android:ellipsize="marquee"/>
|
||||
<TextView
|
||||
android:id="@android:id/summary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@android:id/title"
|
||||
android:layout_alignLeft="@android:id/title"
|
||||
android:layout_alignStart="@android:id/title"
|
||||
android:layout_gravity="start"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:maxLines="4"
|
||||
style="@style/PreferenceSummaryTextStyle"/>
|
||||
</RelativeLayout>
|
||||
<!-- Using UnPressableLinearLayout as a workaround to disable the pressed state propagation
|
||||
to the children of this container layout. Otherwise, the animated pressed state will also
|
||||
play for the thumb in the AbsSeekBar in addition to the preference's ripple background.
|
||||
The background of the SeekBar is also set to null to disable the ripple background -->
|
||||
<androidx.preference.UnPressableLinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingLeft="0dp"
|
||||
android:paddingStart="0dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false">
|
||||
<!-- The total height of the Seekbar widget's area should be 48dp - this allows for an
|
||||
increased touch area so you do not need to exactly tap the thumb to move it. However,
|
||||
setting the Seekbar height directly causes the thumb and seekbar to be misaligned on
|
||||
API 22 and 23 - so instead we just set 15dp padding above and below, to account for the
|
||||
18dp default height of the Seekbar thumb for a total of 48dp.
|
||||
Note: we set 0dp padding at the start and end of this seekbar to allow it to properly
|
||||
fit into the layout, but this means that there's no leeway on either side for touch
|
||||
input - this might be something we should reconsider down the line. -->
|
||||
<SeekBar
|
||||
android:id="@+id/seekbar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="@dimen/preference_seekbar_padding_horizontal"
|
||||
android:paddingStart="@dimen/preference_seekbar_padding_horizontal"
|
||||
android:paddingRight="@dimen/preference_seekbar_padding_horizontal"
|
||||
android:paddingEnd="@dimen/preference_seekbar_padding_horizontal"
|
||||
android:paddingTop="@dimen/preference_seekbar_padding_vertical"
|
||||
android:paddingBottom="@dimen/preference_seekbar_padding_vertical"
|
||||
|
||||
android:background="@null"/>
|
||||
<!-- If the value is shown, we reserve a minimum width of 36dp to allow for consistent
|
||||
seekbar width for smaller values. If the value is ~4 or more digits, it will expand
|
||||
into the seekbar width. -->
|
||||
<TextView
|
||||
android:id="@+id/seekbar_value"
|
||||
android:minWidth="@dimen/preference_seekbar_value_minWidth"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingRight="0dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="right"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
android:ellipsize="marquee"
|
||||
android:fadingEdge="horizontal"
|
||||
android:scrollbars="none"/>
|
||||
<!-- Wulkanowy start -->
|
||||
<TextView
|
||||
android:minWidth="0dp"
|
||||
android:paddingLeft="0dp"
|
||||
android:paddingStart="0dp"
|
||||
android:paddingRight="0dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="left"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
android:ellipsize="marquee"
|
||||
android:fadingEdge="horizontal"
|
||||
android:scrollbars="none"
|
||||
android:text="%"
|
||||
/>
|
||||
<!-- Wulkanowy end -->
|
||||
</androidx.preference.UnPressableLinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
@ -11,6 +11,7 @@
|
||||
android:gravity="center"
|
||||
android:maxLength="5"
|
||||
android:minWidth="20dp"
|
||||
android:padding="1dp"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold"
|
||||
|
@ -1,6 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/attendanceMenuCalculator"
|
||||
android:icon="@drawable/ic_menu_attendance_calculator"
|
||||
android:orderInCategory="0"
|
||||
android:title="@string/attendance_calculator_button"
|
||||
app:iconTint="@color/material_on_surface_emphasis_medium"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/attendanceMenuSummary"
|
||||
android:icon="@drawable/ic_menu_attendance_summary"
|
||||
|
@ -29,6 +29,13 @@
|
||||
android:title="@string/message_forward"
|
||||
app:iconTint="@color/material_on_surface_emphasis_medium"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/messagePreviewMenuRestore"
|
||||
android:icon="@drawable/ic_menu_message_restore"
|
||||
android:orderInCategory="1"
|
||||
android:title="@string/message_restore_from_trash"
|
||||
app:iconTint="@color/material_on_surface_emphasis_medium"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/messagePreviewMenuDelete"
|
||||
android:icon="@drawable/ic_menu_message_delete"
|
||||
@ -36,6 +43,13 @@
|
||||
android:title="@string/message_move_to_trash"
|
||||
app:iconTint="@color/material_on_surface_emphasis_medium"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/messagePreviewMenuDeleteForever"
|
||||
android:icon="@drawable/ic_menu_message_delete_forever"
|
||||
android:orderInCategory="1"
|
||||
android:title="@string/message_delete_forever"
|
||||
app:iconTint="@color/material_on_surface_emphasis_medium"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/messagePreviewMenuMute"
|
||||
android:icon="@drawable/ic_settings_notifications"
|
||||
|
@ -1,6 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/messageTabContextMenuRestore"
|
||||
android:icon="@drawable/ic_menu_message_restore"
|
||||
android:orderInCategory="1"
|
||||
android:title="@string/message_restore_from_trash"
|
||||
app:iconTint="@color/material_on_surface_emphasis_medium"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:id="@+id/messageTabContextMenuDelete"
|
||||
android:icon="@drawable/ic_menu_message_delete"
|
||||
@ -8,6 +15,13 @@
|
||||
android:title="@string/message_move_to_trash"
|
||||
app:iconTint="@color/material_on_surface_emphasis_medium"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:id="@+id/messageTabContextMenuDeleteForever"
|
||||
android:icon="@drawable/ic_menu_message_delete_forever"
|
||||
android:orderInCategory="1"
|
||||
android:title="@string/message_delete_forever"
|
||||
app:iconTint="@color/material_on_surface_emphasis_medium"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:id="@+id/messageTabContextMenuSelectAll"
|
||||
android:icon="@drawable/ic_message_select_all"
|
||||
|
@ -1,5 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="sort_alphabetically">Abecedně</string>
|
||||
<string name="sort_by_date">Podle data</string>
|
||||
<string name="sort_by_average">Podle průměru</string>
|
||||
<string name="sort_by_attendance_percentage">Podle procenta docházky</string>
|
||||
<string name="sort_by_subject_attendance_balance">Podle rovnováhy docházky předmětu</string>
|
||||
<string-array name="app_theme_entries" tools:ignore="InconsistentArrays">
|
||||
<item>Světlý</item>
|
||||
<item>Tmavý</item>
|
||||
@ -31,11 +36,6 @@
|
||||
<item>0,5</item>
|
||||
<item>0,75</item>
|
||||
</string-array>
|
||||
<string-array name="grade_sorting_mode_entries">
|
||||
<item>Abecedně</item>
|
||||
<item>Podle data</item>
|
||||
<item>Podle průměru</item>
|
||||
</string-array>
|
||||
<string-array name="grade_color_scheme_entries">
|
||||
<item>Dzienniczek+</item>
|
||||
<item>Wulkanowy</item>
|
||||
@ -59,7 +59,7 @@
|
||||
<string-array name="dashboard_tile_entries">
|
||||
<item>Šťastné číslo</item>
|
||||
<item>Nepřečtené zprávy</item>
|
||||
<item>Frekvence</item>
|
||||
<item>Docházka</item>
|
||||
<item>Lekce</item>
|
||||
<item>Známky</item>
|
||||
<item>Domácí úkoly</item>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<string name="login_title">Přihlášení</string>
|
||||
<string name="main_title">Wulkanowy</string>
|
||||
<string name="grade_title">Známky</string>
|
||||
<string name="attendance_title">Frekvence</string>
|
||||
<string name="attendance_title">Docházka</string>
|
||||
<string name="exam_title">Zkoušky</string>
|
||||
<string name="timetable_title">Plán lekce</string>
|
||||
<string name="settings_title">Nastavení</string>
|
||||
@ -56,7 +56,7 @@
|
||||
<string name="login_invalid_email">Neplatný e-mail</string>
|
||||
<string name="login_invalid_login">Místo e-mailu použijte přiřazené přihlašovací údaje</string>
|
||||
<string name="login_invalid_custom_email">Použijte přiřazené přihlašovací nebo e-mail v @%1$s</string>
|
||||
<string name="login_invalid_domain_suffix">Invalid domain suffix</string>
|
||||
<string name="login_invalid_domain_suffix">Neplatná přípona domény</string>
|
||||
<string name="login_invalid_symbol">Neplatný symbol. Pokud jej nemůžete najít, kontaktujte školu</string>
|
||||
<string name="login_invalid_symbol_definitely">Nevymýšlejte si! Pokud symbol nemůžete najít, kontaktujte školu</string>
|
||||
<string name="login_incorrect_symbol">Žák nebyl nalezen. Zkontrolujte správnost symbolu a vybrané varianty deníku UONET+</string>
|
||||
@ -64,7 +64,7 @@
|
||||
<string name="login_symbol_helper">Symbol najdete na stránce deníku v  <b>Uczeń</b>→ <b>Dostęp Mobilny</b> → <b>Wygeneruj kod dostępu</b>.\n\nUjistěte se, že jste nastavili správnou variantu deníku v poli <b>Variace deníku UONET+</b> na první přihlašovací obrazovce</string>
|
||||
<string name="login_select_student">Vyberte žáky, kteří se mají do aplikace přihlásit</string>
|
||||
<string name="login_advanced">Jiné možnosti</string>
|
||||
<string name="login_advanced_warning_mobile_api">V tomto režimu nefungují následující: šťastné číslo, statistiky třídy, shrnutí frekvencí, ospravedlnění nepřítomnosti, dokončené lekce, informace o škole a prohlížení seznamu registrovaných zařízení</string>
|
||||
<string name="login_advanced_warning_mobile_api">V tomto režimu nefungují následující: šťastné číslo, statistiky třídy, shrnutí docházky, ospravedlnění nepřítomnosti, dokončené lekce, informace o škole a prohlížení seznamu registrovaných zařízení</string>
|
||||
<string name="login_advanced_warning_scraper">Tento režim zobrazuje stejná data, která se zobrazují na webových stránkách deníka</string>
|
||||
<string name="login_advanced_warning_hybrid">Kombinace nejlepších vlastností ostatních dvou režimů. Funguje rychleji než scraper a poskytuje funkce, které nejsou k dispozici v režimu Mobile API. Je to v experimentální fázi</string>
|
||||
<string name="login_privacy_policy">Ochrana osobních údajů</string>
|
||||
@ -98,8 +98,8 @@
|
||||
<string name="main_log_in">Přihlásit se</string>
|
||||
<string name="main_session_expired">Relace vypršela</string>
|
||||
<string name="main_session_relogin">Relace vypršela. Přihlaste se prosím znovu</string>
|
||||
<string name="main_expired_credentials_description">Heslo k vašemu účtu bylo změněno. Musíte se znovu přihlásit do Wulkanového</string>
|
||||
<string name="main_expired_credentials_title">Heslo bylo změněno</string>
|
||||
<string name="main_expired_credentials_title">Heslo vypršelo nebo bylo změněno</string>
|
||||
<string name="main_expired_credentials_description">Platnost hesla k vašemu účtu vypršela nebo bylo změněno. Budete se muset znovu přihlásit do Wulkanového</string>
|
||||
<string name="main_support_title">Podpora aplikace</string>
|
||||
<string name="main_support_description">Líbí se Vám tato aplikace? Podpořte její vývoj tím, že povolíte neinvazivní reklamy, které můžete kdykoliv vypnout</string>
|
||||
<string name="main_support_positive">Zapnout reklamy</string>
|
||||
@ -264,7 +264,12 @@
|
||||
<string name="additional_lessons_end">Čas ukončení</string>
|
||||
<string name="additional_lessons_end_time_error">Čas ukončení musí být pozdější než čas zahájení</string>
|
||||
<!--Attendance-->
|
||||
<string name="attendance_summary_button">Shrnutí frekvencí</string>
|
||||
<string name="attendance_summary_button">Shrnutí docházky</string>
|
||||
<string name="attendance_calculator_button">Kalkulačka docházky</string>
|
||||
<string name="attendance_calculator_summary_balance_positive"><b>%1$d</b> nad cílem</string>
|
||||
<string name="attendance_calculator_summary_balance_neutral">přesně v cíli</string>
|
||||
<string name="attendance_calculator_summary_balance_negative"><b>%1$d</b> pod cílem</string>
|
||||
<string name="attendance_calculator_summary_values">%1$d/%2$d přítomnosti</string>
|
||||
<string name="attendance_absence_school">Nepřítomnost ze školních důvodů</string>
|
||||
<string name="attendance_absence_excused">Omluvená nepřítomnost</string>
|
||||
<string name="attendance_absence_unexcused">Neomluvená nepřítomnost</string>
|
||||
@ -282,22 +287,22 @@
|
||||
<string name="attendance_excuse_no_selection">Musíte vybrat alespoň jednu nepřítomnost!</string>
|
||||
<string name="attendance_excuse_title">Ospravedlnit</string>
|
||||
<plurals name="attendance_notify_new_items_title">
|
||||
<item quantity="one">Nové frekvence</item>
|
||||
<item quantity="few">Nové frekvence</item>
|
||||
<item quantity="many">Nové frekvence</item>
|
||||
<item quantity="other">Nové frekvence</item>
|
||||
<item quantity="one">Nová docházka</item>
|
||||
<item quantity="few">Nové docházky</item>
|
||||
<item quantity="many">Nové docházky</item>
|
||||
<item quantity="other">Nové docházky</item>
|
||||
</plurals>
|
||||
<plurals name="attendance_notify_new_items">
|
||||
<item quantity="one">%1$d nové frekvence</item>
|
||||
<item quantity="few">%1$d nové frekvence</item>
|
||||
<item quantity="many">%1$d nových frekvencí</item>
|
||||
<item quantity="other">%1$d nových frekvencí</item>
|
||||
<item quantity="one">%1$d nová docházka</item>
|
||||
<item quantity="few">%1$d nové docházky</item>
|
||||
<item quantity="many">%1$d nových docházek</item>
|
||||
<item quantity="other">%1$d nových docházek</item>
|
||||
</plurals>
|
||||
<plurals name="attendance_number_item">
|
||||
<item quantity="one">%d frekvence</item>
|
||||
<item quantity="few">%d frekvence</item>
|
||||
<item quantity="many">%d frekvencí</item>
|
||||
<item quantity="other">%d frekvencí</item>
|
||||
<item quantity="one">%d docházka</item>
|
||||
<item quantity="few">%d docházky</item>
|
||||
<item quantity="many">%d docházek</item>
|
||||
<item quantity="other">%d docházek</item>
|
||||
</plurals>
|
||||
<!--Attendance summary-->
|
||||
<string name="attendance_summary_total">Společně</string>
|
||||
@ -336,8 +341,10 @@
|
||||
<string name="message_forward">Poslat dále</string>
|
||||
<string name="message_select_all">Vybrat vše</string>
|
||||
<string name="message_unselect_all">Odznačit vše</string>
|
||||
<string name="message_restore_from_trash">Obnovit z koše</string>
|
||||
<string name="message_move_to_trash">Přesunout do koše</string>
|
||||
<string name="message_delete_forever">Odstranit natrvalo</string>
|
||||
<string name="message_restore_success">Zpráva úspěšně obnovena</string>
|
||||
<string name="message_delete_success">Zpráva byla úspěšně odstraněna</string>
|
||||
<string name="message_mailbox_type_student">žák</string>
|
||||
<string name="message_mailbox_type_parent">rodič</string>
|
||||
@ -383,6 +390,7 @@
|
||||
<item quantity="other">%1$d vybraných</item>
|
||||
</plurals>
|
||||
<string name="message_messages_deleted">Zprávy odstraněné</string>
|
||||
<string name="message_messages_restored">Obnovené zprávy</string>
|
||||
<string name="message_mailbox_chooser_title">Vyberte poštovní schránku</string>
|
||||
<string name="message_incognito_mode_on">Anonymní režim je zapnutý</string>
|
||||
<string name="message_incognito_description">Díky anonymnímu režimu není odesílatel upozorněn, když si zprávu přečtete</string>
|
||||
@ -728,6 +736,8 @@
|
||||
<string name="pref_view_grade_average_mode">Možnosti vypočítaného průměru</string>
|
||||
<string name="pref_view_grade_average_force_calc">Vynutit průměrný výpočet podle aplikace</string>
|
||||
<string name="pref_view_present">Zobrazit přítomnost</string>
|
||||
<string name="pref_attendance_target">Cílová docházka</string>
|
||||
<string name="pref_view_attendance_calculator_sorting_mode">Třídění kalkulačky docházky</string>
|
||||
<string name="pref_view_app_theme">Motiv</string>
|
||||
<string name="pref_view_expand_grade">Rozvíjení známek</string>
|
||||
<string name="pref_view_timetable_show_groups">Zobrazit skupiny vedle předmětů</string>
|
||||
@ -794,8 +804,7 @@
|
||||
<string name="pref_grades_appearance_header">Známky</string>
|
||||
<string name="pref_dashboard_appearance_header">Domů</string>
|
||||
<string name="pref_dashboard_appearance_tiles_title">Viditelnost dlaždic</string>
|
||||
<string name="pref_hidden_settings_title">Tajná nastavení</string>
|
||||
<string name="pref_attendance_appearance_view">Frekvence</string>
|
||||
<string name="pref_attendance_appearance_view">Docházka</string>
|
||||
<string name="pref_timetable_appearance_view">Plán lekce</string>
|
||||
<string name="pref_grades_advanced_header">Známky</string>
|
||||
<string name="pref_counted_average_advanced_header">Vypočítaný průměr</string>
|
||||
@ -823,7 +832,7 @@
|
||||
<string name="channel_upcoming_lessons">Nadcházející lekce</string>
|
||||
<string name="channel_debug">Ladění</string>
|
||||
<string name="channel_change_timetable">Změny plánu lekcí</string>
|
||||
<string name="channel_new_attendance">Nové frekvence</string>
|
||||
<string name="channel_new_attendance">Nové docházky</string>
|
||||
<!--Colors-->
|
||||
<string name="all_black">Černá</string>
|
||||
<string name="all_red">Červená</string>
|
||||
@ -850,14 +859,16 @@
|
||||
<string name="auth_description">Pro provoz aplikace potřebujeme potvrdit vaši identitu. Zadejte PESEL žáka <b>%1$s</b> v níže uvedeném poli</string>
|
||||
<string name="auth_button_skip">Zatím přeskočit</string>
|
||||
<!--Captcha-->
|
||||
<string name="captcha_dialog_title">Probíhá ověřování. Počkejte…</string>
|
||||
<string name="captcha_dialog_title">Webová stránka deníku VULCAN vyžaduje ověření</string>
|
||||
<string name="captcha_dialog_description"><b>Proč se mi to zobrazuje?</b>\nWebová stránka deníku, ze které Wulkanowy stahuje data, zobrazuje stejnou obrazovku jako výše, takže Wulkanowy ji musí také zobrazit, aby bylo možné získávat data z této stránky. Nedá se to obejít</string>
|
||||
<string name="captcha_verified_message">Úspěšně ověřeno</string>
|
||||
<!--Errors-->
|
||||
<string name="error_no_internet">Žádné internetové připojení</string>
|
||||
<string name="error_invalid_device_datetime">Vyskytla se chyba. Zkontrolujte hodiny svého zařízení</string>
|
||||
<string name="error_account_inactive">This account is inactive. Try logging in again</string>
|
||||
<string name="error_account_inactive">Tento účet je neaktivní. Zkuste se znovu přihlásit</string>
|
||||
<string name="error_timeout">Nelze se připojit ke deníku. Servery mohou být přetíženy. Prosím zkuste to znovu později</string>
|
||||
<string name="error_login_failed">Načítání dat se nezdařilo. Prosím zkuste to znovu později</string>
|
||||
<string name="error_password_invalid">Vaše heslo vypršelo nebo bylo změněno. Přihlaste se znovu</string>
|
||||
<string name="error_password_change_required">Je vyžadována změna hesla pro deník</string>
|
||||
<string name="error_service_unavailable">Probíhá údržba deníku UONET+. Zkuste to později znovu</string>
|
||||
<string name="error_unknown_uonet">Neznámá chyba deniku UONET+. Prosím zkuste to znovu později</string>
|
||||
@ -868,8 +879,8 @@
|
||||
<string name="error_feature_not_available">Funkce není k dispozici. Přihlaste se v jiném režimu než Mobile API</string>
|
||||
<string name="error_field_required">Toto pole je povinné</string>
|
||||
<!-- Mute system -->
|
||||
<string name="message_mute">Mute</string>
|
||||
<string name="message_unmute">Unmute</string>
|
||||
<string name="message_mute_success">You have muted this user</string>
|
||||
<string name="message_unmute_success">You have unmuted this user</string>
|
||||
<string name="message_mute">Ztlumit</string>
|
||||
<string name="message_unmute">Zrušit ztlumení</string>
|
||||
<string name="message_mute_success">Ztlumili jste tohoto uživatele</string>
|
||||
<string name="message_unmute_success">Zrušili jste ztlumení tohoto uživatele</string>
|
||||
</resources>
|
||||
|
@ -1,75 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string-array name="app_theme_entries" tools:ignore="InconsistentArrays">
|
||||
<item>Light</item>
|
||||
<item>Dark</item>
|
||||
<item>Black (AMOLED)</item>
|
||||
</string-array>
|
||||
<string-array name="app_language_entries">
|
||||
<item>System language</item>
|
||||
<item>Polski</item>
|
||||
<item>English</item>
|
||||
<item>Pусский</item>
|
||||
<item>Українська</item>
|
||||
<item>Deutsch</item>
|
||||
<item>Čeština</item>
|
||||
<item>Slovenčina</item>
|
||||
</string-array>
|
||||
<string-array name="services_interval_entries">
|
||||
<item>15 minutes</item>
|
||||
<item>30 minutes</item>
|
||||
<item>1 hour</item>
|
||||
<item>2 hours</item>
|
||||
<item>6 hours</item>
|
||||
<item>12 hours</item>
|
||||
<item>24 hours</item>
|
||||
</string-array>
|
||||
<string-array name="grade_modifier_entries">
|
||||
<item>0,00</item>
|
||||
<item>0,25</item>
|
||||
<item>0,33</item>
|
||||
<item>0,5</item>
|
||||
<item>0,75</item>
|
||||
</string-array>
|
||||
<string-array name="grade_sorting_mode_entries">
|
||||
<item>Alphabetically</item>
|
||||
<item>By date</item>
|
||||
<item>By average</item>
|
||||
</string-array>
|
||||
<string-array name="grade_color_scheme_entries">
|
||||
<item>Dzienniczek+</item>
|
||||
<item>Wulkanowy</item>
|
||||
<item>Grade colors in register</item>
|
||||
</string-array>
|
||||
<string-array name="default_expand_grade_entries">
|
||||
<item>Up to 1 at once</item>
|
||||
<item>Always expanded</item>
|
||||
<item>Unlimited expansions</item>
|
||||
</string-array>
|
||||
<string-array name="grade_average_mode_entries">
|
||||
<item>Average of grades only from selected semester</item>
|
||||
<item>Average of averages from both semesters</item>
|
||||
<item>Average of grades from the whole year</item>
|
||||
</string-array>
|
||||
<string-array name="timetable_show_gaps_entries">
|
||||
<item>Don\'t show</item>
|
||||
<item>Only between lessons</item>
|
||||
<item>Before and between lessons</item>
|
||||
</string-array>
|
||||
<string-array name="dashboard_tile_entries">
|
||||
<item>Lucky number</item>
|
||||
<item>Unread messages</item>
|
||||
<item>Attendance</item>
|
||||
<item>Lessons</item>
|
||||
<item>Grades</item>
|
||||
<item>Homework</item>
|
||||
<item>School announcements</item>
|
||||
<item>Exams</item>
|
||||
<item>Conferences</item>
|
||||
</string-array>
|
||||
<string-array name="hidden_settings_entries">
|
||||
<item>Bad attendance</item>
|
||||
<item>Notes</item>
|
||||
<item>Fake attendance</item>
|
||||
</string-array>
|
||||
</resources>
|
@ -1,5 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="sort_alphabetically">Alphabetically</string>
|
||||
<string name="sort_by_date">By date</string>
|
||||
<string name="sort_by_average">By average</string>
|
||||
<string name="sort_by_attendance_percentage">By attendance percentage</string>
|
||||
<string name="sort_by_subject_attendance_balance">By subject attendance balance</string>
|
||||
<string-array name="app_theme_entries" tools:ignore="InconsistentArrays">
|
||||
<item>Licht</item>
|
||||
<item>Dunkel</item>
|
||||
@ -31,11 +36,6 @@
|
||||
<item>0,5</item>
|
||||
<item>0,75</item>
|
||||
</string-array>
|
||||
<string-array name="grade_sorting_mode_entries">
|
||||
<item>Alphabetisch</item>
|
||||
<item>Nach Datum</item>
|
||||
<item>Nach Durchschnitt</item>
|
||||
</string-array>
|
||||
<string-array name="grade_color_scheme_entries">
|
||||
<item>Dzienniczek+</item>
|
||||
<item>Wulkanowy</item>
|
||||
|
@ -98,8 +98,8 @@
|
||||
<string name="main_log_in">Anmelden</string>
|
||||
<string name="main_session_expired">Die Sitzung ist abgelaufen</string>
|
||||
<string name="main_session_relogin">Die Sitzung ist abgelaufen, bitte loggen Sie sich erneut ein</string>
|
||||
<string name="main_expired_credentials_description">Your account password has been changed. You need to log in to Wulkanowy again</string>
|
||||
<string name="main_expired_credentials_title">Password changed</string>
|
||||
<string name="main_expired_credentials_title">Password has expired or been changed</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_support_title">Anwendungsunterstützung</string>
|
||||
<string name="main_support_description">Gefällt Ihnen diese App? Unterstützen Sie ihre Entwicklung, indem Sie nicht-invasive Werbung aktivieren, die Sie jederzeit deaktivieren können</string>
|
||||
<string name="main_support_positive">Werbung aktivieren</string>
|
||||
@ -237,6 +237,11 @@
|
||||
<string name="additional_lessons_end_time_error">Endzeit muss grösser sein als Startzeit</string>
|
||||
<!--Attendance-->
|
||||
<string name="attendance_summary_button">Übersicht über die Schulbesuch</string>
|
||||
<string name="attendance_calculator_button">Attendance calculator</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_negative"><b>%1$d</b> under target</string>
|
||||
<string name="attendance_calculator_summary_values">%1$d/%2$d presences</string>
|
||||
<string name="attendance_absence_school">Aus schulischen Gründen abwesend</string>
|
||||
<string name="attendance_absence_excused">Entschuldigte Abwesenheit</string>
|
||||
<string name="attendance_absence_unexcused">Unentschuldigtes Abwesenheit</string>
|
||||
@ -296,8 +301,10 @@
|
||||
<string name="message_forward">Weiterleiten</string>
|
||||
<string name="message_select_all">Alle auswählen</string>
|
||||
<string name="message_unselect_all">Alle abwählen</string>
|
||||
<string name="message_restore_from_trash">Restore from trash</string>
|
||||
<string name="message_move_to_trash">In Papierkorb verschieben</string>
|
||||
<string name="message_delete_forever">Dauerhaft löschen</string>
|
||||
<string name="message_restore_success">Message restored successfully</string>
|
||||
<string name="message_delete_success">Nachricht erfolgreich gelöscht</string>
|
||||
<string name="message_mailbox_type_student">schüler</string>
|
||||
<string name="message_mailbox_type_parent">Eltern</string>
|
||||
@ -335,6 +342,7 @@
|
||||
<item quantity="other">%1$d ausgewählt</item>
|
||||
</plurals>
|
||||
<string name="message_messages_deleted">Nachrichten gelöscht</string>
|
||||
<string name="message_messages_restored">Messages restored</string>
|
||||
<string name="message_mailbox_chooser_title">Postfach auswählen</string>
|
||||
<string name="message_incognito_mode_on">Incognito mode is on</string>
|
||||
<string name="message_incognito_description">Thanks to incognito mode sender is not notified when you read the message</string>
|
||||
@ -634,6 +642,8 @@
|
||||
<string name="pref_view_grade_average_mode">Berechnete Durchschnittsoptionen</string>
|
||||
<string name="pref_view_grade_average_force_calc">Mittelwertberechnung durch App erzwingen</string>
|
||||
<string name="pref_view_present">Anwesendheit zeigen</string>
|
||||
<string name="pref_attendance_target">Attendance target</string>
|
||||
<string name="pref_view_attendance_calculator_sorting_mode">Attendance calculator sorting</string>
|
||||
<string name="pref_view_app_theme">Thema</string>
|
||||
<string name="pref_view_expand_grade">Steigende Sorten</string>
|
||||
<string name="pref_view_timetable_show_groups">Gruppen neben Schulfächen anzeigen</string>
|
||||
@ -756,7 +766,8 @@
|
||||
<string name="auth_description">To operate the application, we need to confirm your identity. Please enter the student\'s PESEL <b>%1$s</b> in the field below</string>
|
||||
<string name="auth_button_skip">Skip for now</string>
|
||||
<!--Captcha-->
|
||||
<string name="captcha_dialog_title">Verification is in progress. Wait…</string>
|
||||
<string name="captcha_dialog_title">VULCAN\'s website requires verification</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>
|
||||
<!--Errors-->
|
||||
<string name="error_no_internet">Keine Internetverbindung</string>
|
||||
@ -764,6 +775,7 @@
|
||||
<string name="error_account_inactive">This account is inactive. Try logging in again</string>
|
||||
<string name="error_timeout">Registrierungsverbindung fehlgeschlagen. Server können überlastet sein. Bitte versuchen Sie es später noch einmal</string>
|
||||
<string name="error_login_failed">Das Laden der Daten ist fehlgeschlagen. Bitte versuchen Sie es später noch einmal</string>
|
||||
<string name="error_password_invalid">Your password has expired or been changed. Please log in again</string>
|
||||
<string name="error_password_change_required">Passwortänderung für Registrierung erforderlich</string>
|
||||
<string name="error_service_unavailable">Wartung im Gange UONET + Klassenbuch. Versuchen Sie es später noch einmal</string>
|
||||
<string name="error_unknown_uonet">Unbekannter UONET + Registerfehler. Versuchen Sie es später erneut</string>
|
||||
|
@ -1,75 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string-array name="app_theme_entries" tools:ignore="InconsistentArrays">
|
||||
<item>Light</item>
|
||||
<item>Dark</item>
|
||||
<item>Black (AMOLED)</item>
|
||||
</string-array>
|
||||
<string-array name="app_language_entries">
|
||||
<item>System language</item>
|
||||
<item>Polski</item>
|
||||
<item>English</item>
|
||||
<item>Pусский</item>
|
||||
<item>Українська</item>
|
||||
<item>Deutsch</item>
|
||||
<item>Čeština</item>
|
||||
<item>Slovenčina</item>
|
||||
</string-array>
|
||||
<string-array name="services_interval_entries">
|
||||
<item>15 minutes</item>
|
||||
<item>30 minutes</item>
|
||||
<item>1 hour</item>
|
||||
<item>2 hours</item>
|
||||
<item>6 hours</item>
|
||||
<item>12 hours</item>
|
||||
<item>24 hours</item>
|
||||
</string-array>
|
||||
<string-array name="grade_modifier_entries">
|
||||
<item>0,00</item>
|
||||
<item>0,25</item>
|
||||
<item>0,33</item>
|
||||
<item>0,5</item>
|
||||
<item>0,75</item>
|
||||
</string-array>
|
||||
<string-array name="grade_sorting_mode_entries">
|
||||
<item>Alphabetically</item>
|
||||
<item>By date</item>
|
||||
<item>By average</item>
|
||||
</string-array>
|
||||
<string-array name="grade_color_scheme_entries">
|
||||
<item>Dzienniczek+</item>
|
||||
<item>Wulkanowy</item>
|
||||
<item>Grade colors in register</item>
|
||||
</string-array>
|
||||
<string-array name="default_expand_grade_entries">
|
||||
<item>Up to 1 at once</item>
|
||||
<item>Always expanded</item>
|
||||
<item>Unlimited expansions</item>
|
||||
</string-array>
|
||||
<string-array name="grade_average_mode_entries">
|
||||
<item>Average of grades only from selected semester</item>
|
||||
<item>Average of averages from both semesters</item>
|
||||
<item>Average of grades from the whole year</item>
|
||||
</string-array>
|
||||
<string-array name="timetable_show_gaps_entries">
|
||||
<item>Don\'t show</item>
|
||||
<item>Only between lessons</item>
|
||||
<item>Before and between lessons</item>
|
||||
</string-array>
|
||||
<string-array name="dashboard_tile_entries">
|
||||
<item>Lucky number</item>
|
||||
<item>Unread messages</item>
|
||||
<item>Attendance</item>
|
||||
<item>Lessons</item>
|
||||
<item>Grades</item>
|
||||
<item>Homework</item>
|
||||
<item>School announcements</item>
|
||||
<item>Exams</item>
|
||||
<item>Conferences</item>
|
||||
</string-array>
|
||||
<string-array name="hidden_settings_entries">
|
||||
<item>Bad attendance</item>
|
||||
<item>Notes</item>
|
||||
<item>Fake attendance</item>
|
||||
</string-array>
|
||||
</resources>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user